Error 404 when calling Google Cloud Endpoint API from Google Apps Script - google-app-engine

I am trying to call a Google Cloud Endpoint API (developed on App Engine) via Google Apps Script. The endpoint is up and running, honestly I don't know which URL I should use but through Google Chrome Web Tools it looks like the URL is something like:
https://myapp.appspot.com/_ah/api/myendpointapi/v1/myEndPointMethod/
Along with API parameters directly included in the URL, separeted by slashes:
https://myapp.appspot.com/_ah/api/myendpointapi/v1/myEndPointMethod/param1value/param2value/...
Now, in order to call that API from Google App Script I am using the following code snippet:
function myFunction() {
var params =
{
"param1" : "param1value",
"param2" : "param2value",
};
var result = UrlFetchApp.fetch('https://myapp.appspot.com/_ah/api/myendpointapi/v1/myEndPointMethod/', params);
DocumentApp.getUi().alert(result);
}
However I always get a 404 error. If I have to be honest I don't even know if UrlFetchApp is the correct way of calling the API. I noticed this thread on StackOverflow but no one answered. What's the correct URL to use? Many thanks.
EDIT: Now I am trying with an API method which does not require any parameter. I found a way to call a specific URL (using method='get' as suggested by the answer below) but now I get a 401 error because it says I am not logged in. I believe I need to use some kind of OAuth parameter now. Any idea? I tryed using OAuthConfig but no luck with that as well :( From App Engine logs I can see the following error:
com.google.api.server.spi.auth.GoogleIdTokenUtils verifyToken: verifyToken: null
com.google.api.server.spi.auth.AppEngineAuthUtils getIdTokenEmail:
getCurrentUser: idToken=null
function myFunction() {
var result = UrlFetchApp.fetch('myurl', googleOAuth_());
result = result.getContentText();
}
function googleOAuth_() {
var SCOPE = 'https://www.googleapis.com/auth/drive';
var NAME = 'myAPIName';
var oAuthConfig = UrlFetchApp.addOAuthService(NAME);
oAuthConfig.setRequestTokenUrl('https://www.google.com/accounts/OAuthGetRequestToken?scope='+SCOPE);
oAuthConfig.setAuthorizationUrl('https://www.google.com/accounts/OAuthAuthorizeToken');
oAuthConfig.setAccessTokenUrl('https://www.google.com/accounts/OAuthGetAccessToken');
oAuthConfig.setConsumerKey('anonymous');
oAuthConfig.setConsumerSecret('anonymous');
return {oAuthServiceName:NAME, oAuthUseToken:'always'};
}

UrlFetchApp is the only way to call a Google Cloud Endpoints API at the moment. The second parameter to UrlFetchApp.fetch is a special key-value map of advanced options. To pass POST parameters, you need to do the following:
UrlFetchApp.fetch(url, {
method: 'post',
payload: {
"param1" : "param1value",
"param2" : "param2value",
}
});

I was fighting a similar (not the same) problem, when testing feasibility of a GCM backed by EndPoints server. Basically testing if it is possible to get the Google Spreadsheet Appscript to send notification to an Android device. Please bear with me, the following explanation may be a bit convoluted;
Starting with a standard 'Cloud Messaging for Android', backed by the 'App Engine Backend with Google Cloud Messaging', I managed to build a test system that would send messages between Android devices (Github here).
Here is a VERY sparse EndPoints server code that handles register / un-register Android devices, as well as reporting registered devices and sending a message to a list of registered devices.
WARNING: This is not a production quality code, it is stripped of any logging, error handling in order to keep it short.
#Api( name = "gcmEP", version = "v1",
namespace = #ApiNamespace(ownerDomain = "epgcm.example.com", ownerName = "epgcm.example.com", packagePath = "" )
)
public class GcmEP {
#ApiMethod(name = "registToken")
public void registToken(#Named("token") String token) {
if (ofy().load().type(TokenRec.class).filter("token", token).first().now() == null) {
ofy().save().entity(new TokenRec(token)).now();
}
}
#ApiMethod(name = "unregToken")
public void unregToken(#Named("token") String token) {
TokenRec record = ofy().load().type(TokenRec.class).filter("token", token).first().now();
if (record != null) {
ofy().delete().entity(record).now();
}
}
#ApiMethod(name = "listTokens")
public CollectionResponse<TokenRec> listTokens() {
return CollectionResponse.<TokenRec>builder().setItems(ofy().load().type(TokenRec.class).list()).build();
}
#ApiMethod(name = "sendMsg")
public void sendMsg(#Named("message") String message) throws IOException {
if (message != null && message.length() > 0) {
Sender sender = new Sender(System.getProperty("gcm.api.key"));
Message msg = new Message.Builder().addData("message", message).build();
for (TokenRec record : ofy().load().type(TokenRec.class).list()) {
Result result = sender.send(msg, record.getToken(), 4);
if (result.getMessageId() != null) {
// handle CanonicalRegistrationId
} else {
// handle errors, delete record
}
}
}
}
}
Android code for registration and message sending is shown here, even if it is not relevant.
GcmEP mRegSvc;
String mToken;
// register device on EndPoints backend server
private void registerMe() {
new Thread(new RegisterMe(this)).start();
}
private class RegisterMe implements Runnable {
Activity mAct;
public RegisterMe(Activity act) { mAct = act; }
public void run() {
String senderId = null;
if (mAct != null) try {
if (mRegSvc == null) {
mRegSvc = new GcmEP
.Builder(AndroidHttp.newCompatibleTransport(), new AndroidJsonFactory(), null).setRootUrl(UT.ROOT_URL).build();
}
senderId = getString(R.string.gcm_defaultSenderId);
mToken = InstanceID.getInstance(mAct).getToken(senderId, GoogleCloudMessaging.INSTANCE_ID_SCOPE, null);
mRegSvc.registToken(mToken).execute();
GcmPubSub.getInstance(mAct).subscribe(mToken, "/topics/global", null); // subscribing to all 'topics' from 'mToken'
} catch (IOException e) { e.printStackTrace(); }
}
}
// send message to EndPoints backend server
new Thread(new Runnable() {
#Override
public void run() {
if (mRegSvc != null) try {
mRegSvc.sendMsg("hello").execute();
} catch (IOException e) { e.printStackTrace(); }
}
}).start();
// receive GCM message
public class GcmListenSvc extends GcmListenerService {
#Override
public void onMessageReceived(String senderId, Bundle data) {
Log.i("_X_", data.getString("message"));
}
}
What is relevant, thought, there is also an APIs Explorer created for the project, that can be used to send messages to your Android device from any browser.
If you use this Explorer, you can see the GET, POST requests for your EndPoints backend server, i.e.
list all registered devices:
GET https://epgcm.appspot.com/_ah/api/gcmEP/v1/tokenrec?fields=items
send a message to all registered devices:
POST https://epgcm.appspot.com/_ah/api/gcmEP/v1/sendMsg/Hello%20World!
Now, you can use this knowledge to send messages to your Android device from an AppScript code as shown:
Version 1: Get list of registered devices and send a GCM message to all of them (or a filtered set).
function sendMsg() {
var msg = 'test from CODE.GS';
var url = 'https://epgcm.appspot.com/_ah/api/gcmEP/v1/tokenrec?fields=items';
var params = { method : 'get'};
var response = UrlFetchApp.fetch(url, params);
var data = JSON.parse(response.getContentText());
var regIds = [];
for (i in data.items)
regIds.push(data.items[i].token);
var payload = JSON.stringify({
'registration_ids' : regIds,
'data' : { 'message' : msg }
});
var params = {
'contentType' : 'application/json',
'headers' : {'Authorization' : 'key=AIza............................'},
'method' : 'post',
'payload' : payload
};
url = 'https://android.googleapis.com/gcm/send';
UrlFetchApp.fetch(url, params);
}
This version relies on code from an old YouTube video, and I don't know if the call to 'android.googleapis.com' is still supported (but it works).
Version 2: Use the EndPoints's 'sendMsg' directly.
function sendMsg() {
var msg = 'test from CODE.GS';
var params = { method : 'post'};
var url = 'https://demoepgcm.appspot.com/_ah/api/gcmEP/v1/sendMsg/' + encodeURIComponent(msg.trim());
UrlFetchApp.fetch(url, params);
}
I have to admit I've never written a line of JavaScript code before, so it may not be up-to-par, but I made it work as a 'proof of concept'.
I would like to get feedback about this problem from people-who-know, since there is so little published info on this specific issue.

Related

Authorization code flow with Identitity4 and OidcClient

For a Winforms Desktop application I will use the authorization code flow with PKCE. As Identity provider I use IdentityServer and as client library OicdClient.
Next step I have to decide which Browser to use for the user login:
SystemBrowser
Extended WebBrowser
For SystemBrowser speaks the simple/clear implementation of the flow.
For Extended WebBrowser speaks that some user may have no SystemBrowser. But the WebBrowser is an older IE version? and is it allowed to use for a secure authentication?
Nevertheless I tried the "Extended WebBrowser" Sample and stumble integrating it in to my prototype Environment with own IS4 server. Therefore I need some clarity with the code flow and redirect.
I already had implemented this authorization code flow with pure .Net classes, but using OicdClient makes me little confusing(in the beginning like a black box).
My question is how does the redirect work with this libraries and who are responsible for redirect and who are responsible to receive the redirect with the code (to exchange for access token)?
The code flow has following steps (without details like clientID, PKCE ...):
Send a code request to IS4
IS4 Response with a login page (shown in a Browser)
After successful login IS4 sends to redirect URL with code
A HttpListener receives this redirect with code and
Send a request to IS4 with the code to receive a access token
With OidcClient and using the Automatic Mode:
var options = new OidcClientOptions
{
Authority = "https://demo.identityserver.io",
ClientId = "native",
RedirectUri = redirectUri,
Scope = "openid profile api",
Browser = new SystemBrowser()
};
var client = new OidcClient(options);
var result = await client.LoginAsync();
Here is to much magic for me. Only a call to LoginAsync() makes it work...
An important point seems to be the Browser property of the options with the IBrowser interface and its implementation of this method:
public async Task<BrowserResult> InvokeAsync(BrowserOptions options, CancellationToken cancellationToken)
{
using (var listener = new LoopbackHttpListener(Port, _path))
{
OpenBrowser(options.StartUrl);
try
{
var result = await listener.WaitForCallbackAsync();
if (String.IsNullOrWhiteSpace(result))
{
return new BrowserResult { ResultType = BrowserResultType.UnknownError, Error = "Empty response." };
}
return new BrowserResult { Response = result, ResultType = BrowserResultType.Success };
}
catch (TaskCanceledException ex)
{ ....}
}
}
if I try to map to the flow steps:
Login page: OpenBrowser(options.StartUrl);
Redirect will be done by IS4? The SystemBrowser from sample does not do this.
Receive the code: await listener.WaitForCallbackAsync();
1 and 5 are probably done by the OicdClient. This Example is fairly clear, need confimation that redirect is done by IS4.
The implementation in the other example Extended WebBrowser
public async Task<BrowserResult> InvokeAsync(BrowserOptions options, CancellationToken cancellationToken = default(CancellationToken))
{
using (var form = _formFactory.Invoke())
using (var browser = new ExtendedWebBrowser()
{
Dock = DockStyle.Fill
})
{
var signal = new SemaphoreSlim(0, 1);
var result = new BrowserResult
{
ResultType = BrowserResultType.UserCancel
};
form.FormClosed += (o, e) =>
{
signal.Release();
};
browser.NavigateError += (o, e) =>
{
e.Cancel = true;
if (e.Url.StartsWith(options.EndUrl))
{
result.ResultType = BrowserResultType.Success;
result.Response = e.Url;
}
else
{
result.ResultType = BrowserResultType.HttpError;
result.Error = e.StatusCode.ToString();
}
signal.Release();
};
browser.BeforeNavigate2 += (o, e) =>
{
var b = e.Url.StartsWith(options.EndUrl);
if (b)
{
e.Cancel = true;
result.ResultType = BrowserResultType.Success;
result.Response = e.Url;
signal.Release();
}
};
form.Controls.Add(browser);
browser.Show();
System.Threading.Timer timer = null;
form.Show();
browser.Navigate(options.StartUrl);
await signal.WaitAsync();
if (timer != null) timer.Change(Timeout.Infinite, Timeout.Infinite);
form.Hide();
browser.Hide();
return result;
}
}
Done by: browser.Navigate(options.StartUrl);
Redirect by IS4
Receive of code in event handle: NavigateError ???
Is here something wrong?
On IS4 the AccountController.Login is called
that calls /connect/authorize/callback? with the redirect_uri.
But this doesn't come to BeforeNavigate2. instead NavigateError event appears where the result set to:
result.ResultType = BrowserResultType.Success;
result.Response = e.Url;
Current best practice is to use the user's default web browser and not to embed a browser component. As for how to implement that - since you can't intercept browser navigation events using this approach you'd need to implement an HTTP listener that can accept the POST request from your identityserver4 implementation.
Have a read of this: https://auth0.com/blog/oauth-2-best-practices-for-native-apps/
And this RFC: https://www.rfc-editor.org/rfc/rfc8252

SignalR + WebAPI + AngularJS + HubContext, Client's still don't receive messages

For the last few days I have been trying to implement SignalR into my AngularJS/WebAPI application.
I have been able to successfully send/receive messages from client to client, however when I push messages purely from the Server, none of the clients receive any messages.
I have seen many people having the same problem, the answer always seems to be using GlobalHost.ConnectionManager.GetHubContext which I have been implementing, without error, however the clients still don't receive any of the messages.
I thought that perhaps it's because WebAPI calls are asynchronous and and therefore takes place on a different thread, but I can't be sure. Please could someone have a look and tell me what I'm doing wrong.
This is my Hub Class:
public class ChatHub : Hub
{
public void Send(string name, string message)
{
// Call the broadcastMessage
Clients.All.broadcastMessage(name, message);
}
public void RunMe()
{
System.Diagnostics.Debug.WriteLine("Client Started");
}
public static void Notify(string name, string message)
{
var hubContext = GlobalHost.ConnectionManager.GetHubContext<ChatHub>();
hubContext.Clients.All.broadcastMessage(name, message);
}
}
This is the Angular Controller in Javascript:
$scope.chat = $.connection.chatHub;
$scope.chat.client.broadcastMessage = function (name, message) {
$scope.$apply(function () {
var scope = angular.element($('#discussion')).scope();
scope.chatMessage = message;
alert(message);
});
};
$.connection.hub.start()
.done(function ()
{
console.log('Now connected, connection ID=' + $.connection.hub.id);
$scope.chat.server.runMe();
})
.fail(function(){ console.log('Could not Connect!'); });
$('#sendmessage').click(function () {
$scope.chat.server.send("SERVER", $('#inputMessage').val());
});
This is the Controller that is trying to notify the clients from the server
public class UserController : ApiController
{
#region METHODS
[ActionName("Create")]
[HttpPost]
public HttpResponseMessage Create(JObject parameters)
{
//DYNAMIC DATA
dynamic data = parameters;
//CHECK IF CALL FAILED
if (data == null)
return Request.CreateResponse(HttpStatusCode.InternalServerError, "Request is null");
//PERFORM REQUEST
using (var svc = new UserService())
{
//SET Parameters
String Username = data.Username;
String Password = data.Password;
//NOTIFY USERS
ChatHub.Notify("SERVER", "SERVER MESSAGE");
//CREATE Response
var response = svc.Create(Username, Password);
//RESPOND
return Request.CreateResponse(HttpStatusCode.OK, response);
}
}
}
So just to reiterate, when the "sendmessage" button is clicked on my UI, it sends a message to the server which is then received again by the clients, this works 100%,
However when I call the static Notify method from the Controller None of the clients receive any messages.
Calling the function does not return any errors.
Please could someone help me!
<!--Reference the SignalR library. -->
<script src="Scripts/jquery.signalR-2.2.0.min.js"></script>
Check your jQuery.signalR version.
If you are using dependency injection, the example at ASP.NET is wrong, you have to set your GlobalHost.DependendcyResolver in the the Global.asax file. not in the startup class.

Web API 405 Error with $http.post

I'm receiving a 405 error with a POST request using $http.post. What's weird is that I'm using $http.post in another area of my application and it works just fine.
I'm using AngularJS for client side, and Web API for server side. I've posted all relevant information (apart from my web.config) that I can think of. Is there something very obvious I'm missing here?
code below does not work (throws 405)
Here's the api controller method that I'm trying to hit:
public async Task<IHttpActionResult> LinkLogin(string provider)
{
Account user = await _repo.FindByNameAsync(User.Identity.Name);
if (user == null)
{
return BadRequest("User does not exist!");
}
return new ChallengeResult(provider, null, "auth/Manage/LinkLoginCallback", user.Id);
}
Here's how I'm trying to hit it on the client side:
var _linkLogin = function (provider) {
$http.post(serviceBase + 'auth/Manage/LinkLogin', provider).then(function (response) {
return response;
});
};
CODE BELOW IS CODE THAT WORKS
Api controller function that works:
// POST auth/Authorization/Register
[AllowAnonymous]
[Route("Register")]
public async Task<IHttpActionResult> Register(UserModel userModel)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
IdentityResult result = await _repo.RegisterUser(userModel);
IHttpActionResult errorResult = GetErrorResult(result);
if (errorResult != null)
{
return errorResult;
}
return Ok();
}
Calling it from the client side:
var _saveRegistration = function (registration) {
_logOut();
return $http.post(serviceBase + 'auth/Authorization/register', registration).then(function (response) {
return response;
});
};
Here is my web api configuration:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "AuthenticationApi",
routeTemplate: "auth/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Routes.MapODataServiceRoute("ODataRoute", "api", GenerateEdmModel());
var jsonFormatter = config.Formatters.OfType<JsonMediaTypeFormatter>().First();
jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
}
private static IEdmModel GenerateEdmModel()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
return builder.GetEdmModel();
}
}
Now I have tried a number of different solutions posted on the web to no avail, the following are links to things I have tried:
Web api not supporting POST method
Web API Put Request generates an Http 405 Method Not Allowed error
http://blog.dontpaniclabs.com/post/2013/01/23/That-Pesky-Requested-Resource-Does-Not-Support-HTTP-Method-POST-Error-When-Using-MVC-Web-API
I hate answering my own question. If anyone else runs into this issue it's because you're trying to send a simple string value to a web api controller.
I used this solution with success: http://jasonwatmore.com/post/2014/04/18/Post-a-simple-string-value-from-AngularJS-to-NET-Web-API.aspx
If the link is dead, you simple wrap the string value in double quotes in your POST request like so:
$http.post(Config.apiUrl + '/processfile', '"' + fileName + '"');

