Skip to content

[io/http] Add a HTTP Client parameter on WebSocket.connect to allow a custom HTTP Client for web socket connections. #46040

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 14 commits into from
Closed

Conversation

rnewquist
Copy link
Contributor

@rnewquist rnewquist commented May 17, 2021

The WebSocket abstract class was changed to allow an optional parameter called customClient that takes in a HTTPClient and passes it to the WebSocket Implementation.
The WebSocket implementation takes the customClient, checks if its null, if its not null, it uses the customClient in place of the static HTTPClient that the WebSocket Implementation offers.
This custom client does not override the static HTTPClient, so all previous functionality remains the same when the customClient is not present.

TEST=testStaticClientUserAgentStaysTheSame() in web_socket_test.dart in standalone_2/standalone
TEST=new SecurityConfiguration(secure: true).runTests(); in web_socket_error_test.dart and web_socket_test.dart in standalone_2/standalone

Bug: #34284

@google-cla google-cla bot added the cla: yes label May 17, 2021
@rnewquist
Copy link
Contributor Author

@athomas it was such minor code, I just created a new PR.

@rnewquist rnewquist changed the title exposed http client exposed http client for web sockets May 17, 2021
@rnewquist
Copy link
Contributor Author

This fixes #34284 by opening up the http client used in the web socket code, allowing users to add a custom Security Context and cert failure callback to the web socket. Currently, the only solution is to create a global http override, which isn't an ideal solution.

@mit-mit
Copy link
Member

mit-mit commented Aug 3, 2021

Code review is https://dart-review.googlesource.com/c/sdk/+/200262

@a-siva
Copy link
Contributor

a-siva commented Aug 3, 2021

The signature of connect has been changed in the implementation but this PR does not seem to make the corresponding change in the 'abstract class WebSocket' code in websocket.dart.
Another question, the code could have set a userAgent using the 'set userAgent' method, now with the static variable _httpClient being changed to this new value, the getter userAgent would return whatever the new client has set. Not sure if this would be a problem, should the userAgent set previously be transferred to the new object ?

@rnewquist
Copy link
Contributor Author

rnewquist commented Aug 3, 2021

@a-siva
The abstract webSocket shouldn't need changing, it's only the connection part that needs to have the changes. The abstract webSocket has the appropriate connections, it's literally the httpClient being a private final that is preventing any sort of certificate pinning.
As for the second question, the user agent would get replaced if the new httpClient contains a user agent, however if it doesn't, the user agent gets set to what ever the private http client was set to.
_httpClient = customClient..userAgent ??= _httpClient.userAgent;
When creating the httpClient, there is no default userAgent, it gets set to null, so the code above would replace the userAgent with an existing user agent or another null value if the incoming http client contains a null userAgent.
I suppose another approach would be to allow the httpClient to have exposed getters and setters for the webSocket as well. I'm not sure if that would cause an issue though as I have not tested that, but I can verify, at least for my use case, this method does work for certificate pinning.

