Skip to content
This repository has been archived by the owner on Dec 18, 2018. It is now read-only.

Negotiate + SignalR Service Support for the Java client #2882

Merged
merged 17 commits into from
Sep 5, 2018

Conversation

mikaelm12
Copy link
Contributor

@mikaelm12 mikaelm12 commented Aug 28, 2018

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

@@ -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'
Copy link
Contributor Author

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been approved 👍

Copy link
Contributor

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;
Copy link
Contributor Author

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

Copy link
Contributor

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;
Copy link
Contributor Author

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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

Copy link
Contributor

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)

Copy link
Member

@BrennanConroy BrennanConroy left a 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;
Copy link
Member

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) {
Copy link
Member

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){
Copy link
Member

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('?'));
Copy link
Member

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 '/'

Copy link
Member

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");
Copy link
Member

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;
Copy link
Member

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");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dupe code

Copy link
Contributor Author

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());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for?

Copy link
Contributor Author

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

Copy link
Member

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)
Copy link
Member

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")){
Copy link
Member

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);
Copy link
Member

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?

Copy link
Contributor Author

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

@mikaelm12
Copy link
Contributor Author

So I was testing this on Android and it turns out that the Apache HttpClient library isn't supported.
https://developer.android.com/about/versions/marshmallow/android-6.0-changes#behavior-apache-http-client

I'm looking into using Squares OkHttp Library as a replacement.

@davidfowl
Copy link
Member

OkHttp is the pretty common android replacement. Grpc uses it https://github.com/grpc/grpc-java/tree/master/okhttp

@mikaelm12
Copy link
Contributor Author

OkHttp is the pretty common android replacement.

Yeah, should have just started there. Hindsight is 20/20 I guess.

return result.toString();
}

private static String resolveNegotiateUrl(String url){
Copy link
Member

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

@@ -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'
Copy link
Member

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

Copy link
Contributor Author

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){
Copy link
Member

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"));
Copy link
Member

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

@mikaelm12
Copy link
Contributor Author

android-signalrservice

😄

@mikaelm12
Copy link
Contributor Author

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

@mikaelm12
Copy link
Contributor Author

Updated

url = WS + url.substring(HTTP.length());
}
if (url.indexOf(ACCESS_TOKEN_PREFIX) > 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this exist?

Copy link
Contributor Author

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

Copy link
Member

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) {
Copy link
Member

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?

Copy link
Contributor Author

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[]{});
Copy link
Member

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?

Copy link
Member

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?

Copy link
Contributor Author

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)

Copy link
Contributor Author

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

Copy link
Contributor

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:

const headers = request.headers;
if (headers) {
Object.keys(headers)
.forEach((header) => {
xhr.setRequestHeader(header, headers[header]);
});
}

@mikaelm12
Copy link
Contributor Author

Ping

Copy link
Member

@BrennanConroy BrennanConroy left a 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() {
Copy link
Member

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() {
Copy link
Member

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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove

@mikaelm12
Copy link
Contributor Author

There are no tests that check the redirect url behavior.

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

return new NegotiateResponse(result);
}

private static String resolveNegotiateUrl(String url) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this this this

@analogrelay
Copy link
Contributor

Formatting changes made a bunch of noise in HubConnection unfortunately :(

@mikaelm12
Copy link
Contributor Author

Final reviews?

Copy link
Member

@BrennanConroy BrennanConroy left a 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) {
Copy link
Member

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

@mikaelm12 mikaelm12 merged commit dc6088b into release/2.2 Sep 5, 2018
@BrennanConroy BrennanConroy deleted the mikaelm12/Negotiate branch September 6, 2018 17:18
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants