I'm currently trying to setup a web-socket server on an SAP application server as a proof of concept. The application which is connecting to the web-socket server is not going to be a UI5 or WebDynpro application but just a middle-ware program running on a headless computer.
Following a quick guide, I've setup the push channel and I have an object with the interface methods ON_START, ON_MESSAGE and etc. I'm currently testing the interface using wscat which you can get from npm.
When I tried connecting to my service for the first time using wscat I was receiving a HTTP 500 error.
I wasn't sure why I was getting the 500 error, so I tried to access the URL via http and a web browser to see what was happening.
500 SAP Internal Server Error
ERROR: Cross-Site Request Forgery (XSRF) check has failed ! (termination: ABORT_MESSAGE_STATE)
I had seen these tokens also in use by Gateway services, so I had created a quick gateway service and sent a GET request with X-CSRF-Token: Fetch except the token that I get from this doesn't work when I attempt to use uri parameter sap-XSRF.
Going forward, I started to debug CL_APC_MANAGER function HANDLE_REQUEST to see if my request comes in at all. I also wanted to trace where the origin of the 500 error comes from. I've managed to trace it back to CL_APC_MANAGER method CHECK_XSRF.
METHOD check_xsrf.
DATA: lv_xsrf_token TYPE string.
*
* validate XSRF token
*
lv_xsrf_token = i_server->request->get_form_field( name = if_http_form_fields_sap=>sap_xsrf ).
IF lv_xsrf_token IS INITIAL.
lv_xsrf_token = i_server->request->get_header_field( name = if_http_form_fields_sap=>sap_xsrf ).
ENDIF.
IF lv_xsrf_token IS INITIAL.
r_successful = abap_false.
ELSE.
CALL METHOD i_server->validate_xsrf_token
EXPORTING
token = lv_xsrf_token
IMPORTING
successful = r_successful
EXCEPTIONS
token_not_found = 1
cookie_not_found = 2
internal_error = 3
called_by_public_service = 4
OTHERS = 5.
IF sy-subrc <> 0 OR abap_false = r_successful.
r_successful = abap_false.
ELSE.
r_successful = abap_true.
ENDIF.
ENDIF.
ENDMETHOD.
If I skip this check manually with the debugger, than I'm able to connect to my web-socket server without a problem.
However I'm not sure at all how I'm actually supposed to get this token before attempting to connect. I noticed the XSRF Tokens are saved in database table SECURITY_CONTEXT. The only problem is an entry is created in this table with the key I need to have after I attempt to connect. I need it before and I'm not sure what the procedure is for retrieving a token properly.
Is there anybody with previous experience using these that can shed some light? Thanks in advance.
EDIT I'm using Version 740 with Service Pack 4.
The "correct" way to do have the header generated correctly is by maintaining table APC_CROSS_ORIGIN (transaction SAPC_CROSS_ORIGIN).
WebSockets functionality was only released for customer use in 7.40SP5, which probably explains why you don't have that table in your system. I'd recommend using your workaround for now, until your system has been patched.
Related
Let's say I have a bunch of clients who all have their own numeric IDs. Each of them connect to my server through SockJS, with something like:
var sock = new SockJS("localhost:8080/sock/100");
In this case, 100 is that client's numeric ID, but it could be any number with any number of digits. How can I set up a SockJS router in my server-side code that allows for the client to set up a SockJS connection through a URL that varies based on what the user's ID is? Here's a simplified version of what I have on the server-side right now:
public void start() {
HttpServer server = vertx.createHttpServer();
SockJSHandler sockHandler = SockJSHandler.create(vertx);
router.route("/sock/*").handler(sockHandler);
server.requestHandler(router::accept).listen(8080);
}
This works fine if the client connects through localhost:8080/sock, but it doesn't seem to work if I add "/100" to the end of the URL. Instead of getting the default "Welcome to SockJS!" message, I just get "Not Found." I tried setting a path regex and I got an error saying that sub-routers can't use pattern URLs. So is there some way to allow for the client to connect through a variable URL, whether it's /sock/100, /sock/15, or /sock/1123123?
Ideally, I'd be able to capture the numeric ID that the client uses (like with routing REST API calls, when you could add "/:ID" to the routing path and then capture the value that the client uses), but I can't find anything that works for SockJS connections.
Since it seems that SockJS connections are considered to be the same as sub-routers, and sub-routers can't have pattern URLs, is there some work-around for this? Or is it not possible?
Edit
Just to add to what I said above, I've tried a couple different things which haven't seemed to work yet.
I tried setting up an initial, generic main router, which then re-directs to the SockJS handler. Here's the idea I had:
router.routeWithRegex("/sock/\\d+").handler(context -> {
context.reroute("/final");
});
router.route("/final").handler(SockJSHandler.create(vertx));
With this, if I access localhost:8080/sock/100 directly through the browser, it takes me to the "Welcome to SockJS!" page, and the Chrome network tab shows that a websocket connection has been created when I test it through my client.
However, I still get an error because the websocket shows a 200 status code rather than 101, and I'm not 100% sure as to why that is happening, but I would guess that it has to do with the response that the initial handler produces. If I try to set the initial handler's status code to 101, I still get an error, because then the initial handler fails.
If there's some way to work around these status codes (it seems like the websocket is expecting 101 but the initial handler is expecting 200, and I think I can only pick one), then that could potentially solve this. Any ideas?
I'm getting rejected only on the accountSummaries/list management call, everything else works fine - heck, it works even when executing it from the reference page! I double checked that the account being used was correct and, as I said, I have no issues performing the simple accounts/list call.
I'm using the python library, and for both of those calls no parameters are needed (so the chance of some embarrassing error are very low).
Basically I'm simply getting the service client and performing the simplest possible call:
a = client.AnalyticsManagementClient() # super simple wrapper
a._service.management().accounts().list().execute()
a._service.management().accountSummaries().list().execute()
The first call works fine, the second one returns a 403 error. Anyone have an idea why that might happen?
Full error is HttpError: <HttpError 403 when requesting https://www.googleapis.com/analytics/v3/management/accountSummaries?alt=json returned "Insufficient Permission">
It was just a scope issue: accounts needs at least one among
https://www.googleapis.com/auth/analytics
https://www.googleapis.com/auth/analytics.edit
https://www.googleapis.com/auth/analytics.readonly
while accountSummaries allows only the last two; it seems to be the only one that does not work with the analytics scope, which is the one our client was requesting.
The following code should post a form to an endpoint (which returns 302) and, after following the redirect, parse the url of the page and return some information from there.
val start = System.currentTimeMillis()
val requestHolder = WS.url(conf("login.url"))
.withRequestTimeout(loginRequestTimeOut)
.withFollowRedirects(true) //This appears to have no effect...
requestHolder.post(getMap(username, password))
.map(resp =>{
Logger.debug(resp.status.toString)
val loginResponse = getResponse(resp)
val end = System.currentTimeMillis()
Logger.debug("Login for the user: "+username+", request took: " + (end - start) + " milliseconds.")
loginResponse
})
The problem is that .withFollowRedirects(true) appears to have no effect on the query. The status of the response is 302 and the request does not follow the redirect.
I've gone through the process manually using httpie and following the redirects does lead to the correct page.
Any help or insight would be much appreciated.
POST redirection isn't as well supported as GET redirection. W3 specification says:
If the 301 status code is received in response to a request other than GET or HEAD, the user agent MUST NOT automatically redirect the request unless it can be confirmed by the user, since this might change the conditions under which the request was issued.
Some browsers don't do that, and just ignore. Have a look also at the 307 status:
307 Temporary Redirect (since HTTP/1.1)
In this case, the request should be repeated with another URI; however, future requests should still use the original URI. In contrast to how 302 was historically implemented, the request method is not allowed to be changed when reissuing the original request. For instance, a POST request should be repeated using another POST request.
There is also a discussion about this on Programmer Stack Exchange.
I've had a lot of trouble with withFollowRedirects and POST.
At some point, while fighting to make things work, I had .withFollowRedirects(false) in my code, then removed it during cleanups & things broke. My current guess is that if this option is not explicitly made false, the default behavior is to follow redirects (302 in my case) with some faulty mechanism. Perhaps the default mechanism uses POST again with same arguments. But in my case, interacting with Google App Script (GAS), one needs to use GET to retrieve JSON output of a POST.
Whatever the mechanism was doing, I was getting 400 with no further diagnostics.
After wasting hours, I realized that .withFollowRedirects(false) was in fact truly needed: it disabled Play's messing with redirects, I was able to see the 302 response & handle the following GET manually with success.
When I send GET http requests to an EJB served by jetty, I often get a 401 response even though the auth parameters are correct.
When I look into jetty logs I see this :
2013-06-27 11:54:11.004:DBUG:oejs.Server:REQUEST /app/general/launch on AsyncHttpConnection#3adf0ddc,g=HttpGenerator{s=0,h=-1,b=-1,c=-1},p=HttpParser{s=-5,l=34,c=0},r=1
2013-06-27 11:54:11.021:DBUG:oejs.Server:RESPONSE /app/general/launch 401
2013-06-27 11:54:11.066:DBUG:oejs.Server:REQUEST /app/general/launch on AsyncHttpConnection#3adf0ddc,g=HttpGenerator{s=0,h=-1,b=-1,c=-1},p=HttpParser{s=-5,l=102,c=0},r=2
I suspect that the request is not fully read (too large request entity or too large headers?)
as it is parsed twice for a single request. Is there a way to fix this ?
what does HttpParser{s=-5,l=34,c=0} and HttpParser{s=-5,l=102,c=0} mean ?
when I desactivate authentication (security constraints using simple jetty realm). the request is only parsed once.
401 means that the server requires authentication credentials that the client either has not sent or the ones sent by the client have not been authorized.
Some client implementations will resend the request if they receive a 401 including the credentials. If your client is doing that, that would explain why you get the request twice on the server.
The HttpParser toString() method returns the current status of the HttpParser. Here's the code:
return String.format("%s{s=%d,l=%d,c=%d}",
getClass().getSimpleName(),
_state,
_length,
_contentLength);
So s is the state. -5 is STATE_HEADER. And l and c represent the length and the contentLength.
I'm writing a REST based web service, and I'm trying to figure out the best way to handle error conditions.
Currently the service is returning HTTP Errors, such as Bad Request, but how can I return extra information to give developers using the web service an idea what they're doing wrong?
For example: creating a user with a null username returns an error of Bad Request. How can I add that the error was caused by a null username parameter?
According to the HTTP spec, the text that comes after the three digit response code, the "Reason-Phrase", can only be replaced with a logical equivalent. So you can't respond with 400 null user and expect anything useful to happen. Indeed, The client is not required to examine or display the Reason- Phrase.
In general, the HTTP response entity (typically the page that accompanies the response) should contain information useful to the client to guide them forward, even when the response is an error. On the web, most such errors are HTML, and are devoid of machine readable information, but most browsers do show the error to the user (and SO's error page is pretty good!).
So for a primarily machine readable resource you have two options:
Pass a human readable message anyway. Return 400 Bad Request with a HTML response, which the client may opt to show to the user. It's dead easy but it's a bit like throwing an unchecked exception, it passes all the hard work to the client, or indeed the end user.
Allow clients to recover. Return 400 Bad Request with a machine readable response which is part of your API, so clients can recover from known error conditions. This is harder, like throwing a checked exception, it becomes part of the API, and it allows clients to recover gracefully if they want to.
You could even make the server support both scenarios by defining a media type for the machie readable error recovery document, and allow clients to "accept" them: Accept: application/atom+xml, application/my.proprietary.errors+json
Clients that forget the mandatory field can opt in to getting machine readable errors or human readable errors by choosing to Accepting the error media type.
It's stated in the HTTP spec that most error codes should return some basic text that gives a clarification of why the error is being returned. The basic Java Servlet Spec defines the HttpServletResponse.sendError(int Code, String message) for this purpose.
String desc = "my Description";
throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(desc).type("text/plain").build());