Skip to content
This repository has been archived by the owner on Apr 14, 2023. It is now read-only.

Improvements for Authorization #53

Merged
merged 40 commits into from
Feb 5, 2017
Merged

Improvements for Authorization #53

merged 40 commits into from
Feb 5, 2017

Conversation

Urigo
Copy link
Contributor

@Urigo Urigo commented Dec 20, 2016

  • Use ws package instead of websocket (https://npmcompare.com/compare/websocket,ws)
  • Add onConnect
  • Add onDisconnect
  • Add onUnsubscribe
  • Add connectionParams support (like headers) to support authentication
  • Add support for rejection on new connection (to support authentication)
  • Add socket options to support port/path options
  • Unit tests

Related:

Sashko Stubailo and others added 26 commits December 20, 2016 18:39
public client: any;
public subscriptions: Subscriptions;
private url: string;
private maxId: number;
private connectionParams: ConnectionParams;
private subscriptionTimeout: number;
Copy link
Member

Choose a reason for hiding this comment

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

This is a pre-existing issue but one worth considering: if you use reconnect, then subscriptionTimeout (which cannot be disabled) seems broken. Like, any subscribe is going to time out if you call it while you're offline, even though you might later reconnect! Will apps actually respond usefully to failed subscriptions by trying to subscribe again later? Tracing the timeout error looks like it would eventually get passed to an onError handler passed to subscribeToMore, which neither your GS PR nor GitHunt-react passes. Hmm...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, need to handle this with onError, will update the GS PR.

- `options?: Object` : optional object to modify default client behavior
* `timeout: number` : how long the client should wait in ms for a subscription to be started (default 5000 ms)
- `options?: Object` : optional, object to modify default client behavior
* `timeout?: number` : how long the client should wait in ms for a subscription to be started (default 5000 ms)how long the client should wait in ms for a subscription to be started (default 5000 ms)
Copy link
Member

Choose a reason for hiding this comment

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

This line is doubled.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

Copy link
Member

Choose a reason for hiding this comment

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

👍

README.md Outdated
* `onSubscribe?: (message: SubscribeMessage, params: SubscriptionOptions, webSocketRequest: WebSocketRequest)` : optional method to create custom params that will be used when resolving this subscription
* `onSubscribe?: (message: SubscribeMessage, params: SubscriptionOptions, webSocket: WebSocket)` : optional method to create custom params that will be used when resolving this subscription
* `onUnsubscribe?: (webSocket: WebSocket)` : optional method that called when a client unsubscribe
* `onConnect?: (connectionParams: Object, webSocket: WebSocket)` : optional method that called when a client connects to the socket, called with the `connectionParams` from the client, the return value of this callback will be available as part of the `context` of the subscription. return `false` or throw and exception to reject the connection.
Copy link
Member

Choose a reason for hiding this comment

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

document that it may return a Promise?

Copy link
Member

Choose a reason for hiding this comment

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

also "an exception" not "and"

Copy link
Member

Choose a reason for hiding this comment

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

and be more specific about how the result is used: "if the return value is an object, its elements will be added to the context" or something

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated

Copy link
Member

Choose a reason for hiding this comment

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

👍


## Client-server messages
Each message has a type, as well as associated fields depending on the message type.
### Client -> Server

#### INIT
Copy link
Member

Choose a reason for hiding this comment

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

So interesting naming wrinkle here. The existing messages all start with SUBSCRIPTION_ and your new ones don't. Abstractly I think it's great to separate the INIT messages from the SUBSCRIPTION messages. But now SUBSCRIPTION_KEEPALIVE is weirdly named. Since we're doing enough backwards-incompatible stuff here maybe rename that one to KEEPALIVE?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renamed to KEEPALIVE

Copy link
Member

Choose a reason for hiding this comment

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

Good but there are a couple remaining references to SUBSCRIPTION_KEEPALIVE

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

Copy link
Member

Choose a reason for hiding this comment

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

👍

declare let window: any;
let _global = typeof global !== 'undefined' ? global : (typeof window !== 'undefined' ? window: {});

import * as NodeSocket from 'ws';
Copy link
Member

Choose a reason for hiding this comment

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

So the old websocket package has a browser field in its package.json which points to a tiny file which just exports the native WebSocket object. This means that when, eg, webpack (probably Meteor too, I only tested with GitHunt-react) bundles up subscriptions-transport-ws, it won't actually include all of the Node-oriented code from websocket in the client bundle.

That doesn't seem to work here: ws doesn't have the browser field so you're always going to bundle the entire ws library into the client bundle even though none of it is used. This seems bad. I think this would happen even if you converted the import into a require and put it under a conditional?

I think you can probably fix this by using the "browser" field yourself in this package's package.json somehow? I know we want to be able to use Client on the server too (for tests if nothing else) but there's probably some way to do it that keeps code splitting working.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The previous package, websocket had the logic that inside the file client-websocket internally, and this was the browser file.

I changed the usage so ws will be required only when there is no native WebSocket available, as suggested here: websockets/ws#837 , and also added browser property to expose ./dist/client.js only.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm. dist/client.js still pulls in dist/client-websocket.js which has a require('ws') written in it (as opposed to in the old websocket module whose lib/client.js literally doesn't mention require). Are you sure this will work properly when webpacked? I admit I forget how I tested this a few weeks ago...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure. I will verify it.

Copy link
Member

Choose a reason for hiding this comment

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

Please do verify this.

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 changed the implementation of the client package, and now the client package look for the native WebSocket object, and you can provide an optional implementation if you are using Node.
This way, ws package is not part of the Meteor client bundle, and not loaded at all.

Other option, is to use object for browser field and specify that ws is ignored for browsers. But this is not supported yet for Meteor build (meteor/meteor#6890).

src/server.ts Outdated
return;
}

// accept connection
const connection: Connection = request.accept(GRAPHQL_SUBSCRIPTIONS, request.origin);
request.resume();
Copy link
Member

Choose a reason for hiding this comment

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

Huh, we call resume synchronously with pause? That seems weird... why call pause in the first place? Also pause/resume is the ancient streams API, is this really the right thing to use?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah we don't need it at all.. removed the pause and resume

Copy link
Member

Choose a reason for hiding this comment

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

👍

case INIT:
let onConnectPromise = Promise.resolve(true);
if (this.onConnect) {
onConnectPromise = Promise.resolve(this.onConnect(parsedMessage.payload, connection));
Copy link
Member

Choose a reason for hiding this comment

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

It's good that onConnect doesn't have to be synchronous, but you shouldn't process other messages until it's done running! As far as I can tell more onMessages can be invoked while onConnect is running. (This should be easy to write a test for.)

Perhaps store the Promise that contains the initResult on this, and then it before doing anything else?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Copy link
Member

Choose a reason for hiding this comment

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

Would it be simpler to get rid of initResult and just resolve the initPromise with initResult directly? You don't use initResult anywhere that's not already inside a initPromise.then...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed! Thanks

Copy link
Member

Choose a reason for hiding this comment

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

👍

src/server.ts Outdated

case SUBSCRIPTION_START:
const baseParams: SubscriptionOptions = {
query: parsedMessage.query,
variables: parsedMessage.variables,
operationName: parsedMessage.operationName,
context: {},
context: Object.assign({}, this.initResult),
Copy link
Member

Choose a reason for hiding this comment

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

can't this.initResult be true? Maaaybe that works with Object.assign but it sure feels weird.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

Copy link
Member

@glasser glasser Jan 9, 2017

Choose a reason for hiding this comment

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

Really? Still looks the same to me...

Copy link
Member

Choose a reason for hiding this comment

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

Oh, I see, you only set initResult if it's an object. Maybe this is also the answer to my last question about why initResult isn't the value of initPromise? (But you still could probably make it be the value of initPromise by converting non-objects to {}.)

Copy link
Member

Choose a reason for hiding this comment

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

👍

src/server.ts Outdated
delete connectionSubscriptions[subId];
}
break;

default:
this.sendSubscriptionFail(connection, subId, {
errors: [{
message: 'Invalid message type. Message type must be `subscription_start` or `subscription_end`.'
}]
message: 'Invalid message type!.',
Copy link
Member

Choose a reason for hiding this comment

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

! or . not both

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Copy link
Member

Choose a reason for hiding this comment

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

👍

src/server.ts Outdated
private sendInitResult(connection: WebSocket, result: any): void {
connection.send(JSON.stringify(result), () => {
if (result.type === INIT_FAIL) {
connection.close();
Copy link
Member

Choose a reason for hiding this comment

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

Why call both? Deserves at least a comment on why it's not appropriate to call just one of the similar methods

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a comment about this.
close closes the socket and sends the error code, while terminate sends the actual FIN packet and terminates the entire WS connection

Copy link
Member

Choose a reason for hiding this comment

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

Are you sure that this actually works? Does the close packet actually get sent out over the network or does calling terminate synchronously close the socket too fast (I think it does). You can test this with wireshark or tcpdump or something...

Copy link
Member

Choose a reason for hiding this comment

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

This needs to be verified too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Tested it and it looks fine, the client side gets the close message with the code, and the the socket terminates. Also, this is how ws uses it in it's unit tests.

@helfer
Copy link
Contributor

helfer commented Jan 10, 2017

@Urigo I merged some PRs on the other branch, but then I saw that you guys are still working here, so I'll let you publish the new version. Because of the new typings a minor version bump will be required.

@stubailo
Copy link
Contributor

@mlp5ab this is the pr for auth!

@Urigo Urigo merged commit e6a12d8 into apollographql:master Feb 5, 2017
@Urigo Urigo deleted the feature/ws branch February 5, 2017 12:03
baconz pushed a commit to PhiloInc/subscriptions-transport-ws that referenced this pull request Apr 24, 2018
* typings.json: shut up typings install

* package.json: bump all dependencies

Signed-off-by: Pierre Carrier <pierre@meteor.com>
@wawhal
Copy link

wawhal commented Jul 16, 2018

@Urigo Any documentation related to the changes by this PR?
To be more specific, I am looking to set custom HTTP headers while using subscription-transport-ws. Just adding it to the connection params doesn't seem to be working.

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.

5 participants