-
Notifications
You must be signed in to change notification settings - Fork 446
Negotiate + SignalR Service Support for the Java client #2882
Conversation
clients/java/signalr/build.gradle
Outdated
@@ -18,6 +18,7 @@ dependencies { | |||
testImplementation group: 'junit', name: 'junit', version: '4.12' | |||
implementation "org.java-websocket:Java-WebSocket:1.3.8" | |||
implementation 'com.google.code.gson:gson:2.8.5' | |||
compile 'org.apache.httpcomponents:httpclient:4.5.6' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New dependency for an implementation of an HttpClient
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has been approved 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like compile
has been deprecated and implementation
should be used instead (like the other dependencies)
|
||
public HubConnection(String url, Transport transport, Logger logger){ | ||
private static int MAX_NEGOTIATE_ATTEMPTS = 5; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just arbitrarily put 5 for the max. I think it's much higher for TS
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if(negotiateAttempts > 0 && this.negotiateResponse.accessToken != null){ | ||
this.accessToken = negotiateResponse.accessToken; | ||
this.negotiateResponse = Negotiate.processNegotiate(url, this.negotiateResponse.accessToken); | ||
this.url = this.url + "&id=" + negotiateResponse.connectionId + "&access_token=" + this.accessToken; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should I just store the accessToken
in the Authorization
header instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will just keep appending &id
and &access_token
as the client keeps getting redirected. I'm not sure the logic needs to be different based on which attempt we're on. The loop in the C# client
doesn't really do that. The initial URL and initial Access Token aren't relevant at all once the negotiate response comes in (#2859 notwithstanding, but we should come back to that)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests?
@@ -22,8 +22,13 @@ | |||
private HubConnectionState connectionState = HubConnectionState.DISCONNECTED; | |||
private Logger logger; | |||
private List<Consumer<Exception>> onClosedCallbackList; | |||
private boolean skipNegotiate = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
false by default
@@ -30,10 +31,15 @@ public HubConnectionBuilder configureLogging(Logger logger) { | |||
return this; | |||
} | |||
|
|||
public HubConnectionBuilder skipNeotiate(boolean skip) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove this, add it in a different PR when withUrl
takes options
public HubConnection(String url, Transport transport, Logger logger){ | ||
private static int MAX_NEGOTIATE_ATTEMPTS = 5; | ||
|
||
public HubConnection(String url, Transport transport, Logger logger, boolean skipNegotiate){ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove this overload
|
||
public static NegotiateResponse processNegotiate(String url, String accessTokenHeader) throws IOException { | ||
HttpClient client = HttpClientBuilder.create().build(); | ||
url = url.substring(0, url.indexOf('?')) + "negotiate" + url.substring(url.indexOf('?')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assumes the url ends with '/'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there some library code you can use instead of trying to correctly parse urls?
|
||
public static NegotiateResponse processNegotiate(String url) throws IOException { | ||
HttpClient client = HttpClientBuilder.create().build(); | ||
HttpPost post = new HttpPost(url + "/negotiate"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assumes there is no query string present
public NegotiateResponse(String negotiatePayload) { | ||
JsonObject negotiateResponse= jsonParser.parse(negotiatePayload).getAsJsonObject(); | ||
if (negotiateResponse.has("url")) { | ||
this.shouldRedirect = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unneeded, just check if the redirectUrl
is non-empty
return; | ||
} | ||
this.connectionId = negotiateResponse.get("connectionId").getAsString(); | ||
negotiateResponse.get("availableTransports"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dupe code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was debugging
|
||
public WebSocketTransport(String url, Logger logger) throws URISyntaxException { | ||
this.url = formatUrl(url); | ||
this.logger = logger; | ||
if (url.indexOf(ACCESS_TOKEN_PREFIX) > 0){ | ||
this.accessToken = url.substring(url.indexOf(ACCESS_TOKEN_PREFIX) + ACCESS_TOKEN_PREFIX.length()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was for when I was storing the accessToken
and then setting it in the headers. I removed that code but forgot this. But looks like I'll be going back down that route
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like it should be removed.
|
||
HubConnection hubConnection = new HubConnectionBuilder() | ||
.withUrl(input) | ||
.skipNeotiate(false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This shouldn’t be on by default
} | ||
negotiateAttempts++; | ||
} while (this.negotiateResponse.shouldRedirect && negotiateAttempts < MAX_NEGOTIATE_ATTEMPTS); | ||
if(!negotiateResponse.availableTransports.contains("WebSockets")){ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Formatting...
url = url.substring(0, url.indexOf('?')) + "negotiate" + url.substring(url.indexOf('?')); | ||
HttpPost post = new HttpPost(url); | ||
post.setHeader("Authorization", "Bearer " + accessTokenHeader); | ||
HttpResponse response = client.execute(post); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this http client support futures?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like there's an Async flavor of this HttpClient https://hc.apache.org/httpcomponents-asyncclient-ga/httpasyncclient/apidocs/org/apache/http/nio/client/HttpAsyncClient.html
I can update the Async issue to note that we should be using this once we start using CompletableFuture
So I was testing this on Android and it turns out that the Apache HttpClient library isn't supported. I'm looking into using Squares OkHttp Library as a replacement. |
OkHttp is the pretty common android replacement. Grpc uses it https://github.com/grpc/grpc-java/tree/master/okhttp |
Yeah, should have just started there. Hindsight is 20/20 I guess. |
return result.toString(); | ||
} | ||
|
||
private static String resolveNegotiateUrl(String url){ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: the formatting in this method
clients/java/signalr/build.gradle
Outdated
@@ -19,6 +19,7 @@ dependencies { | |||
implementation "org.java-websocket:Java-WebSocket:1.3.8" | |||
implementation 'com.google.code.gson:gson:2.8.5' | |||
implementation 'org.apache.httpcomponents:httpclient:4.5.6' | |||
implementation 'com.squareup.okhttp3:okhttp:3.11.0' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to do an OSS request
cc @Eilon
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Already sent an email before updating the PR
|
||
// Check if we have a query string. If we do then we ignore it for now. | ||
int queryStringIndex = url.indexOf('?'); | ||
if(queryStringIndex > 0){ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you have what I call @anurse virus. The if(
always touching 😄
return; | ||
} | ||
this.connectionId = negotiateResponse.get("connectionId").getAsString(); | ||
JsonArray transports = ((JsonArray)negotiateResponse.get("availableTransports")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Get rid of the extra parenthesis
Okay, this now works on Android and I no longer store the accessToken in the url. Moved to headers. And the formatting should be good too |
Updated |
url = WS + url.substring(HTTP.length()); | ||
} | ||
if (url.indexOf(ACCESS_TOKEN_PREFIX) > 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this exist?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We parse the access token from the url so we can put it in a header when we construct the underlying WebSocketClient. We actually know the accessToken before we even contruct the transport. I can add a ctor that takes headers and get rid of this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would be much cleaner.
this.transport = transport; | ||
} | ||
} | ||
|
||
public HubConnection(String url, Transport transport, Logger logger) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you add 2 constructors?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For testing. Once I add the skipNegotiate option to the builder I can clean this up
public static NegotiateResponse processNegotiate(String url, String accessTokenHeader) throws IOException { | ||
url = resolveNegotiateUrl(url); | ||
OkHttpClient client = new OkHttpClient(); | ||
RequestBody body = RequestBody.create(null, new byte[]{}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are the create arguments?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why aren’t we copying all of the headers here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are the create arguments?
create(MediaType contentType, byte[] content)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why aren’t we copying all of the headers here?
This seems to be the way we do this in the TS client
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a little hidden, but we do copy all the headers in the JavaScript client:
SignalR/clients/ts/signalr/src/HttpClient.ts
Lines 184 to 190 in 201c058
const headers = request.headers; | |
if (headers) { | |
Object.keys(headers) | |
.forEach((header) => { | |
xhr.setRequestHeader(header, headers[header]); | |
}); | |
} |
Ping |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are no tests that check the redirect url behavior.
public class NegotiateResponseTest { | ||
|
||
@Test | ||
public void VerifyNegoitateResponse() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Negotiate
} | ||
|
||
@Test | ||
public void VerifyRedirectNegoitateResponse() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Negotiate
onClosedCallbackList = new ArrayList<>(); | ||
} | ||
|
||
onClosedCallbackList.add(callback); | ||
} | ||
|
||
public void setSkipNegotiate(boolean skipNegotiate) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove
I wanted to add this but this requires adding infrastructure to allow passing in custom HttpClients for testing. That should happen eventually but not in this PR imo. Trying to stop this PR from growing any more |
clients/java/signalr/src/main/java/com/microsoft/aspnet/signalr/WebSocketTransport.java
Outdated
Show resolved
Hide resolved
return new NegotiateResponse(result); | ||
} | ||
|
||
private static String resolveNegotiateUrl(String url) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add some tests for this please. See https://github.com/aspnet/SignalR/blob/release/2.2/clients/ts/signalr/tests/Common.ts#L17 and https://github.com/aspnet/SignalR/blob/release/2.2/clients/ts/signalr/tests/HttpConnection.test.ts#L220 for some reference
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. 👍
if (!skipNegotiate) { | ||
int negotiateAttempts = 0; | ||
do { | ||
this.accessToken = (this.negotiateResponse == null) ? null : this.negotiateResponse.getAccessToken(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this this this
Formatting changes made a bunch of noise in HubConnection unfortunately :( |
Final reviews? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to be consistent with our use of this
return new NegotiateResponse(result); | ||
} | ||
|
||
public static String resolveNegotiateUrl(String url) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll need to look at the public API later
Added support for the negotiation dance between the client and server and also added support for the SignalR service.
Was originally going to make these two different PRs but I really wanted to see the Java client working with the service 😄
Issue: #2871
and issue: #2862
Need to add tests