Host WebSocket Server in ASP.NET WebAPI on Console Application

I have built an ASP.NET WebAPI which is hosted on a console application. There are some web apis I had created and worked well. Then I tried to implement web socket service on it. The server side code was like below
[RoutePrefix("api/notification")]
public class NotificationController : ApiController
{
[HttpGet]
[Route("")]
public HttpResponseMessage Get()
{
if (HttpContext.Current.IsWebSocketRequest)
{
HttpContext.Current.AcceptWebSocketRequest(new NotificationWebSocketHandler());
return Request.CreateResponse(HttpStatusCode.SwitchingProtocols);
}
else
{
return Request.CreateResponse(HttpStatusCode.BadRequest);
}
}
public class NotificationWebSocketHandler : WebSocketHandler
{
private static WebSocketCollection _clients;
static NotificationWebSocketHandler()
{
_clients = new WebSocketCollection();
Task.Factory.StartNew(() =>
{
while (true)
{
Task.Delay(TimeSpan.FromSeconds(5));
if (_clients.Count > 0)
{
_clients.Broadcast(Guid.NewGuid().ToString());
}
}
});
}
public override void OnOpen()
{
_clients.Add(this);
Console.WriteLine("Web socket client opened, client count = {0}", _clients.Count);
}
public override void OnClose()
{
_clients.Remove(this);
Console.WriteLine("Web socket client closed, client count = {0}", _clients.Count);
}
}
}
But when I opened the client page (which was built on AngularJS) I got the error message said WebSocket connection to 'ws://10.222.115.220:8080/api/notification/' failed: Error during WebSocket handshake: Unexpected response code: 500
My client side code was
app.shared.controllers.controller('DashboardCtrl', function ($q, $scope) {
var ws = new WebSocket("ws://10.222.115.220:8080/api/notification/");
ws.onopen = function () {
console.log('web socket opened');
}
ws.onmessage = function (message) {
$scope.seed = message;
};
$scope.seed = '(empty)';
});
When I attached debug and added a breakpoint at the entry of my Get function, and I found the error 500 was raised without this breakpoint hit. I think this means the error was generated by WebAPI internally.
I'm not sure if anything wrong in my code, or WebSocket feature doesn't support console application host.
you are using the Web API in "Self Host"-mode? (hosted in the console)
There you have the problem that the object HttpContext.Current is "null".
This is a problem of self-hosting. HttpContext.Current is set while using the IIS (web-hosting). This property is not available during self-hosting. I think this could be your problem. (A null-ref-exception is thrown in your web-api-application and returns 500 - internal server error).

WPF and MVC4 Web API Internal Server Error 500 on POST

So I'm attempting to attach to a web api method via a WPF service, but get only a 500 error on anything other than a GET.
WPF call:
using (var client = new HttpClient())
{
var user = new MyUser
{
EntityID = Guid.NewGuid(),
FirstName = "WPF",
LastName = "test"
};
var formatter = new JsonMediaTypeFormatter();
HttpContent content = new ObjectContent<MyUser>(user, formatter);
client.BaseAddress = new Uri("http://localhost:19527/api/");
var response = await client.PostAsJsonAsync("MyUser", content);
//.ContinueWith((postTask) => result = (postTask.Result.Content == null) ? "Could not create user" : "User created successully!");
var r = response.StatusCode;
}'
...and the receiving controller:
public HttpResponseMessage Get(string badgeId)
{
return Request.CreateResponse<bool>(HttpStatusCode.OK, (service.UserByBadge(badgeId) != null));
}
public HttpResponseMessage Put(MyUser user)
{
return Request.CreateResponse<bool>(HttpStatusCode.OK, service.UpsertUser(user));
}
public HttpResponseMessage Post(MyUser user)
{
if (service.UpsertUser(user)) return Request.CreateResponse<MyUser>(HttpStatusCode.OK, service.Get<MyUser>(u => u.BadgeID == user.BadgeID));
return Request.CreateResponse<MyUser>(HttpStatusCode.NoContent, null);
}'
The service on the WebApi controller is a GenericRepository, which is working fine, since the Get method returns as expected. It's only when I use Post that I get the error. Debugging the methods throws the break point in the Get, but not in the Post, so I don't think it's ever being called.
Here's the route config:
routes.MapRoute(
name: "Default",
url: "api/{controller}/{action}/{id}",
defaults: new { controller = "{controller}", action = "{action}", id = UrlParameter.Optional }
);
I've tried different examples from other SO posts, but none appear to address this issue specifically. I'm guessing there's something wrong with how I've constructed the Post() method?
================================================================
RESOLUTION: Model being passed was failing property validations. Why this was causing a 500, not certain. But once I solved for this, API method began working.
If anybody has a "why" explanation, would love to know for future reference.

Resources