HttpClient customClient}) {
//overriding _httpClient if user specifies a custom http client.
if (customClient != null) {
_httpClient = customClient..userAgent ??= _httpClient.userAgent;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think overriding static _httpClient here will be confusing because it means that if you call connect twice with different customClients the first customClient will be overridden with second one. Currently it will result in userAgent for the fist connect reporting the value from customClient of the last connect.

Copy link
Contributor Author

@rnewquist rnewquist Aug 3, 2021

Choose a reason for hiding this comment

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

I agree, however, with how its set up, it would require the httpClient to be tied to the webSocket connection itself (which I think it should be). However that problem exists in the current implementation does it not? So it doesn't solve that problem, but it does solve having a custom httpClient for all webSocket connections.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree, however, with how its set up, it would require the httpClient to be tied to the webSocket connection itself (which I think it should be).

Yeah, I think having httpClient as an instance field of _WebSocketImpl would be cleaner. It could default to static final HttpClient that can be used if user didn't provide customClient.

However that problem exists in the current implementation does it not?

Well, currently there is only one httpClient used by all web socket connections, so it being static is okay.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In that case, would it be best to create a separate ticket for instancing the httpClient with the webSocket? That said, I think that it would be much more clear to the user to have getters and setters for the static httpClient instead of having it passed as a method parameter. If you guys agree, i'll update this PR to reflect that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If it wasn't clear by my proposal, having two separate tasks, one having the existing framework unchanged, but allowing the user to change the httpClient for all webSocket connections, and the other instancing the httpClient on a per webSocket connection basis. Since the first is literally making a private variable accessible, it shouldn't have much, if any, technical debt on the second task.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@aam I am not authorized to change the message in gerrit, so I updated the github message.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you! Looking at how this ends up being copied into Gerrit by Copybara, I think [io/http] <One-liner... should be put into the subject of this PR(replacing exposed http client for web sockets subject) leaving only <Paragarph... etc in the description.
Also word TEST in Test=... lines should be all capital and there should be no space around equal sign.

You can see how we attempt to follow this convention in https://github.com/dart-lang/sdk/commits/main

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@aam fixed, also added secure tests to web_socket_test.dart in both standalones.

Copy link
Member

Choose a reason for hiding this comment

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

FYI, if you start with a PR it's best to keep modifying the PR and just treat Gerrit as read-only (if you modify the review, that will likely break the automatic syncing from GitHub because of the conflicting edits).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@athomas I did not know that, sorry about that. I don't think I have any permission to edit the gerrit, so I "think" it should good, however I will keep that in mind for any future changes that I do.

@athomas
Copy link
Member

athomas commented Sep 9, 2021 via email

@rnewquist rnewquist changed the title exposed http client for web sockets [io/http] Add a HTTP Client parameter on WebSocket.connect to allow a custom HTTP Client for web socket connections. Sep 9, 2021
Future<WebSocket> createClient(int port) => secure
? WebSocket.connect('${secure ? "wss" : "ws"}://$HOST_NAME:$port/',
customClient: HttpClient(context: clientContext))
: WebSocket.connect('${secure ? "wss" : "ws"}://$HOST_NAME:$port/');
Copy link
Contributor

@aam aam Sep 9, 2021

Choose a reason for hiding this comment

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

perhaps this can be simplified by passing null customClient if secure is false?

Future<WebSocket> createClient(int port) => 
      WebSocket.connect('${secure ? "wss" : "ws"}://$HOST_NAME:$port/',
          customClient: secure ? HttpClient(context: clientContext): null);

Copy link
Contributor Author

@rnewquist rnewquist Sep 9, 2021

Choose a reason for hiding this comment

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

completely agree, i wanted to keep the same logic as the server but didn't notice they were calling different functions. I have the customUserAgent there now for the added test.

WebSocket.connect('${secure ? "wss" : "ws"}://$HOST_NAME:$port/');
Future<WebSocket> createClient(int port, {String customUserAgent}) =>
WebSocket.connect('${secure ? "wss" : "ws"}://$HOST_NAME:$port/',
customClient: secure
Copy link
Contributor

@aam aam Sep 9, 2021

Choose a reason for hiding this comment

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

Not sure if you are able to run the web_socket_test to troubleshoot the failure on Gerrit, hopefully you are.
Just to confirm - the issue here is that testShouldSetUserAgent when run with SecurityConfiguration secure:true fails due to the fact that global WebSocket.userAgent is not used in that setting - the connection is made using customClient with its userAgent.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am not able to actually test it. However, WebSocket.userAgent comes from a final static, unless something else is setting the user agent, it shouldn't it never get changed? Would be a lot easier if I could debug it though.

Copy link
Contributor

@aam aam Sep 9, 2021

Choose a reason for hiding this comment

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

You could try following https://github.com/dart-lang/sdk/wiki/Building to get the sdk built locally, test running.
Right, https://github.com/dart-lang/sdk/blob/main/tests/standalone/io/web_socket_test.dart#L550 from the test that fails is setting WebSocket.userAgent directly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Took a while to get it up and running, but it was a simple fix. Because of how the tests are ran and because WebSocket.userAgent comes from a static final, testShouldSetUserAgent and testStaticClientUserAgentStaysTheSame were setting the userAgent to different values at roughly the same time. By just making sure that they are the same value, the static final variable doesn't get changed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you, it looks like there are more failures due to use of WebSocket.connect() when secure is true but without customClient that is needed.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm going to push a fix to the test to this PR if you don't mind @rnewquist.
It's basically about several places in the test where WebSocket.connect is invoked without provided customClient which is needed when running in secure configuration

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@aam that would be perfect, I am a little busy this week, sorry about that.

@@ -1022,7 +1022,9 @@ class _WebSocketImpl extends Stream with _ServiceObject implements WebSocket {
path: uri.path,
query: uri.query,
fragment: uri.fragment);
return _httpClient.openUrl("GET", uri).then((request) {
// Use Custom HTTP Client first is it exists, else
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this comment can be omitted as it just describes what is happening on the next line.
Ideally comments are answering the question "why?", rather than "what?", because normally code answers "what?" question already.

@dart-bot dart-bot closed this in 2917c1c Sep 15, 2021
@aam
Copy link
Contributor

aam commented Sep 15, 2021

thank you @rnewquist once again!

@a-siva
Copy link
Contributor

a-siva commented Sep 17, 2021

I noticed that the status files has a skip
io/web_socket_error_test: Skip # Flaky
for [ $compiler == dartkp && $runtime == dart_precompiled ]
The new tests added here do not run in precompiled mode, is that intended ?
It appears the fix for setting up the resources done in
https://dart-review.googlesource.com/c/sdk/+/213524
does not work for configurations that have a two step process,
see #47239

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants