Compare commits

...

229 Commits

Author SHA1 Message Date
James Coglan 0b7eedc3b5 Bump version to 0.5.2. 2015-02-19 09:42:57 +00:00
James Coglan 201e3dd7c5 Test on Node 0.12 and io.js. 2015-02-19 09:26:06 +00:00
James Coglan 8cf42d6050 Apply @zingaburga's StreamReader optimisations from https://github.com/faye/websocket-driver-node/issues/10. 2015-02-16 20:07:40 +00:00
James Coglan ebe4ce1382 Because Driver.frame() will now admit input when in readyState 2 (to allow for async extension shutdown), the Messages stream should use the text() and binary() methods that will block messages in that state. 2015-02-16 09:51:40 +00:00
James Coglan daa37974c6 Don't emit further error/close events if the driver is already closing. 2015-02-16 09:08:10 +00:00
James Coglan cc5e5c245e Use Extensions.close(callback) to make sure close follows all pending messages.
viz.: https://github.com/faye/websocket-extensions-node/commit/1b77290e8279f654089eac963942309fb19b9312
2015-02-15 13:18:21 +00:00
James Coglan bdc081ab57 Adapt to the changes to the HTTPParser API introduced in iojs. 2015-02-02 21:06:51 +00:00
James Coglan 57e74231cf Bump version to 0.5.1. 2014-12-18 02:19:26 +00:00
James Coglan 5cba268409 Throw an error if drivers are created with unrecognised options. 2014-12-17 22:18:06 +00:00
James Coglan e8992add23 Bump version to 0.5.0 and document how to add extensions. 2014-12-13 13:00:47 +00:00
James Coglan 23675e08ee Don't expose the internal frame structure of messages to extensions; they should not be able to depend on seeing the actual frame boundaries off the wire because that requires all extensions to leave them intact. 2014-12-06 22:08:37 +00:00
James Coglan 66f46330e7 Remove some blank lines. 2014-12-05 19:36:06 +00:00
James Coglan 7376f48d6a Catch errors raised by Extensions.activate(). 2014-12-02 22:53:16 +00:00
James Coglan 330fa2073d Don't assume that messages coming out of extensions have anything but the standard websocket-extensions API. 2014-11-29 01:54:20 +00:00
James Coglan 58474a837c Populate the Message.data field before handing off to extensions. 2014-11-29 01:13:18 +00:00
James Coglan 82182eb348 Close the Extensions object when ending the session. 2014-11-29 00:55:02 +00:00
James Coglan 94041e7a86 websocket-extensions uses Node-style callbacks rather than emitting error events. 2014-11-27 09:32:49 +00:00
James Coglan e73f7db754 Rename validFrame() to validFrameRsv(). 2014-11-26 09:59:08 +00:00
James Coglan 0efc0ebb8d Don't pass the driver into the Extensions constructor. 2014-11-25 23:17:39 +00:00
James Coglan f904131d21 Extract the Extensions class into another module. 2014-11-25 21:14:17 +00:00
James Coglan ee664ba6c9 Decouple Extensions and individual extension libs from the Driver.fail() method, emitting errors instead. 2014-11-25 19:42:54 +00:00
James Coglan 538239b761 Move the header-parsing functions into class methods on Extensions. 2014-11-25 09:11:58 +00:00
James Coglan 2bc103c781 Adjust the data structures in the Extensions parser so that the server sets the order of extensions, per the spec. 2014-11-24 23:03:50 +00:00
James Coglan eceb59e2fb Implement the client-side framework for extensions. 2014-11-24 22:05:31 +00:00
James Coglan 40341c5439 The Client driver requires the crypto module for generating keys. 2014-11-24 00:41:34 +00:00
James Coglan 396637d463 Extract permessage-deflate into a separate module. 2014-11-23 21:46:04 +00:00
James Coglan c986ad231c Enable strict mode on all source files. 2014-11-23 20:09:26 +00:00
James Coglan ef897eed1c Implement deflate compression on server-to-client messages. 2014-11-23 19:45:02 +00:00
James Coglan 724a5f19a4 Flesh out the parameter validation for extensions, and provide better support for multiple values of extension params and HTTP headers. 2014-11-23 15:21:11 +00:00
James Coglan 0332ce2625 Initial support for permessage-deflate, only supports client-to-server compression but enough to pass the Autobahn suite. 2014-11-23 13:11:24 +00:00
James Coglan 6146615801 Extract Hybi parser state into Frame and Message structs.
This makes the structure of all the state in the parser easier to see,
it makes it easier to free memory by deleting a single struct rather
than many related fields, and it produces some data structures that will
be useful for adding extensions.
2014-11-22 13:34:34 +00:00
James Coglan 22d483a5ee Update the 0.4.0 release date. 2014-11-08 19:43:57 +00:00
James Coglan 7325241d11 Move the header setting for Sec-WebSocket-Protocol. 2014-11-08 17:46:03 +00:00
James Coglan eb2d329a16 Clean up some formatting typos. 2014-11-08 17:20:51 +00:00
James Coglan 7e34e7fd40 Correct the name of the test about proxy 'connect' events. 2014-11-08 16:43:36 +00:00
James Coglan 623a93c0b7 Duplicate headers should be allowed if ALLOWED_DUPLICATES *includes* the name. 2014-11-08 16:39:40 +00:00
James Coglan 208be33754 Refactor the way headers are generated in all the drivers. 2014-11-08 16:12:14 +00:00
James Coglan 0bb47c17b4 Remove TLS concerns from the proxy implementation.
This results in a more simple design, in the sense of fewer concerns
being mixed together. It makes the API more verbose, but it's not
difficult to use correctly.

It also means that we make the I/O implementation deal with TLS, so it
can use tls.connect({socket: io}) or the upcoming TLSSocket class,
rather than this module needing a huge amount of boilerplate and
internal indirection to use tls.createSecurePair() and make Proxy wrap
pair.encrypted.

Using tls.connect({socket: io}) would be awkward on this end, because it
expects a stream that emits *incoming* data from the TCP connection. All
the streams in this module emit *outgoing* data that should be written
to TCP, so their polarity is flipped compared to what tls.connect()
wants, and resolving that involves even more stream boilerplate.
2014-11-08 15:20:08 +00:00
James Coglan d6da566641 Only call tls.checkServerIdentity() if it is available (i.e. before Node 0.8.). 2014-11-07 19:17:12 +00:00
James Coglan cde48d625c Move http_parser.js up into the lib/websocket directory. 2014-11-07 19:16:35 +00:00
James Coglan 6eb04d5acc Remove a redundant assignment. 2014-11-06 23:20:32 +00:00
James Coglan 4348bec390 Some cleanup to remove bits I left lying around while refactoring proxy support. 2014-11-06 23:04:11 +00:00
James Coglan 550b3d906d Unify the proxy handshake, TLS handshake, and WebSocket protocol into a single stream when using driver.proxy() to simplify the API. 2014-11-05 23:30:15 +00:00
James Coglan 473fcaafdb Merge pull request #9 from meteor/close-proxy
proxy should emit 'close' when it's done
2014-10-30 00:28:44 +00:00
David Glasser b88b8d6856 proxy should emit 'close' when it's done
This will cause any stream piped into it to stop piping into it. (It's
the only way to unpipe a Node 0.8 Streams1-style pipe.)

This change allows you to remove the empty "stream.on('data')" in
faye-websocket.
2014-10-29 16:47:56 -07:00
James Coglan b905c94f89 Make a TCP connection to the proxy in the proxy example. 2014-10-29 23:07:03 +00:00
James Coglan 7b47563cd1 Mark code up as JavaScript. 2014-10-29 22:38:29 +00:00
James Coglan 07031911e3 s/wail/will/ 2014-10-29 22:31:33 +00:00
James Coglan d2156ac10d Clarify that the 'tls' option is for use with wss: connections. 2014-10-29 22:19:40 +00:00
James Coglan d0033b8208 Change TLS values in the proxy docs. 2014-10-29 22:17:03 +00:00
James Coglan 76f4128634 Redesign the proxy API so that we can more cleanly deal with TLS, either to the proxy server or origin server or both, while still allowing control of the TLS options and custom headers, and not leaking so much of the implementation. 2014-10-29 22:07:01 +00:00
James Coglan 32e6715b6e [wip] Emit a proxy-connect event so the client can perform a TLS handshake at the right time. 2014-10-29 19:27:30 +00:00
James Coglan 042e33aebf Bump version to 0.4.0. 2014-10-27 09:35:13 +00:00
James Coglan bdc9bfe7e8 Add support for connecting via HTTP proxies. 2014-10-27 09:30:11 +00:00
James Coglan 1c33c0ce56 Bump version to 0.3.6. 2014-10-04 08:30:43 +01:00
James Coglan f693b02743 Merge pull request #8 from meteor/close-before-start
driver.close() before driver.start() should close
2014-09-07 21:29:25 +01:00
James Coglan 91f19cb69b Use the SVG build status icon from Travis. 2014-09-07 21:06:45 +01:00
David Glasser dcd7c278b6 driver.close() before driver.start() should close 2014-08-15 10:48:30 -07:00
James Coglan dbedb370b7 Bump version to 0.3.5. 2014-07-06 10:18:19 +01:00
James Coglan 28858d9fb3 Delegate the protocol and version methods correctly in the TCP driver. 2014-07-04 17:49:11 +01:00
James Coglan ae52cf995e Stop the Hybi parser hanging onto data it doesn't need after emitFrame(). 2014-06-16 20:30:44 +01:00
James Coglan dfe7b2b000 Trailing blank lines are bad. 2014-05-17 00:35:31 +01:00
James Coglan d40de878b2 Bump version to 0.3.4. 2014-05-08 02:19:58 +01:00
James Coglan ed3907d5fd Clean up the state management code in StreamReader. 2014-05-07 20:49:49 +01:00
James Coglan 84b6f50f1a Merge pull request #6 from meteor/forget-finished-buffers
StreamReader: Don't hang on to fully read buffers
2014-05-07 20:41:15 +01:00
David Glasser 8d19ee823e StreamReader: Don't hang on to fully read buffers 2014-05-06 11:52:59 -07:00
James Coglan 3bee0366c5 Bump version to 0.3.3. 2014-04-24 23:31:43 +01:00
James Coglan 9f2782da14 Correct the draft-76 status line reason phrase. 2014-04-17 02:23:58 +01:00
James Coglan c752c76712 Bump version to 0.3.2. 2013-12-29 12:21:49 +00:00
James Coglan ad8f08f19e Make Node 0.6 work on Travis. 2013-12-28 17:40:18 +00:00
James Coglan d343c7d21b Document the new lower frame size limit. 2013-12-28 16:56:15 +00:00
James Coglan 0b089ad921 Merge pull request #5 from Zarel/patch-1
Lower default max buffer length
2013-12-27 10:17:09 -08:00
James Coglan e65e837968 Stop parsing Hybi when we go into a failure state. 2013-12-27 18:16:41 +00:00
Guangcong Luo cda22e3bef Lower default max buffer length
Real-world testing shows that the previous max buffer length
of ~1GB was too high, and messages of the size of roughly 300MB
could still cause the process to crash from running out of
memory. 64MB seems like a reasonably conservative maximum.
2013-12-24 19:26:38 -06:00
James Coglan 1ae37d6efe Remove an unused instance variable _lengthBuffer. 2013-12-20 10:20:41 +00:00
James Coglan 82c42a6ce5 Extend max-length checking to draft-{75,76}. 2013-12-20 10:09:31 +00:00
James Coglan aad1519f3f Extend max-length checking to short (<= 125 bytes) frames and to sequences of continuation frames. 2013-12-20 09:50:18 +00:00
James Coglan 11a9b75185 Bump version to 0.3.1. 2013-12-03 00:46:42 +00:00
James Coglan d93c853414 Don't pre-allocate a huge Buffer before we know there's enough chunks in the read queue to complete a message. 2013-12-02 21:07:33 +00:00
James Coglan 2dff35d3e4 Map Node v0.11's magic numbers for HTTP methods. 2013-12-02 13:02:38 +00:00
James Coglan 6588928445 Make HttpParser work on v0.11.6+. 2013-11-29 00:42:49 +00:00
James Coglan 151fddd206 Make the maximum frame length into an option that the user can set. 2013-11-28 23:38:42 +00:00
James Coglan 0b1f16a7ee Merge pull request #2 from meteor/limit-frame-to-max-buffer
Reduce frame max length to max Node Buffer length.
2013-11-28 15:25:05 -08:00
David Glasser f15b331a34 Reduce frame max length to max Node Buffer length. 2013-10-11 19:21:14 -07:00
James Coglan 95261a1779 Bump version to 0.3.0. 2013-09-09 20:15:10 +01:00
James Coglan 12f9f4d444 Add support for Basic Auth URLs to the client driver. 2013-08-13 17:16:49 +01:00
James Coglan d3e81b478e Bump version to 0.2.2. 2013-07-05 15:16:37 +01:00
James Coglan c5b3df986b Migrate to jstest. 2013-07-01 02:10:29 +01:00
James Coglan 286dea4337 Write the reflexive pipe examples as one-liners. [http://nodejsreactions.tumblr.com/post/51566814133/src-pipe-dst-pipe-src] 2013-05-29 02:24:27 +01:00
James Coglan 2378f4c484 An example in the README was missing a closing paren. 2013-05-17 12:46:35 +01:00
James Coglan 5a1dcd5773 Bump version to 0.2.1. 2013-05-17 11:51:34 +01:00
James Coglan 17e2bd4084 Messages should be queued when the driver is in the 'pre-connecting' state, as the client is initially. 2013-05-17 11:48:36 +01:00
James Coglan 3dfaaf497f Expose the isSecureRequest() method. 2013-05-17 11:47:56 +01:00
James Coglan 0e4f13d0e4 Bump version to 0.2.0. 2013-05-12 15:09:18 +01:00
James Coglan 4f8e10ead6 Add a changelog. 2013-05-12 15:08:17 +01:00
James Coglan 97451f81ab Set status and headers as soon as we have them so they're available if there is an error. 2013-05-12 14:44:17 +01:00
James Coglan 390ebee4d8 Parameterize the port we run the example server on. 2013-05-12 13:19:13 +01:00
James Coglan de001a0f54 Remove a blank line. 2013-05-12 01:45:56 +01:00
James Coglan 26c4873f3e Apply offset to the consumed offset from HTTP parsing before we check whether the whole buffer has been used. 2013-05-12 01:45:04 +01:00
James Coglan b2472e89a6 Document the Server driver. 2013-05-12 01:31:14 +01:00
James Coglan 1124df2bf5 Add a Server driver for running WebSockets through a bare TCP server. 2013-05-12 01:13:50 +01:00
James Coglan ea4e289669 Add a setHeader() function to allow peers to add headers to their handshake responses. 2013-05-11 22:26:20 +01:00
James Coglan d7c65eedd9 Expose the status and headers from the response in the client driver. 2013-05-11 21:58:37 +01:00
James Coglan d594f7d708 Include more possible headers as indicators of a secure connection. 2013-05-11 21:55:52 +01:00
James Coglan 746977d2e4 Don't package the spec directory. 2013-05-04 20:40:53 +01:00
James Coglan aa25393a6c Check for GET in isWebSocket() and expose the isSecureRequest() method. 2013-05-04 17:27:25 +01:00
James Coglan 88202e66d5 Stream.end() should write() if given an empty stream. 2013-05-04 17:26:59 +01:00
James Coglan 25c075a408 Rename the module to websocket-driver. 2013-05-04 16:42:44 +01:00
James Coglan a582cb8224 Justify some lines in readme properly. 2013-05-04 15:05:16 +01:00
James Coglan 37da202fd2 Readme should tell you how to install the library. 2013-05-04 14:59:38 +01:00
James Coglan 4965dc1214 Some small changes to the readme. 2013-05-04 14:49:51 +01:00
James Coglan f16ad50db0 Raise an error if the length header is larger than the max int that JS can represent precisely. 2013-05-04 13:33:27 +01:00
James Coglan d716650499 Add more robust error handling for Hybi and Client and emit error events when the protocol goes wrong. 2013-05-02 00:09:14 +01:00
James Coglan 103c39c664 Correct the name of a test suite. 2013-05-01 02:11:32 +01:00
James Coglan cbb33e4924 Allow default handler options to be overridden. 2013-05-01 00:59:54 +01:00
James Coglan 23f296b926 Replace the getVersion() method with a version property. 2013-05-01 00:55:49 +01:00
James Coglan b56a960bb2 s/addListener/on/ 2013-05-01 00:24:33 +01:00
James Coglan 86a6d59617 Write request body before piping socket in the docs. 2013-04-30 23:47:43 +01:00
James Coglan f6481422b9 Firm up the event system. Make handlers only emit their own events, and then map those to Messages and IO events with a bit of glue. Only emit 'data' when streams are readable, same for 'end'. 2013-04-30 23:35:51 +01:00
James Coglan 9975aa089b Use the Node EventEmitter class to publish events. 2013-04-30 08:20:14 +01:00
James Coglan 46f58dc5b0 s/Node/Note/ 2013-04-29 22:03:58 +01:00
James Coglan d2e958096c Refer to the draft-76 request body as 'body' rather than 'head'. 2013-04-29 22:00:39 +01:00
James Coglan e9bdd17cef Correct a typo in stream docs. 2013-04-29 21:54:52 +01:00
James Coglan e98eb72c6c Remove comments about Node streams. 2013-04-29 21:51:00 +01:00
James Coglan 5dfe9ecdad Return the protocol version from the Client handler. 2013-04-29 21:35:57 +01:00
James Coglan c957874493 Make protocol.http() select the right driver for the request. 2013-04-29 20:46:17 +01:00
James Coglan b354dd69fa Implement the draft-76 protocol. 2013-04-29 20:27:35 +01:00
James Coglan c70c5618da Only end the messages stream when we trigger onclose. 2013-04-29 19:11:26 +01:00
James Coglan 6c025087f7 Emit 'end' from the messages stream when the socket's onclose event runs. 2013-04-29 19:08:10 +01:00
James Coglan 9ed82bc0ab Extract the IO and Messages streams and flesh them out to implement backpressure. 2013-04-29 19:00:08 +01:00
James Coglan 94a8d0b7be Correct typo in server-side example. 2013-04-29 09:18:21 +01:00
James Coglan e3b8033c58 Placeholder for draft-76 support just to get the common tests passing. 2013-04-29 08:56:05 +01:00
James Coglan 950cd9f1e2 Implement and test draft-75. 2013-04-29 08:40:05 +01:00
James Coglan 6f7942d5b7 Begin writing the draft75 module. 2013-04-29 07:48:09 +01:00
James Coglan c0e1e8d2c3 Add a title to call out the streams interface. 2013-04-29 07:40:30 +01:00
James Coglan 0ff6767355 GitHub's markdown processor handles bolded code inconsistently. 2013-04-29 07:39:53 +01:00
James Coglan 83865ac1da Some corrections to the docs. 2013-04-29 07:33:01 +01:00
James Coglan 52216a5c27 Merge a couple of paragraphs. 2013-04-29 00:04:20 +01:00
James Coglan fc4c166fef Remove an item from .npmignore. 2013-04-29 00:02:46 +01:00
James Coglan ab416619b7 Add a README. 2013-04-29 00:01:40 +01:00
James Coglan 41ddd82932 Add tests for Hybi and Client handlers and fix some minor bugs. 2013-04-28 23:22:05 +01:00
James Coglan 847bdc1773 Strip the repo down to just the protocol handlers. 2013-04-28 19:04:03 +01:00
James Coglan 13e27bdc3b Update Node versions for Travis. 2013-04-28 18:56:32 +01:00
James Coglan 62a0c7597f First steps toward streaming I/O-independent parsing. Implement the Hybi and Client protocol handlers as stateful protcol drivers with two duplex streams, one for messages and one for I/O. 2013-04-28 18:55:58 +01:00
James Coglan 78f8bfb26b Remove parser specs. 2013-04-28 14:01:28 +01:00
James Coglan 99aacf67a5 Bump copyright date. 2013-02-15 00:02:57 +00:00
James Coglan 1a6cdd3e43 Bump version to 0.4.4. 2013-02-14 22:40:48 +00:00
James Coglan 0e2417cd51 Change name used in Autobahn tests. 2013-02-14 22:39:13 +00:00
James Coglan 86f882f189 Return early rather than have an else-clause. 2013-02-10 23:41:37 +00:00
James Coglan 87f1abad08 s/close/finalize/ -- I missed one rename in the last commit. 2012-12-24 16:43:41 +00:00
James Coglan b86f505441 If WebSocket.close() is called without asking for an ack, then don't return if we're in the CLOSING state. This can happen if one peer sends a closing frame and the other peer terminates TCP before sending the reply, and as currently implemented this will no emit a close event or end the Node stream. 2012-12-24 16:37:57 +00:00
James Coglan 14a1a7372d Use progress bar using Autobahn client tests. 2012-12-23 18:25:50 +00:00
James Coglan ee22f75959 Remove trailing whitespace. 2012-12-22 23:13:04 +00:00
James Coglan 7b63baa5a6 Fix specs on v0.9. 2012-12-22 23:12:08 +00:00
James Coglan 0dc749e565 Merge pull request #18 from gsoltis/master
Fix for closing a socket in the CONNECTING state
2012-10-15 11:54:53 -07:00
Greg Soltis 3280f38c27 Fix check of readyState for immediate close 2012-10-15 11:34:44 -07:00
James Coglan 64b52a0be2 Update Node versions for Travis. 2012-10-13 17:02:07 +01:00
James Coglan bd6e89f290 If close() is called in the CONNECTING readyState, then close the TCP connection immediately without waiting for the handshake to complete. 2012-10-13 16:59:58 +01:00
James Coglan f50de64532 Update changelog. 2012-07-09 08:58:00 +01:00
James Coglan d071ec3acd Check that incoming requests have an output stream before doing anything with it. 2012-07-08 20:30:53 +01:00
James Coglan 59fd729a03 Bump version to 0.4.3. 2012-07-01 18:15:01 +01:00
James Coglan b458959e6a Update Node versions for Travis. 2012-07-01 18:12:54 +01:00
James Coglan 7f18af7c90 Use Connection:close on EventSource connections. 2012-05-21 19:41:26 +01:00
James Coglan 680d8fc759 Remove an expensive test. 2012-05-21 19:37:40 +01:00
James Coglan 90d039e371 Bump version to 0.4.2. 2012-04-06 18:57:29 +01:00
James Coglan b42cdf741d Add error code 1011. 2012-04-06 13:52:21 +01:00
James Coglan bd7c52dfa2 Use / as the default path if the client URI has no path. 2012-04-01 22:57:16 +01:00
James Coglan adee8ba51c Bump version to 0.4.1. 2012-02-26 19:00:04 +00:00
James Coglan 3675cfb798 Treat anything but a Buffer as a text message in WebSocket#send. 2012-02-25 09:49:53 +00:00
James Coglan 1222308201 Return boolean from EventSource.send(). 2012-02-16 23:15:16 +00:00
James Coglan 8c9ae84740 Treat numbers as strings when passed to Socket.send(). 2012-02-16 23:09:29 +00:00
James Coglan d404c78c46 Add items to .npmignore. 2012-02-13 12:43:12 +00:00
James Coglan a6e2fe2aaa Bump version to 0.4.0. 2012-02-13 09:15:57 +00:00
James Coglan 8f71ec1f8b Set EventSource readyState during constructor and attach pings and event listeners after sending handshake. 2012-02-13 00:26:43 +00:00
James Coglan 74b76e56b7 Only send ping messages for EventSource if the user specifies an interval, for symmetry with WebSocket. 2012-02-12 23:59:20 +00:00
James Coglan c958c5ead9 Don't send a message on connect from the server. 2012-02-12 23:54:03 +00:00
James Coglan faa1f11c28 Clean up ping code. 2012-02-11 14:06:43 +00:00
James Coglan a9659df7d8 Set readyState to OPEN as soon as possible. 2012-02-11 13:36:12 +00:00
James Coglan fe4314e62b Revert accidental change to ws.html. 2012-02-11 12:32:51 +00:00
James Coglan 8523605f88 Document return value of WebSocket.ping(). 2012-02-11 12:10:39 +00:00
James Coglan 25794989f0 Push ping logic down into HybiParser. 2012-02-11 12:07:40 +00:00
James Coglan 29cae88d2c Provide a default value (empty string) for WebSocket ping messages. 2012-02-11 12:02:04 +00:00
James Coglan e6c03b7629 Add a ping() interface to server-side WebSocket and EventSource connections. 2012-02-11 11:59:13 +00:00
James Coglan d18a818699 Do not buffer send() calls during the CONNECTING stage of WebSocket clients -- browsers throw an error in this case. 2012-02-11 11:30:28 +00:00
James Coglan a48333ee1b Node 0.5 is not available on Travis. 2012-02-11 11:23:37 +00:00
James Coglan 6de9e01e69 Buffer calls to ws.send() until the handshake is completed. 2012-02-11 11:22:28 +00:00
James Coglan 51d5978283 Pass options object to tls.connect or it hangs on Node 0.7. 2012-02-08 22:44:04 +00:00
James Coglan a2b2559bd6 Add npm test script to package.json. 2012-02-08 22:26:00 +00:00
James Coglan c3bf5e9fa8 Fix client connection error detection on Node 0.7. 2012-02-08 22:25:44 +00:00
James Coglan ce5c6015b3 Add Travis CI configuration. 2012-02-08 22:09:09 +00:00
James Coglan 8d860a83d9 Tiny formatting change. 2012-02-07 23:42:19 +00:00
James Coglan a8ab367a0f Provide usable and accurate (HAProxy-safe) onopen event on the server side. 2012-02-07 23:39:42 +00:00
James Coglan f0678e6c91 Bump version to 0.3.1. 2012-01-16 20:19:26 +00:00
James Coglan ce0f81c8d5 Call setNoDelay(true) on sockets. 2012-01-16 20:18:23 +00:00
James Coglan e2090ce019 Bump version to 0.3.0. 2012-01-13 21:41:29 +00:00
James Coglan 22a3507dad Correct EventSource error handler. 2012-01-13 21:24:54 +00:00
James Coglan 3ba69e4242 Correct variable name in WebSocket example. 2012-01-12 22:32:52 +00:00
James Coglan cc3581fd0c Reorder handlers in example server. 2012-01-12 20:18:00 +00:00
James Coglan 3488318ae6 Document the EventSource API. 2012-01-12 20:12:35 +00:00
James Coglan 91eb2642ee Store output socket as an instance variable in EventSource. 2012-01-12 19:53:06 +00:00
James Coglan 20dd83257a Implement EventSource connection handler. 2012-01-12 19:48:59 +00:00
James Coglan c82b59259f Remove some text about client versions from the README. 2012-01-09 01:52:58 +00:00
James Coglan 727aba743c Correct readme on default value of WebSocket#protocol. 2011-12-21 19:45:28 +00:00
James Coglan 0aac24bece Update repository location. 2011-12-21 17:05:33 +00:00
James Coglan 4c3eaf8141 Bump version to 0.2.0, add changelog. 2011-12-21 01:28:20 +00:00
James Coglan 0b53267717 protocol property should be '' if not set. 2011-12-21 01:19:39 +00:00
James Coglan bdb0de2cfa Draft-75/76 frames with length headers should be ignored. 2011-12-20 19:44:07 +00:00
James Coglan 6704d33859 Implement the full framing interpreter for draft-75/76, including closing frames for 76. 2011-12-19 22:59:08 +00:00
James Coglan 4240b6bc27 Test that parsers are not sensitive to how data is split across packets. 2011-12-19 21:36:49 +00:00
James Coglan afdf6c223c Change version strings. 2011-12-19 21:25:53 +00:00
James Coglan 2782990fe3 Rename Protocol8Parser to HybiParser. 2011-12-19 21:25:19 +00:00
James Coglan 421f4660e5 Don't use map() to generate masks. 2011-12-19 10:12:26 +00:00
James Coglan 9c1e260784 When client handshake fails, close with the right code and reason. 2011-12-19 00:14:06 +00:00
James Coglan 1ba9b8586a Refactor subprotocol documentation. 2011-12-18 16:16:55 +00:00
James Coglan 05bc759af6 Make protocol vaidation a little more strict. 2011-12-18 16:09:36 +00:00
James Coglan e1aac5db02 Switch client to Sec-WebSocket-Version: 13. 2011-12-18 16:05:10 +00:00
James Coglan c751d96b8b Document subprotocol negotiation. 2011-12-18 16:04:23 +00:00
James Coglan fa2aff1387 Implement client-side subprotocol validation, and expose the protocol property. 2011-12-18 15:57:24 +00:00
James Coglan ea635db3f4 Merge branch 'master' into subprotocols 2011-12-18 15:37:05 +00:00
James Coglan b129b447a2 Refactor masking code. 2011-12-18 11:52:47 +00:00
James Coglan 390882f720 Change package description. 2011-12-18 09:20:56 +00:00
James Coglan 5a0c941075 Explicitly return null where parser methods hit an error. 2011-12-17 19:23:56 +00:00
James Coglan f5a78efcfd Extract 255 into a constant. 2011-12-17 19:22:46 +00:00
James Coglan f0f0bfc951 Inline draft-75 parsing function. 2011-12-17 17:23:17 +00:00
James Coglan 48dfb9578c Merge branch 'master' into subprotocols
Conflicts:
	lib/faye/websocket.js
	lib/faye/websocket/protocol8_parser.js
2011-12-17 15:37:54 +00:00
James Coglan 15f1b4df99 Remove benchmark page. 2011-12-17 15:20:30 +00:00
James Coglan 2dbac35b15 Only send Sec-WebSocket-Protocol response if there is a matching protocol available. 2011-12-15 20:05:03 +00:00
James Coglan 430b8ab7df Formatting tweaks. 2011-12-15 19:59:28 +00:00
James Coglan 283b13b617 Initial support for user-defined subprotocol negotiation on the server side. 2011-12-15 00:33:23 +00:00
46 changed files with 3428 additions and 1583 deletions
+3 -1
View File
@@ -1,4 +1,6 @@
.git
.gitignore
.redcar
.npmignore
.travis.yml
node_modules
spec
+11
View File
@@ -0,0 +1,11 @@
language: node_js
node_js:
- "0.6"
- "0.8"
- "0.10"
- "0.12"
- "iojs"
before_install:
- '[ "${TRAVIS_NODE_VERSION}" = "0.6" ] && npm conf set strict-ssl false || true'
+68
View File
@@ -0,0 +1,68 @@
### 0.5.2 / 2015-02-19
* Fix compatibility with the HTTP parser on io.js
* Use `websocket-extensions` to make sure messages and close frames are kept in order
* Don't emit multiple `error` events
### 0.5.1 / 2014-12-18
* Don't allow drivers to be created with unrecognized options
### 0.5.0 / 2014-12-13
* Support protocol extensions via the websocket-extensions module
### 0.4.0 / 2014-11-08
* Support connection via HTTP proxies using `CONNECT`
### 0.3.6 / 2014-10-04
* It is now possible to call `close()` before `start()` and close the driver
### 0.3.5 / 2014-07-06
* Don't hold references to frame buffers after a message has been emitted
* Make sure that `protocol` and `version` are exposed properly by the TCP driver
### 0.3.4 / 2014-05-08
* Don't hold memory-leaking references to I/O buffers after they have been parsed
### 0.3.3 / 2014-04-24
* Correct the draft-76 status line reason phrase
### 0.3.2 / 2013-12-29
* Expand `maxLength` to cover sequences of continuation frames and `draft-{75,76}`
* Decrease default maximum frame buffer size to 64MB
* Stop parsing when the protocol enters a failure mode, to save CPU cycles
### 0.3.1 / 2013-12-03
* Add a `maxLength` option to limit allowed frame size
* Don't pre-allocate a message buffer until the whole frame has arrived
* Fix compatibility with Node v0.11 `HTTPParser`
### 0.3.0 / 2013-09-09
* Support client URLs with Basic Auth credentials
### 0.2.2 / 2013-07-05
* No functional changes, just updates to package.json
### 0.2.1 / 2013-05-17
* Export the isSecureRequest() method since faye-websocket relies on it
* Queue sent messages in the client's initial state
### 0.2.0 / 2013-05-12
* Add API for setting and reading headers
* Add Driver.server() method for getting a driver for TCP servers
### 0.1.0 / 2013-05-04
* First stable release
-119
View File
@@ -1,119 +0,0 @@
# faye-websocket
This is a robust, general-purpose WebSocket implementation extracted from the
[Faye](http://faye.jcoglan.com) project. It provides classes for easily building
WebSocket servers and clients in Node. It does not provide a server itself, but
rather makes it easy to handle WebSocket connections within an existing
[Node](http://nodejs.org/) application. It does not provide any abstraction
other than the standard [WebSocket API](http://dev.w3.org/html5/websockets/).
The server-side socket can process [draft-75](http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75),
[draft-76](http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76),
[hybi-07](http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07)
and later versions of the protocol. It selects protocol versions automatically,
supports both `text` and `binary` messages, and transparently handles `ping`,
`pong`, `close` and fragmented messages.
## Accepting WebSocket connections in Node
You can handle WebSockets on the server side by listening for HTTP Upgrade
requests, and creating a new socket for the request. This socket object exposes
the usual WebSocket methods for receiving and sending messages. For example this
is how you'd implement an echo server:
```js
var WebSocket = require('faye-websocket'),
http = require('http');
var server = http.createServer();
server.addListener('upgrade', function(request, socket, head) {
var ws = new WebSocket(request, socket, head);
ws.onmessage = function(event) {
ws.send(event.data);
};
ws.onclose = function(event) {
console.log('close', event.code, event.reason);
ws = null;
};
});
server.listen(8000);
```
## Using the WebSocket client
The client supports both the plain-text `ws` protocol and the encrypted `wss`
protocol, and has exactly the same interface as a socket you would use in a web
browser. On the wire it identifies itself as hybi-08, though it's compatible
with servers speaking later versions of the protocol, at least up to version 17.
```js
var WebSocket = require('faye-websocket'),
ws = new WebSocket.Client('ws://www.example.com/');
ws.onopen = function(event) {
console.log('open');
ws.send('Hello, world!');
};
ws.onmessage = function(event) {
console.log('message', event.data);
};
ws.onclose = function(event) {
console.log('close', event.code, event.reason);
ws = null;
};
```
## WebSocket API
The WebSocket API consists of several event handlers and a method for sending
messages.
* <b><tt>onopen</tt></b> fires when the socket connection is established. Event
has no attributes.
* <b><tt>onerror</tt></b> fires when the connection attempt fails. Event has no
attributes.
* <b><tt>onmessage</tt></b> fires when the socket receives a message. Event has
one attribute, <b><tt>data</tt></b>, which is either a `String` (for text
frames) or a `Buffer` (for binary frames).
* <b><tt>onclose</tt></b> fires when either the client or the server closes the
connection. Event has two optional attributes, <b><tt>code</tt></b> and
<b><tt>reason</tt></b>, that expose the status code and message sent by the
peer that closed the connection.
* <b><tt>send(message)</tt></b> accepts either a `String` or a `Buffer` and
sends a text or binary message over the connection to the other peer.
* <b><tt>close(code, reason)</tt></b> closes the connection, sending the given
status code and reason text, both of which are optional.
## License
(The MIT License)
Copyright (c) 2009-2011 James Coglan
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the 'Software'), to deal in
the Software without restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+374
View File
@@ -0,0 +1,374 @@
# websocket-driver [![Build Status](https://travis-ci.org/faye/websocket-driver-node.svg)](https://travis-ci.org/faye/websocket-driver-node)
This module provides a complete implementation of the WebSocket protocols that
can be hooked up to any I/O stream. It aims to simplify things by decoupling the
protocol details from the I/O layer, such that users only need to implement code
to stream data in and out of it without needing to know anything about how the
protocol actually works. Think of it as a complete WebSocket system with
pluggable I/O.
Due to this design, you get a lot of things for free. In particular, if you hook
this module up to some I/O object, it will do all of this for you:
* Select the correct server-side driver to talk to the client
* Generate and send both server- and client-side handshakes
* Recognize when the handshake phase completes and the WS protocol begins
* Negotiate subprotocol selection based on `Sec-WebSocket-Protocol`
* Negotiate and use extensions via the
[websocket-extensions](https://github.com/faye/websocket-extensions-node)
module
* Buffer sent messages until the handshake process is finished
* Deal with proxies that defer delivery of the draft-76 handshake body
* Notify you when the socket is open and closed and when messages arrive
* Recombine fragmented messages
* Dispatch text, binary, ping and close frames
* Manage the socket-closing handshake process
* Automatically reply to ping frames with a matching pong
* Apply masking to messages sent by the client
This library was originally extracted from the [Faye](http://faye.jcoglan.com)
project but now aims to provide simple WebSocket support for any Node-based
project.
## Installation
```
$ npm install websocket-driver
```
## Usage
This module provides protocol drivers that have the same interface on the server
and on the client. A WebSocket driver is an object with two duplex streams
attached; one for incoming/outgoing messages and one for managing the wire
protocol over an I/O stream. The full API is described below.
### Server-side with HTTP
A Node webserver emits a special event for 'upgrade' requests, and this is where
you should handle WebSockets. You first check whether the request is a
WebSocket, and if so you can create a driver and attach the request's I/O stream
to it.
```js
var http = require('http'),
websocket = require('websocket-driver');
var server = http.createServer();
server.on('upgrade', function(request, socket, body) {
if (!websocket.isWebSocket(request)) return;
var driver = websocket.http(request);
driver.io.write(body);
socket.pipe(driver.io).pipe(socket);
driver.messages.on('data', function(message) {
console.log('Got a message', message);
});
driver.start();
});
```
Note the line `driver.io.write(body)` - you must pass the `body` buffer to the
socket driver in order to make certain versions of the protocol work.
### Server-side with TCP
You can also handle WebSocket connections in a bare TCP server, if you're not
using an HTTP server and don't want to implement HTTP parsing yourself.
The driver will emit a `connect` event when a request is received, and at this
point you can detect whether it's a WebSocket and handle it as such. Here's an
example using the Node `net` module:
```js
var net = require('net'),
websocket = require('websocket-driver');
var server = net.createServer(function(connection) {
var driver = websocket.server();
driver.on('connect', function() {
if (websocket.isWebSocket(driver)) {
driver.start();
} else {
// handle other HTTP requests
}
});
driver.on('close', function() { connection.end() });
connection.on('error', function() {});
connection.pipe(driver.io).pipe(connection);
driver.messages.pipe(driver.messages);
});
server.listen(4180);
```
In the `connect` event, the driver gains several properties to describe the
request, similar to a Node request object, such as `method`, `url` and
`headers`. However you should remember it's not a real request object; you
cannot write data to it, it only tells you what request data we parsed from the
input.
If the request has a body, it will be in the `driver.body` buffer, but only as
much of the body as has been piped into the driver when the `connect` event
fires.
### Client-side
Similarly, to implement a WebSocket client you just need to make a driver by
passing in a URL. After this you use the driver API as described below to
process incoming data and send outgoing data.
```js
var net = require('net'),
websocket = require('websocket-driver');
var driver = websocket.client('ws://www.example.com/socket'),
tcp = net.connect(80, 'www.example.com');
tcp.pipe(driver.io).pipe(tcp);
tcp.on('connect', function() {
driver.start();
});
driver.messages.on('data', function(message) {
console.log('Got a message', message);
});
```
Client drivers have two additional properties for reading the HTTP data that was
sent back by the server:
* `driver.statusCode` - the integer value of the HTTP status code
* `driver.headers` - an object containing the response headers
### HTTP Proxies
The client driver supports connections via HTTP proxies using the `CONNECT`
method. Instead of sending the WebSocket handshake immediately, it will send a
`CONNECT` request, wait for a `200` response, and then proceed as normal.
To use this feature, call `driver.proxy(url)` where `url` is the origin of the
proxy, including a username and password if required. This produces a duplex
stream that you should pipe in and out of your TCP connection to the proxy
server. When the proxy emits `connect`, you can then pipe `driver.io` to your
TCP stream and call `driver.start()`.
```js
var net = require('net'),
websocket = require('websocket-driver');
var driver = websocket.client('ws://www.example.com/socket'),
proxy = driver.proxy('http://username:password@proxy.example.com'),
tcp = net.connect(80, 'proxy.example.com');
tcp.pipe(proxy).pipe(tcp, {end: false});
tcp.on('connect', function() {
proxy.start();
});
proxy.on('connect', function() {
driver.io.pipe(tcp).pipe(driver.io);
driver.start();
});
driver.messages.on('data', function(message) {
console.log('Got a message', message);
});
```
The proxy's `connect` event is also where you should perform a TLS handshake on
your TCP stream, if you are connecting to a `wss:` endpoint.
In the event that proxy connection fails, `proxy` will emit an `error`. You can
inspect the proxy's response via `proxy.statusCode` and `proxy.headers`.
```js
proxy.on('error', function(error) {
console.error(error.message);
console.log(proxy.statusCode);
console.log(proxy.headers);
});
```
Before calling `proxy.start()` you can set custom headers using
`proxy.setHeader()`:
```js
proxy.setHeader('User-Agent', 'node');
proxy.start();
```
### Driver API
Drivers are created using one of the following methods:
```js
driver = websocket.http(request, options)
driver = websocket.server(options)
driver = websocket.client(url, options)
```
The `http` method returns a driver chosen using the headers from a Node HTTP
request object. The `server` method returns a driver that will parse an HTTP
request and then decide which driver to use for it using the `http` method. The
`client` method always returns a driver for the RFC version of the protocol with
masking enabled on outgoing frames.
The `options` argument is optional, and is an object. It may contain the
following fields:
* `maxLength` - the maximum allowed size of incoming message frames, in bytes.
The default value is `2^26 - 1`, or 1 byte short of 64 MiB.
* `protocols` - an array of strings representing acceptable subprotocols for use
over the socket. The driver will negotiate one of these to use via the
`Sec-WebSocket-Protocol` header if supported by the other peer.
A driver has two duplex streams attached to it:
* <b>`driver.io`</b> - this stream should be attached to an I/O socket like a
TCP stream. Pipe incoming TCP chunks to this stream for them to be parsed, and
pipe this stream back into TCP to send outgoing frames.
* <b>`driver.messages`</b> - this stream emits messages received over the
WebSocket. Writing to it sends messages to the other peer by emitting frames
via the `driver.io` stream.
All drivers respond to the following API methods, but some of them are no-ops
depending on whether the client supports the behaviour.
Note that most of these methods are commands: if they produce data that should
be sent over the socket, they will give this to you by emitting `data` events on
the `driver.io` stream.
#### `driver.on('open', function(event) {})`
Sets the callback to execute when the socket becomes open.
#### `driver.on('message', function(event) {})`
Sets the callback to execute when a message is received. `event` will have a
`data` attribute containing either a string in the case of a text message or a
`Buffer` in the case of a binary message.
You can also listen for messages using the `driver.messages.on('data')` event,
which emits strings for text messages and buffers for binary messages.
#### `driver.on('error', function(event) {})`
Sets the callback to execute when a protocol error occurs due to the other peer
sending an invalid byte sequence. `event` will have a `message` attribute
describing the error.
#### `driver.on('close', function(event) {})`
Sets the callback to execute when the socket becomes closed. The `event` object
has `code` and `reason` attributes.
#### `driver.addExtension(extension)`
Registers a protocol extension whose operation will be negotiated via the
`Sec-WebSocket-Extensions` header. `extension` is any extension compatible with
the [websocket-extensions](https://github.com/faye/websocket-extensions-node)
framework.
#### `driver.setHeader(name, value)`
Sets a custom header to be sent as part of the handshake response, either from
the server or from the client. Must be called before `start()`, since this is
when the headers are serialized and sent.
#### `driver.start()`
Initiates the protocol by sending the handshake - either the response for a
server-side driver or the request for a client-side one. This should be the
first method you invoke. Returns `true` iff a handshake was sent.
#### `driver.parse(string)`
Takes a string and parses it, potentially resulting in message events being
emitted (see `on('message')` above) or in data being sent to `driver.io`. You
should send all data you receive via I/O to this method by piping a stream into
`driver.io`.
#### `driver.text(string)`
Sends a text message over the socket. If the socket handshake is not yet
complete, the message will be queued until it is. Returns `true` if the message
was sent or queued, and `false` if the socket can no longer send messages.
This method is equivalent to `driver.messages.write(string)`.
#### `driver.binary(buffer)`
Takes a `Buffer` and sends it as a binary message. Will queue and return `true`
or `false` the same way as the `text` method. It will also return `false` if the
driver does not support binary messages.
This method is equivalent to `driver.messages.write(buffer)`.
#### `driver.ping(string = '', function() {})`
Sends a ping frame over the socket, queueing it if necessary. `string` and the
callback are both optional. If a callback is given, it will be invoked when the
socket receives a pong frame whose content matches `string`. Returns `false` if
frames can no longer be sent, or if the driver does not support ping/pong.
#### `driver.close()`
Initiates the closing handshake if the socket is still open. For drivers with no
closing handshake, this will result in the immediate execution of the
`on('close')` driver. For drivers with a closing handshake, this sends a closing
frame and `emit('close')` will execute when a response is received or a protocol
error occurs.
#### `driver.version`
Returns the WebSocket version in use as a string. Will either be `hixie-75`,
`hixie-76` or `hybi-$version`.
#### `driver.protocol`
Returns a string containing the selected subprotocol, if any was agreed upon
using the `Sec-WebSocket-Protocol` mechanism. This value becomes available after
`emit('open')` has fired.
## License
(The MIT License)
Copyright (c) 2010-2015 James Coglan
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the 'Software'), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-40
View File
@@ -1,40 +0,0 @@
var WebSocket = require('../lib/faye/websocket');
var host = 'ws://localhost:9001',
agent = 'Faye (Node ' + process.version + ')',
cases = 0,
skip = [];
var socket = new WebSocket.Client(host + '/getCaseCount');
socket.onmessage = function(event) {
console.log('Total cases to run: ' + event.data);
cases = parseInt(event.data);
};
socket.onclose = function() {
var runCase = function(n) {
if (n > cases) {
socket = new WebSocket.Client(host + '/updateReports?agent=' + encodeURIComponent(agent));
socket.onclose = process.exit
} else if (skip.indexOf(n) >= 0) {
runCase(n + 1);
} else {
console.log('Running test case #' + n + ' ...');
socket = new WebSocket.Client(host + '/runCase?case=' + n + '&agent=' + encodeURIComponent(agent));
socket.onmessage = function(event) {
socket.send(event.data);
};
socket.onclose = function() {
runCase(n + 1);
};
}
};
runCase(1);
};
-41
View File
@@ -1,41 +0,0 @@
<!doctype html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>WebSocket benchmarks</title>
</head>
<body>
<div id="out"></div>
<script type="text/javascript">
var Socket = window.MozWebSocket || window.WebSocket,
socket = new Socket('ws://' + location.hostname + ':' + location.port + '/'),
out = document.getElementById('out');
var msg = '', n = 64000;
while (n--) msg += 'X';
var start = new Date().getTime();
function ping(event) {
var data = (event && event.data) || '0',
id = parseInt(data.split(':')[0]) + 1;
out.innerHTML = id;
if (id === 1000) {
var time = new Date().getTime() - start;
out.innerHTML = 'Time: ' + time;
} else {
socket.send(id + ':' + msg);
}
};
socket.onopen = ping;
socket.onmessage = ping;
</script>
</body>
</html>
-22
View File
@@ -1,22 +0,0 @@
var WebSocket = require('../lib/faye/websocket'),
port = process.argv[2] || 7000,
secure = process.argv[3] === 'ssl',
scheme = secure ? 'wss' : 'ws',
ws = new WebSocket.Client(scheme + '://localhost:' + port + '/');
console.log('Connecting to ' + ws.url);
ws.onopen = function(event) {
console.log('open');
ws.send('Hello, WebSocket!');
};
ws.onmessage = function(event) {
console.log('message', event.data);
// ws.close(1002, 'Going away');
};
ws.onclose = function(event) {
console.log('close', event.code, event.reason);
};
-21
View File
@@ -1,21 +0,0 @@
defaults
mode http
timeout client 5s
timeout connect 5s
timeout server 5s
frontend all 0.0.0.0:3000
mode http
timeout client 120s
option forwardfor
option http-server-close
option http-pretend-keepalive
default_backend sockets
backend sockets
balance uri depth 2
timeout server 120s
server socket1 127.0.0.1:7000
-35
View File
@@ -1,35 +0,0 @@
<!doctype html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>WebSocket test</title>
</head>
<body>
<h1>WebSocket test</h1>
<ul></ul>
<script type="text/javascript">
var logger = document.getElementsByTagName('ul')[0],
Socket = window.MozWebSocket || window.WebSocket,
socket = new Socket('ws://' + location.hostname + ':' + location.port + '/'),
index = 0;
socket.onopen = function() {
logger.innerHTML += '<li>OPEN</li>';
socket.send('Hello, world');
};
socket.addEventListener('message', function(event) {
logger.innerHTML += '<li>MESSAGE: ' + event.data + '</li>';
setTimeout(function() { socket.send(++index + ' ' + event.data) }, 2000);
});
socket.onclose = function(event) {
logger.innerHTML += '<li>CLOSE: ' + event.code + ', ' + event.reason + '</li>';
};
</script>
</body>
</html>
-41
View File
@@ -1,41 +0,0 @@
var WebSocket = require('../lib/faye/websocket'),
fs = require('fs'),
http = require('http'),
https = require('https');
var port = process.argv[2] || 7000,
secure = process.argv[3] === 'ssl';
var staticHandler = function(request, response) {
var path = request.url;
fs.readFile(__dirname + path, function(err, content) {
var status = err ? 404 : 200;
response.writeHead(status, {'Content-Type': 'text/html'});
response.write(content || 'Not found');
response.end();
});
};
var server = secure
? https.createServer({
key: fs.readFileSync(__dirname + '/../spec/server.key'),
cert: fs.readFileSync(__dirname + '/../spec/server.crt')
})
: http.createServer(staticHandler);
server.addListener('upgrade', function(request, socket, head) {
var ws = new WebSocket(request, socket, head);
console.log('open', ws.url, ws.version);
ws.onmessage = function(event) {
ws.send(event.data);
};
ws.onclose = function(event) {
console.log('close', event.code, event.reason);
ws = null;
};
});
server.listen(port);
+22
View File
@@ -0,0 +1,22 @@
var net = require('net'),
websocket = require('../lib/websocket/driver'),
deflate = require('permessage-deflate');
var server = net.createServer(function(connection) {
var driver = websocket.server();
driver.addExtension(deflate);
driver.on('connect', function() {
if (websocket.isWebSocket(driver)) driver.start();
});
driver.on('close', function() { connection.end() });
connection.on('error', function() {});
connection.pipe(driver.io);
driver.io.pipe(connection);
driver.messages.pipe(driver.messages);
});
server.listen(process.argv[2]);
-72
View File
@@ -1,72 +0,0 @@
// API and protocol references:
//
// * http://dev.w3.org/html5/websockets/
// * http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#interface-eventtarget
// * http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#interface-event
// * http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75
// * http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
// * http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17
var Draft75Parser = require('./websocket/draft75_parser'),
Draft76Parser = require('./websocket/draft76_parser'),
Protocol8Parser = require('./websocket/protocol8_parser'),
API = require('./websocket/api');
var getParser = function(request) {
var headers = request.headers;
return headers['sec-websocket-version']
? Protocol8Parser
: (headers['sec-websocket-key1'] && headers['sec-websocket-key2'])
? Draft76Parser
: Draft75Parser;
};
var isSecureConnection = function(request) {
if (request.headers['x-forwarded-proto']) {
return request.headers['x-forwarded-proto'] === 'https';
} else {
return (request.connection && request.connection.authorized !== undefined) ||
(request.socket && request.socket.secure);
}
};
var WebSocket = function(request, socket, head) {
this.request = request;
this._stream = request.socket;
var scheme = isSecureConnection(request) ? 'wss:' : 'ws:';
this.url = scheme + '//' + request.headers.host + request.url;
this.readyState = API.CONNECTING;
this.bufferedAmount = 0;
var Parser = getParser(request);
this._parser = new Parser(this);
var handshake = this._parser.handshakeResponse(head);
try { this._stream.write(handshake, 'binary') } catch (e) {}
this.readyState = API.OPEN;
this.version = this._parser.getVersion();
var event = new API.Event('open');
event.initEvent('open', false, false);
this.dispatchEvent(event);
var self = this;
this._stream.addListener('data', function(data) {
var response = self._parser.parse(data);
if (!response) return;
try { self._stream.write(response, 'binary') } catch (e) {}
});
['close', 'end', 'error'].forEach(function(event) {
self._stream.addListener(event, function() { self.close(1006, '', false) });
});
};
var API = require('./websocket/api');
for (var key in API) WebSocket.prototype[key] = API[key];
WebSocket.Client = require('./websocket/client');
module.exports = WebSocket;
-114
View File
@@ -1,114 +0,0 @@
var API = {
CONNECTING: 0,
OPEN: 1,
CLOSING: 2,
CLOSED: 3,
onopen: null,
onmessage: null,
onerror: null,
onclose: null,
receive: function(data) {
if (this.readyState !== API.OPEN) return false;
var event = new API.Event('message');
event.initEvent('message', false, false);
event.data = data;
this.dispatchEvent(event);
},
send: function(data, type, errorType) {
if (this.readyState === API.CLOSED) return false;
var frame = this._parser.frame(data, type, errorType);
try {
this._stream.write(frame, 'binary');
return true;
} catch (e) {
return false;
}
},
close: function(code, reason, ack) {
if (this.readyState === API.CLOSING ||
this.readyState === API.CLOSED) return;
this.readyState = API.CLOSING;
var close = function() {
this.readyState = API.CLOSED;
this._stream.end();
var event = new API.Event('close', {code: code || 1000, reason: reason || ''});
event.initEvent('close', false, false);
this.dispatchEvent(event);
};
if (ack !== false) {
if (this._parser.close) this._parser.close(code, reason, close, this);
else close.call(this);
} else {
if (this._parser.close) this._parser.close(code, reason);
close.call(this);
}
},
addEventListener: function(eventType, listener, useCapture) {
this._listeners = this._listeners || {};
var list = this._listeners[eventType] = this._listeners[eventType] || [];
list.push(listener);
},
removeEventListener: function(eventType, listener, useCapture) {
if (!this._listeners || !this._listeners[eventType]) return;
if (!listener) {
delete this._listeners[eventType];
return;
}
var list = this._listeners[eventType],
i = list.length;
while (i--) {
if (listener !== list[i]) continue;
list.splice(i,1);
}
},
dispatchEvent: function(event) {
event.target = event.currentTarget = this;
event.eventPhase = API.Event.AT_TARGET;
if (this['on' + event.type])
this['on' + event.type](event);
if (!this._listeners || !this._listeners[event.type]) return;
this._listeners[event.type].forEach(function(listener) {
listener(event);
}, this);
}
};
var Event = function(eventType, options) {
this.type = eventType;
for (var key in options)
this[key] = options[key];
};
Event.prototype.initEvent = function(eventType, canBubble, cancelable) {
this.type = eventType;
this.bubbles = canBubble;
this.cancelable = cancelable;
};
Event.prototype.stopPropagation = function() {};
Event.prototype.preventDefault = function() {};
Event.CAPTURING_PHASE = 1;
Event.AT_TARGET = 2;
Event.BUBBLING_PHASE = 3;
API.Event = Event;
module.exports = API;
-78
View File
@@ -1,78 +0,0 @@
var API = require('./api'),
net = require('net'),
tls = require('tls');
var Protocol8Parser = require('./protocol8_parser');
var Client = function(url) {
this.url = url;
this._uri = require('url').parse(url);
this.readyState = API.CONNECTING;
this.bufferedAmount = 0;
var secure = (this._uri.protocol === 'wss:'),
self = this,
onConnect = function() { self._onConnect() },
connection = secure
? tls.connect(this._uri.port || 443, this._uri.hostname, onConnect)
: net.createConnection(this._uri.port || 80, this._uri.hostname);
this._parser = new Protocol8Parser(this, {masking: true});
this._stream = connection;
if (!secure) connection.addListener('connect', onConnect);
connection.addListener('data', function(data) {
self._onData(data);
});
connection.addListener('close', function() {
self.close(1006, '', false);
});
connection.addListener('error', function() {});
};
Client.prototype._onConnect = function() {
this._handshake = this._parser.createHandshake(this._uri, this._stream);
this._message = [];
try {
this._stream.write(this._handshake.requestData(), 'binary');
} catch (e) {}
};
Client.prototype._onData = function(data) {
switch (this.readyState) {
case API.CONNECTING:
var bytes = this._handshake.parse(data);
for (var i = 0, n = bytes.length; i < n; i++)
this._message.push(bytes[i]);
if (!this._handshake.isComplete()) return;
if (this._handshake.isValid()) {
this.readyState = API.OPEN;
var event = new API.Event('open');
event.initEvent('open', false, false);
this.dispatchEvent(event);
this._parser.parse(this._message);
} else {
this.readyState = API.CLOSED;
var event = new API.Event('close');
event.initEvent('close', false, false);
this.dispatchEvent(event);
}
break;
case API.OPEN:
case API.CLOSING:
this._parser.parse(data);
}
};
for (var key in API) Client.prototype[key] = API[key];
module.exports = Client;
-63
View File
@@ -1,63 +0,0 @@
var Draft75Parser = function(webSocket) {
this._socket = webSocket;
this._buffer = [];
this._buffering = false;
};
var instance = {
FRAME_START : new Buffer([0x00]),
FRAME_END : new Buffer([0xFF]),
getVersion: function() {
return 'draft-75';
},
handshakeResponse: function() {
return new Buffer('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
'Upgrade: WebSocket\r\n' +
'Connection: Upgrade\r\n' +
'WebSocket-Origin: ' + this._socket.request.headers.origin + '\r\n' +
'WebSocket-Location: ' + this._socket.url + '\r\n\r\n',
'utf8');
},
parse: function(data) {
for (var i = 0, n = data.length; i < n; i++)
this._handleChar(data[i]);
},
frame: function(data) {
var buffer = new Buffer(data, 'utf8'),
frame = new Buffer(buffer.length + 2);
this.FRAME_START.copy(frame, 0);
buffer.copy(frame, 1);
this.FRAME_END.copy(frame, buffer.length + 1);
return frame;
},
_handleChar: function(data) {
switch (data) {
case 0x00:
this._buffering = true;
break;
case 0xFF:
this._buffer = new Buffer(this._buffer);
this._socket.receive(this._buffer.toString('utf8', 0, this._buffer.length));
this._buffer = [];
this._buffering = false;
break;
default:
if (this._buffering) this._buffer.push(data);
}
}
};
for (var key in instance)
Draft75Parser.prototype[key] = instance[key];
module.exports = Draft75Parser;
-79
View File
@@ -1,79 +0,0 @@
var crypto = require('crypto'),
Draft75Parser = require('./draft75_parser'),
Draft76Parser = function() { Draft75Parser.apply(this, arguments) };
var bridge = function() {};
bridge.prototype = Draft75Parser.prototype;
Draft76Parser.prototype = new bridge();
var numberFromKey = function(key) {
return parseInt(key.match(/[0-9]/g).join(''), 10);
};
var spacesInKey = function(key) {
return key.match(/ /g).length;
};
var bigEndian = function(number) {
var string = '';
[24,16,8,0].forEach(function(offset) {
string += String.fromCharCode(number >> offset & 0xFF);
});
return string;
};
Draft76Parser.prototype.getVersion = function() {
return 'draft-76';
};
Draft76Parser.prototype.handshakeResponse = function(head) {
var request = this._socket.request, tmp;
var response = new Buffer('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
'Upgrade: WebSocket\r\n' +
'Connection: Upgrade\r\n' +
'Sec-WebSocket-Origin: ' + request.headers.origin + '\r\n' +
'Sec-WebSocket-Location: ' + this._socket.url + '\r\n\r\n',
'binary');
var signature = this.handshakeSignature(head);
if (signature) {
tmp = new Buffer(response.length + signature.length);
response.copy(tmp, 0);
signature.copy(tmp, response.length);
response = tmp;
}
return response;
};
Draft76Parser.prototype.handshakeSignature = function(head) {
if (head.length === 0) return null;
this._handshakeComplete = true;
var request = this._socket.request,
key1 = request.headers['sec-websocket-key1'],
value1 = numberFromKey(key1) / spacesInKey(key1),
key2 = request.headers['sec-websocket-key2'],
value2 = numberFromKey(key2) / spacesInKey(key2),
MD5 = crypto.createHash('md5');
MD5.update(bigEndian(value1));
MD5.update(bigEndian(value2));
MD5.update(head.toString('binary'));
return new Buffer(MD5.digest('binary'), 'binary');
};
Draft76Parser.prototype.parse = function(data) {
if (this._handshakeComplete)
return Draft75Parser.prototype.parse.call(this, data);
return this.handshakeSignature(data);
};
module.exports = Draft76Parser;
-312
View File
@@ -1,312 +0,0 @@
var crypto = require('crypto'),
Handshake = require('./protocol8_parser/handshake'),
Reader = require('./protocol8_parser/stream_reader');
var Protocol8Parser = function(webSocket, options) {
this._reset();
this._socket = webSocket;
this._reader = new Reader();
this._stage = 0;
this._masking = options && options.masking;
};
var instance = {
FIN: 128,
MASK: 128,
RSV1: 64,
RSV2: 32,
RSV3: 16,
OPCODE: 15,
LENGTH: 127,
OPCODES: {
continuation: 0,
text: 1,
binary: 2,
close: 8,
ping: 9,
pong: 10
},
ERRORS: {
normal_closure: 1000,
going_away: 1001,
protocol_error: 1002,
unacceptable: 1003,
encoding_error: 1007,
policy_violation: 1008,
too_large: 1009,
extension_error: 1010
},
FRAGMENTED_OPCODES: [0,1,2],
OPENING_OPCODES: [1,2],
ERROR_CODES: [1000,1001,1002,1003,1007,1008,1009,1010],
UTF8_MATCH: /^([\x00-\x7F]|[\xC2-\xDF][\x80-\xBF]|\xE0[\xA0-\xBF][\x80-\xBF]|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}|\xED[\x80-\x9F][\x80-\xBF]|\xF0[\x90-\xBF][\x80-\xBF]{2}|[\xF1-\xF3][\x80-\xBF]{3}|\xF4[\x80-\x8F][\x80-\xBF]{2})*$/,
getVersion: function() {
var version = this._socket.request.headers['sec-websocket-version'];
return 'protocol-' + version;
},
handshakeResponse: function() {
var secKey = this._socket.request.headers['sec-websocket-key'];
if (!secKey) return;
var SHA1 = crypto.createHash('sha1');
SHA1.update(secKey + Handshake.GUID);
var accept = SHA1.digest('base64');
return new Buffer('HTTP/1.1 101 Switching Protocols\r\n' +
'Upgrade: websocket\r\n' +
'Connection: Upgrade\r\n' +
'Sec-WebSocket-Accept: ' + accept + '\r\n\r\n',
'utf8');
},
createHandshake: function(uri) {
return new Handshake(uri);
},
parse: function(data) {
this._reader.put(data);
var buffer = true;
while (buffer) {
switch (this._stage) {
case 0:
buffer = this._reader.read(1);
if (buffer) this._parseOpcode(buffer[0]);
break;
case 1:
buffer = this._reader.read(1);
if (buffer) this._parseLength(buffer[0]);
break;
case 2:
buffer = this._reader.read(this._lengthSize);
if (buffer) this._parseExtendedLength(buffer);
break;
case 3:
buffer = this._reader.read(4);
if (buffer) {
this._mask = buffer;
this._stage = 4;
}
break;
case 4:
buffer = this._reader.read(this._length);
if (buffer) {
this._payload = buffer;
this._emitFrame();
this._stage = 0;
}
break;
}
}
},
_parseOpcode: function(data) {
var rsvs = [this.RSV1, this.RSV2, this.RSV3].filter(function(rsv) {
return (data & rsv) === rsv;
}, this);
if (rsvs.length > 0) return this._socket.close(this.ERRORS.protocol_error, null, false);
this._final = (data & this.FIN) === this.FIN;
this._opcode = (data & this.OPCODE);
this._mask = [];
this._payload = [];
var valid = false;
for (var key in this.OPCODES) {
if (this.OPCODES[key] === this._opcode)
valid = true;
}
if (!valid) return this._socket.close(this.ERRORS.protocol_error, null, false);
if (this.FRAGMENTED_OPCODES.indexOf(this._opcode) < 0 && !this._final)
return this._socket.close(this.ERRORS.protocol_error, null, false);
if (this._mode && this.OPENING_OPCODES.indexOf(this._opcode) >= 0)
return this._socket.close(this.ERRORS.protocol_error, null, false);
this._stage = 1;
},
_parseLength: function(data) {
this._masked = (data & this.MASK) === this.MASK;
this._length = (data & this.LENGTH);
if (this._length >= 0 && this._length <= 125) {
this._stage = this._masked ? 3 : 4;
} else {
this._lengthBuffer = [];
this._lengthSize = (this._length === 126 ? 2 : 8);
this._stage = 2;
}
},
_parseExtendedLength: function(buffer) {
this._length = this._getInteger(buffer);
this._stage = this._masked ? 3 : 4;
},
frame: function(data, type, code) {
if (this._closed) return;
var isText = (typeof data === 'string'),
opcode = this.OPCODES[type || (isText ? 'text' : 'binary')],
buffer = isText ? new Buffer(data, 'utf8') : data,
insert = code ? 2 : 0,
length = buffer.length + insert,
header = (length <= 125) ? 2 : (length <= 65535 ? 4 : 10),
offset = header + (this._masking ? 4 : 0),
masked = this._masking ? this.MASK : 0,
frame = new Buffer(length + offset),
mask, i;
frame[0] = this.FIN | opcode;
if (length <= 125) {
frame[1] = masked | length;
} else if (length <= 65535) {
frame[1] = masked | 126;
frame[2] = Math.floor(length / 256);
frame[3] = length & 255;
} else {
frame[1] = masked | 127;
frame[2] = Math.floor(length / Math.pow(2,56)) & 255;
frame[3] = Math.floor(length / Math.pow(2,48)) & 255;
frame[4] = Math.floor(length / Math.pow(2,40)) & 255;
frame[5] = Math.floor(length / Math.pow(2,32)) & 255;
frame[6] = Math.floor(length / Math.pow(2,24)) & 255;
frame[7] = Math.floor(length / Math.pow(2,16)) & 255;
frame[8] = Math.floor(length / Math.pow(2,8)) & 255;
frame[9] = length & 255;
}
if (code) {
frame[offset] = Math.floor(code / 256);
frame[offset+1] = code & 255;
}
buffer.copy(frame, offset + insert);
if (this._masking) {
mask = new Buffer([1,2,3,4].map(function() { return Math.floor(Math.random() * 256) }));
mask.copy(frame, header);
for (i = 0; i < length; i++)
frame[offset + i] = frame[offset + i] ^ mask[i % 4];
}
return frame;
},
close: function(code, reason, callback, context) {
if (this._closed) return;
if (callback) this._closingCallback = [callback, context];
this._socket.send(reason || '', 'close', code || this.ERRORS.normal_closure);
this._closed = true;
},
buffer: function(fragment) {
for (var i = 0, n = fragment.length; i < n; i++)
this._buffer.push(fragment[i]);
},
_emitFrame: function() {
var payload = this._unmask(this._payload, this._mask),
opcode = this._opcode;
if (opcode === this.OPCODES.continuation) {
if (!this._mode) return this._socket.close(this.ERRORS.protocol_error, null, false);
this.buffer(payload);
if (this._final) {
var message = new Buffer(this._buffer);
if (this._mode === 'text') message = this._encode(message);
this._reset();
if (message !== null) this._socket.receive(message);
else this._socket.close(this.ERRORS.encoding_error, null, false);
}
}
else if (opcode === this.OPCODES.text) {
if (this._final) {
var message = this._encode(payload);
if (message !== null) this._socket.receive(message);
else this._socket.close(this.ERRORS.encoding_error, null, false);
} else {
this._mode = 'text';
this.buffer(payload);
}
}
else if (opcode === this.OPCODES.binary) {
if (this._final) {
this._socket.receive(payload);
} else {
this._mode = 'binary';
this.buffer(payload);
}
}
else if (opcode === this.OPCODES.close) {
var code = (payload.length >= 2) ? 256 * payload[0] + payload[1] : null,
reason = (payload.length > 2) ? this._encode(payload.slice(2)) : null;
if (!(payload.length === 0) &&
!(code !== null && code >= 3000 && code < 5000) &&
this.ERROR_CODES.indexOf(code) < 0)
code = this.ERRORS.protocol_error;
if (payload.length > 125 || (payload.length > 2 && !reason))
code = this.ERRORS.protocol_error;
this._socket.close(code, (payload.length > 2) ? reason : null, false);
if (this._closingCallback)
this._closingCallback[0].call(this._closingCallback[1]);
}
else if (opcode === this.OPCODES.ping) {
if (payload.length > 125) return this._socket.close(this.ERRORS.protocol_error, null, false);
this._socket.send(payload, 'pong');
}
},
_reset: function() {
this._mode = null;
this._buffer = [];
},
_encode: function(buffer) {
try {
var string = buffer.toString('binary', 0, buffer.length);
if (!this.UTF8_MATCH.test(string)) return null;
} catch (e) {}
return buffer.toString('utf8', 0, buffer.length);
},
_getInteger: function(bytes) {
var number = 0;
for (var i = 0, n = bytes.length; i < n; i++)
number += bytes[i] << (8 * (n - 1 - i));
return number;
},
_unmask: function(payload, mask) {
var unmasked = new Buffer(payload.length), b;
for (var i = 0, n = payload.length; i < n; i++) {
b = payload[i];
if (mask.length > 0) b = b ^ mask[i % 4];
unmasked[i] = b;
}
return unmasked;
}
};
for (var key in instance)
Protocol8Parser.prototype[key] = instance[key];
module.exports = Protocol8Parser;
@@ -1,77 +0,0 @@
var crypto = require('crypto');
var Handshake = function(uri) {
this._uri = uri;
var buffer = new Buffer(16), i = 16;
while (i--) buffer[i] = Math.floor(Math.random() * 256);
this._key = buffer.toString('base64');
var SHA1 = crypto.createHash('sha1');
SHA1.update(this._key + Handshake.GUID);
this._accept = SHA1.digest('base64');
var HTTPParser = process.binding('http_parser').HTTPParser,
parser = new HTTPParser(HTTPParser.RESPONSE || 'response'),
current = null,
self = this;
this._nodeVersion = HTTPParser.RESPONSE ? 6 : 4;
this._complete = false;
this._headers = {};
this._parser = parser;
parser.onHeaderField = function(b, start, length) {
current = b.toString('utf8', start, start + length);
};
parser.onHeaderValue = function(b, start, length) {
self._headers[current] = b.toString('utf8', start, start + length);
};
parser.onHeadersComplete = function(info) {
self._status = info.statusCode;
var headers = info.headers;
if (!headers) return;
for (var i = 0, n = headers.length; i < n; i += 2)
self._headers[headers[i]] = headers[i+1];
};
parser.onMessageComplete = function() {
self._complete = true;
};
};
Handshake.GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
Handshake.prototype.requestData = function() {
var u = this._uri;
return new Buffer('GET ' + u.pathname + (u.search || '') + ' HTTP/1.1\r\n' +
'Host: ' + u.hostname + (u.port ? ':' + u.port : '') + '\r\n' +
'Upgrade: websocket\r\n' +
'Connection: Upgrade\r\n' +
'Sec-WebSocket-Key: ' + this._key + '\r\n' +
'Sec-WebSocket-Version: 8\r\n\r\n',
'utf8');
};
Handshake.prototype.parse = function(data) {
var consumed = this._parser.execute(data, 0, data.length),
offset = (this._nodeVersion < 6) ? 1 : 0;
return (consumed === data.length) ? [] : data.slice(consumed + offset);
};
Handshake.prototype.isComplete = function() {
return this._complete;
};
Handshake.prototype.isValid = function() {
if (this._status !== 101) return false;
var upgrade = this._headers.Upgrade,
connection = this._headers.Connection;
return upgrade && /^websocket$/i.test(upgrade) &&
connection && connection.split(/\s*,\s*/).indexOf('Upgrade') >= 0 &&
this._headers['Sec-WebSocket-Accept'] === this._accept;
};
module.exports = Handshake;
@@ -1,43 +0,0 @@
var StreamReader = function() {
this._queue = [];
this._cursor = 0;
};
StreamReader.prototype.read = function(bytes) {
return this._readBuffer(bytes);
};
StreamReader.prototype.put = function(buffer) {
if (!buffer || buffer.length === 0) return;
if (!buffer.copy) buffer = new Buffer(buffer);
this._queue.push(buffer);
};
StreamReader.prototype._readBuffer = function(length) {
var buffer = new Buffer(length),
queue = this._queue,
remain = length,
n = queue.length,
i = 0,
chunk, offset, size;
if (remain === 0) return buffer;
while (remain > 0 && i < n) {
chunk = queue[i];
offset = (i === 0) ? this._cursor : 0;
size = Math.min(remain, chunk.length - offset);
chunk.copy(buffer, length - remain, offset, offset + size);
remain -= size;
i += 1;
}
if (remain > 0) return null;
queue.splice(0, i-1);
this._cursor = (i === 1 ? this._cursor : 0) + size;
return buffer;
};
module.exports = StreamReader;
+50
View File
@@ -0,0 +1,50 @@
'use strict';
// Protocol references:
//
// * http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75
// * http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
// * http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17
var Base = require('./driver/base'),
Client = require('./driver/client'),
Server = require('./driver/server');
var Driver = {
client: function(url, options) {
options = options || {};
if (options.masking === undefined) options.masking = true;
return new Client(url, options);
},
server: function(options) {
options = options || {};
if (options.requireMasking === undefined) options.requireMasking = true;
return new Server(options);
},
http: function() {
return Server.http.apply(Server, arguments);
},
isSecureRequest: function(request) {
return Server.isSecureRequest(request);
},
isWebSocket: function(request) {
if (request.method !== 'GET') return false;
var connection = request.headers.connection || '',
upgrade = request.headers.upgrade || '';
return request.method === 'GET' &&
connection.toLowerCase().split(/ *, */).indexOf('upgrade') >= 0 &&
upgrade.toLowerCase() === 'websocket';
},
validateOptions: function(options, validKeys) {
Base.validateOptions(options, validKeys);
}
};
module.exports = Driver;
+139
View File
@@ -0,0 +1,139 @@
'use strict';
var Emitter = require('events').EventEmitter,
util = require('util'),
streams = require('../streams'),
Headers = require('./headers');
var Base = function(request, url, options) {
Emitter.call(this);
Base.validateOptions(options || {}, ['maxLength', 'masking', 'requireMasking', 'protocols']);
this._request = request;
this._options = options || {};
this._maxLength = this._options.maxLength || this.MAX_LENGTH;
this._headers = new Headers();
this.__queue = [];
this.readyState = 0;
this.url = url;
this.io = new streams.IO(this);
this.messages = new streams.Messages(this);
this._bindEventListeners();
};
util.inherits(Base, Emitter);
Base.validateOptions = function(options, validKeys) {
for (var key in options) {
if (validKeys.indexOf(key) < 0)
throw new Error('Unrecognized option: ' + key);
}
};
var instance = {
// This is 64MB, small enough for an average VPS to handle without
// crashing from process out of memory
MAX_LENGTH: 0x3ffffff,
STATES: ['connecting', 'open', 'closing', 'closed'],
_bindEventListeners: function() {
var self = this;
// Protocol errors are informational and do not have to be handled
this.messages.on('error', function() {});
this.on('message', function(event) {
var messages = self.messages;
if (messages.readable) messages.emit('data', event.data);
});
this.on('error', function(error) {
var messages = self.messages;
if (messages.readable) messages.emit('error', error);
});
this.on('close', function() {
var messages = self.messages;
if (!messages.readable) return;
messages.readable = messages.writable = false;
messages.emit('end');
});
},
getState: function() {
return this.STATES[this.readyState] || null;
},
addExtension: function(extension) {
return false;
},
setHeader: function(name, value) {
if (this.readyState > 0) return false;
this._headers.set(name, value);
return true;
},
start: function() {
if (this.readyState !== 0) return false;
this._write(this._handshakeResponse());
if (this._stage !== -1) this._open();
return true;
},
text: function(message) {
return this.frame(message);
},
binary: function(message) {
return false;
},
ping: function() {
return false;
},
close: function(reason, code) {
if (this.readyState !== 1) return false;
this.readyState = 3;
this.emit('close', new Base.CloseEvent(null, null));
return true;
},
_open: function() {
this.readyState = 1;
this.__queue.forEach(function(args) { this.frame.apply(this, args) }, this);
this.__queue = [];
this.emit('open', new Base.OpenEvent());
},
_queue: function(message) {
this.__queue.push(message);
return true;
},
_write: function(chunk) {
var io = this.io;
if (io.readable) io.emit('data', chunk);
}
};
for (var key in instance)
Base.prototype[key] = instance[key];
Base.ConnectEvent = function() {};
Base.OpenEvent = function() {};
Base.CloseEvent = function(code, reason) {
this.code = code;
this.reason = reason;
};
Base.MessageEvent = function(data) {
this.data = data;
};
module.exports = Base;
+132
View File
@@ -0,0 +1,132 @@
'use strict';
var crypto = require('crypto'),
url = require('url'),
util = require('util'),
HttpParser = require('../http_parser'),
Base = require('./base'),
Hybi = require('./hybi'),
Proxy = require('./proxy');
var Client = function(_url, options) {
this.version = 'hybi-13';
Hybi.call(this, null, _url, options);
this.readyState = -1;
this._key = Client.generateKey();
this._accept = Hybi.generateAccept(this._key);
this._http = new HttpParser('response');
var uri = url.parse(this.url),
auth = uri.auth && new Buffer(uri.auth, 'utf8').toString('base64');
this._pathname = (uri.pathname || '/') + (uri.search || '');
this._headers.set('Host', uri.host);
this._headers.set('Upgrade', 'websocket');
this._headers.set('Connection', 'Upgrade');
this._headers.set('Sec-WebSocket-Key', this._key);
this._headers.set('Sec-WebSocket-Version', '13');
if (this._protocols.length > 0)
this._headers.set('Sec-WebSocket-Protocol', this._protocols.join(', '));
if (auth)
this._headers.set('Authorization', 'Basic ' + auth);
};
util.inherits(Client, Hybi);
Client.generateKey = function() {
return crypto.randomBytes(16).toString('base64');
};
var instance = {
proxy: function(origin, options) {
return new Proxy(this, origin, options);
},
start: function() {
if (this.readyState !== -1) return false;
this._write(this._handshakeRequest());
this.readyState = 0;
return true;
},
parse: function(data) {
if (this.readyState > 0) return Hybi.prototype.parse.call(this, data);
this._http.parse(data);
if (!this._http.isComplete()) return;
this._validateHandshake();
if (this.readyState === 3) return;
this._open();
this.parse(this._http.body);
},
_handshakeRequest: function() {
var extensions = this._extensions.generateOffer();
if (extensions)
this._headers.set('Sec-WebSocket-Extensions', extensions);
var start = 'GET ' + this._pathname + ' HTTP/1.1',
headers = [start, this._headers.toString(), ''];
return new Buffer(headers.join('\r\n'), 'utf8');
},
_failHandshake: function(message) {
message = 'Error during WebSocket handshake: ' + message;
this.emit('error', new Error(message));
this.readyState = 3;
this.emit('close', new Base.CloseEvent(this.ERRORS.protocol_error, message));
},
_validateHandshake: function() {
this.statusCode = this._http.statusCode;
this.headers = this._http.headers;
if (this._http.statusCode !== 101)
return this._failHandshake('Unexpected response code: ' + this._http.statusCode);
var headers = this._http.headers,
upgrade = headers['upgrade'] || '',
connection = headers['connection'] || '',
accept = headers['sec-websocket-accept'] || '',
protocol = headers['sec-websocket-protocol'] || '';
if (upgrade === '')
return this._failHandshake("'Upgrade' header is missing");
if (upgrade.toLowerCase() !== 'websocket')
return this._failHandshake("'Upgrade' header value is not 'WebSocket'");
if (connection === '')
return this._failHandshake("'Connection' header is missing");
if (connection.toLowerCase() !== 'upgrade')
return this._failHandshake("'Connection' header value is not 'Upgrade'");
if (accept !== this._accept)
return this._failHandshake('Sec-WebSocket-Accept mismatch');
this.protocol = null;
if (protocol !== '') {
if (this._protocols.indexOf(protocol) < 0)
return this._failHandshake('Sec-WebSocket-Protocol mismatch');
else
this.protocol = protocol;
}
try {
this._extensions.activate(this.headers['sec-websocket-extensions']);
} catch (e) {
return this._failHandshake(e.message);
}
}
};
for (var key in instance)
Client.prototype[key] = instance[key];
module.exports = Client;
+120
View File
@@ -0,0 +1,120 @@
'use strict';
var Base = require('./base'),
util = require('util');
var Draft75 = function(request, url, options) {
Base.apply(this, arguments);
this._stage = 0;
this.version = 'hixie-75';
this._headers.set('Upgrade', 'WebSocket');
this._headers.set('Connection', 'Upgrade');
this._headers.set('WebSocket-Origin', this._request.headers.origin);
this._headers.set('WebSocket-Location', this.url);
};
util.inherits(Draft75, Base);
var instance = {
close: function() {
if (this.readyState === 3) return false;
this.readyState = 3;
this.emit('close', new Base.CloseEvent(null, null));
return true;
},
parse: function(buffer) {
if (this.readyState > 1) return;
var data, message, value;
for (var i = 0, n = buffer.length; i < n; i++) {
data = buffer[i];
switch (this._stage) {
case -1:
this._body.push(data);
this._sendHandshakeBody();
break;
case 0:
this._parseLeadingByte(data);
break;
case 1:
value = (data & 0x7F);
this._length = value + 128 * this._length;
if (this._closing && this._length === 0) {
return this.close();
}
else if ((0x80 & data) !== 0x80) {
if (this._length === 0) {
this._stage = 0;
}
else {
this._skipped = 0;
this._stage = 2;
}
}
break;
case 2:
if (data === 0xFF) {
message = new Buffer(this._buffer).toString('utf8', 0, this._buffer.length);
this.emit('message', new Base.MessageEvent(message));
this._stage = 0;
}
else {
if (this._length) {
this._skipped += 1;
if (this._skipped === this._length)
this._stage = 0;
} else {
this._buffer.push(data);
if (this._buffer.length > this._maxLength) return this.close();
}
}
break;
}
}
},
frame: function(data) {
if (this.readyState === 0) return this._queue([data]);
if (this.readyState > 1) return false;
var buffer = new Buffer(data, 'utf8'),
frame = new Buffer(buffer.length + 2);
frame[0] = 0x00;
frame[buffer.length + 1] = 0xFF;
buffer.copy(frame, 1);
this._write(frame);
return true;
},
_handshakeResponse: function() {
var start = 'HTTP/1.1 101 Web Socket Protocol Handshake',
headers = [start, this._headers.toString(), ''];
return new Buffer(headers.join('\r\n'), 'utf8');
},
_parseLeadingByte: function(data) {
if ((0x80 & data) === 0x80) {
this._length = 0;
this._stage = 1;
} else {
delete this._length;
delete this._skipped;
this._buffer = [];
this._stage = 2;
}
}
};
for (var key in instance)
Draft75.prototype[key] = instance[key];
module.exports = Draft75;
+110
View File
@@ -0,0 +1,110 @@
'use strict';
var Base = require('./base'),
Draft75 = require('./draft75'),
crypto = require('crypto'),
util = require('util');
var numberFromKey = function(key) {
return parseInt(key.match(/[0-9]/g).join(''), 10);
};
var spacesInKey = function(key) {
return key.match(/ /g).length;
};
var bigEndian = function(number) {
var string = '';
[24, 16, 8, 0].forEach(function(offset) {
string += String.fromCharCode(number >> offset & 0xFF);
});
return string;
};
var Draft76 = function(request, url, options) {
Draft75.apply(this, arguments);
this._stage = -1;
this._body = [];
this.version = 'hixie-76';
this._headers.clear();
this._headers.set('Upgrade', 'WebSocket');
this._headers.set('Connection', 'Upgrade');
this._headers.set('Sec-WebSocket-Origin', this._request.headers.origin);
this._headers.set('Sec-WebSocket-Location', this.url);
};
util.inherits(Draft76, Draft75);
var instance = {
BODY_SIZE: 8,
start: function() {
if (!Draft75.prototype.start.call(this)) return false;
this._started = true;
this._sendHandshakeBody();
return true;
},
close: function() {
if (this.readyState === 3) return false;
this._write(new Buffer([0xFF, 0x00]));
this.readyState = 3;
this.emit('close', new Base.CloseEvent(null, null));
return true;
},
_handshakeResponse: function() {
var start = 'HTTP/1.1 101 WebSocket Protocol Handshake',
headers = [start, this._headers.toString(), ''];
return new Buffer(headers.join('\r\n'), 'binary');
},
_handshakeSignature: function() {
if (this._body.length < this.BODY_SIZE) return null;
var body = new Buffer(this._body.slice(0, this.BODY_SIZE));
var headers = this._request.headers,
key1 = headers['sec-websocket-key1'],
value1 = numberFromKey(key1) / spacesInKey(key1),
key2 = headers['sec-websocket-key2'],
value2 = numberFromKey(key2) / spacesInKey(key2),
md5 = crypto.createHash('md5');
md5.update(bigEndian(value1));
md5.update(bigEndian(value2));
md5.update(body.toString('binary'));
return new Buffer(md5.digest('binary'), 'binary');
},
_sendHandshakeBody: function() {
if (!this._started) return;
var signature = this._handshakeSignature();
if (!signature) return;
this._write(signature);
this._stage = 0;
this._open();
if (this._body.length > this.BODY_SIZE)
this.parse(this._body.slice(this.BODY_SIZE));
},
_parseLeadingByte: function(data) {
if (data !== 0xFF)
return Draft75.prototype._parseLeadingByte.call(this, data);
this._closing = true;
this._length = 0;
this._stage = 1;
}
};
for (var key in instance)
Draft76.prototype[key] = instance[key];
module.exports = Draft76;
+35
View File
@@ -0,0 +1,35 @@
'use strict';
var Headers = function() {
this.clear();
};
Headers.prototype.ALLOWED_DUPLICATES = ['set-cookie', 'set-cookie2', 'warning', 'www-authenticate'];
Headers.prototype.clear = function() {
this._sent = {};
this._lines = [];
};
Headers.prototype.set = function(name, value) {
if (value === undefined) return;
name = this._strip(name);
value = this._strip(value);
var key = name.toLowerCase();
if (!this._sent.hasOwnProperty(key) || this.ALLOWED_DUPLICATES.indexOf(key) >= 0) {
this._sent[key] = true;
this._lines.push(name + ': ' + value + '\r\n');
}
};
Headers.prototype.toString = function() {
return this._lines.join('');
};
Headers.prototype._strip = function(string) {
return string.toString().replace(/^ */, '').replace(/ *$/, '');
};
module.exports = Headers;
+474
View File
@@ -0,0 +1,474 @@
'use strict';
var crypto = require('crypto'),
util = require('util'),
Extensions = require('websocket-extensions'),
Base = require('./base'),
Frame = require('./hybi/frame'),
Message = require('./hybi/message'),
Reader = require('./hybi/stream_reader');
var Hybi = function(request, url, options) {
Base.apply(this, arguments);
this._extensions = new Extensions();
this._reader = new Reader();
this._stage = 0;
this._masking = this._options.masking;
this._protocols = this._options.protocols || [];
this._requireMasking = this._options.requireMasking;
this._pingCallbacks = {};
if (typeof this._protocols === 'string')
this._protocols = this._protocols.split(/ *, */);
if (!this._request) return;
var secKey = this._request.headers['sec-websocket-key'],
protos = this._request.headers['sec-websocket-protocol'],
version = this._request.headers['sec-websocket-version'],
supported = this._protocols;
this._headers.set('Upgrade', 'websocket');
this._headers.set('Connection', 'Upgrade');
this._headers.set('Sec-WebSocket-Accept', Hybi.generateAccept(secKey));
if (protos !== undefined) {
if (typeof protos === 'string') protos = protos.split(/ *, */);
this.protocol = protos.filter(function(p) { return supported.indexOf(p) >= 0 })[0];
if (this.protocol) this._headers.set('Sec-WebSocket-Protocol', this.protocol);
}
this.version = 'hybi-' + version;
};
util.inherits(Hybi, Base);
Hybi.mask = function(payload, mask, offset) {
if (!mask || mask.length === 0) return payload;
offset = offset || 0;
for (var i = 0, n = payload.length - offset; i < n; i++) {
payload[offset + i] = payload[offset + i] ^ mask[i % 4];
}
return payload;
};
Hybi.generateAccept = function(key) {
var sha1 = crypto.createHash('sha1');
sha1.update(key + Hybi.GUID);
return sha1.digest('base64');
};
Hybi.GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
var instance = {
BYTE: 255,
FIN: 128,
MASK: 128,
RSV1: 64,
RSV2: 32,
RSV3: 16,
OPCODE: 15,
LENGTH: 127,
OPCODES: {
continuation: 0,
text: 1,
binary: 2,
close: 8,
ping: 9,
pong: 10
},
OPCODE_CODES: [0, 1, 2, 8, 9, 10],
MESSAGE_OPCODES: [0, 1, 2],
OPENING_OPCODES: [1, 2],
TWO_POWERS: [0, 1, 2, 3, 4, 5, 6, 7].map(function(n) { return Math.pow(2, 8 * n) }),
ERRORS: {
normal_closure: 1000,
going_away: 1001,
protocol_error: 1002,
unacceptable: 1003,
encoding_error: 1007,
policy_violation: 1008,
too_large: 1009,
extension_error: 1010,
unexpected_condition: 1011
},
ERROR_CODES: [1000, 1001, 1002, 1003, 1007, 1008, 1009, 1010, 1011],
MIN_RESERVED_ERROR: 3000,
MAX_RESERVED_ERROR: 4999,
// http://www.w3.org/International/questions/qa-forms-utf-8.en.php
UTF8_MATCH: /^([\x00-\x7F]|[\xC2-\xDF][\x80-\xBF]|\xE0[\xA0-\xBF][\x80-\xBF]|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}|\xED[\x80-\x9F][\x80-\xBF]|\xF0[\x90-\xBF][\x80-\xBF]{2}|[\xF1-\xF3][\x80-\xBF]{3}|\xF4[\x80-\x8F][\x80-\xBF]{2})*$/,
addExtension: function(extension) {
this._extensions.add(extension);
return true;
},
parse: function(data) {
this._reader.put(data);
var buffer = true;
while (buffer) {
switch (this._stage) {
case 0:
buffer = this._reader.read(1);
if (buffer) this._parseOpcode(buffer[0]);
break;
case 1:
buffer = this._reader.read(1);
if (buffer) this._parseLength(buffer[0]);
break;
case 2:
buffer = this._reader.read(this._frame.lengthBytes);
if (buffer) this._parseExtendedLength(buffer);
break;
case 3:
buffer = this._reader.read(4);
if (buffer) {
this._frame.maskingKey = buffer;
this._stage = 4;
}
break;
case 4:
buffer = this._reader.read(this._frame.length);
if (buffer) {
this._emitFrame(buffer);
this._stage = 0;
}
break;
default:
buffer = null;
}
}
},
text: function(message) {
if (this.readyState > 1) return false;
return this.frame(message, 'text');
},
binary: function(message) {
if (this.readyState > 1) return false;
return this.frame(message, 'binary');
},
ping: function(message, callback) {
if (this.readyState > 1) return false;
message = message || '';
if (callback) this._pingCallbacks[message] = callback;
return this.frame(message, 'ping');
},
close: function(reason, code) {
reason = reason || '';
code = code || this.ERRORS.normal_closure;
if (this.readyState <= 0) {
this.readyState = 3;
this.emit('close', new Base.CloseEvent(code, reason));
return true;
} else if (this.readyState === 1) {
this.readyState = 2;
this._extensions.close(function() { this.frame(reason, 'close', code) }, this);
return true;
} else {
return false;
}
},
frame: function(data, type, code) {
if (this.readyState <= 0) return this._queue([data, type, code]);
if (this.readyState > 2) return false;
if (data instanceof Array) data = new Buffer(data);
var message = new Message(),
isText = (typeof data === 'string'),
payload, buffer;
message.rsv1 = message.rsv2 = message.rsv3 = false;
message.opcode = this.OPCODES[type || (isText ? 'text' : 'binary')];
payload = isText ? new Buffer(data, 'utf8') : data;
if (code) {
buffer = payload;
payload = new Buffer(2 + buffer.length);
payload[0] = ~~(code / 256) & this.BYTE;
payload[1] = code & this.BYTE;
buffer.copy(payload, 2);
}
message.data = payload;
var onMessageReady = function(message) {
var frame = new Frame();
frame.final = true;
frame.rsv1 = message.rsv1;
frame.rsv2 = message.rsv2;
frame.rsv3 = message.rsv3;
frame.opcode = message.opcode;
frame.masked = !!this._masking;
frame.length = message.data.length;
frame.payload = message.data;
if (frame.masked) frame.maskingKey = crypto.randomBytes(4);
this._sendFrame(frame);
};
if (this.MESSAGE_OPCODES.indexOf(message.opcode) >= 0)
this._extensions.processOutgoingMessage(message, function(error, message) {
if (error) return this._fail('extension_error', error.message);
onMessageReady.call(this, message);
}, this);
else
onMessageReady.call(this, message);
return true;
},
_sendFrame: function(frame) {
var length = frame.length,
header = (length <= 125) ? 2 : (length <= 65535 ? 4 : 10),
offset = header + (frame.masked ? 4 : 0),
buffer = new Buffer(offset + length),
BYTE = this.BYTE,
masked = frame.masked ? this.MASK : 0;
buffer[0] = (frame.final ? this.FIN : 0) |
(frame.rsv1 ? this.RSV1 : 0) |
(frame.rsv2 ? this.RSV2 : 0) |
(frame.rsv3 ? this.RSV3 : 0) |
frame.opcode;
if (length <= 125) {
buffer[1] = masked | length;
} else if (length <= 65535) {
buffer[1] = masked | 126;
buffer[2] = ~~(length / 256);
buffer[3] = length & BYTE;
} else {
buffer[1] = masked | 127;
buffer[2] = ~~(length / Math.pow(2, 56)) & BYTE;
buffer[3] = ~~(length / Math.pow(2, 48)) & BYTE;
buffer[4] = ~~(length / Math.pow(2, 40)) & BYTE;
buffer[5] = ~~(length / Math.pow(2, 32)) & BYTE;
buffer[6] = ~~(length / Math.pow(2, 24)) & BYTE;
buffer[7] = ~~(length / Math.pow(2, 16)) & BYTE;
buffer[8] = ~~(length / Math.pow(2, 8)) & BYTE;
buffer[9] = length & BYTE;
}
if (frame.masked) {
frame.maskingKey.copy(buffer, header);
Hybi.mask(frame.payload, frame.maskingKey).copy(buffer, offset);
} else {
frame.payload.copy(buffer, offset);
}
this._write(buffer);
},
_handshakeResponse: function() {
var extensions = this._extensions.generateResponse(this._request.headers['sec-websocket-extensions']);
if (extensions) this._headers.set('Sec-WebSocket-Extensions', extensions);
var start = 'HTTP/1.1 101 Switching Protocols',
headers = [start, this._headers.toString(), ''];
return new Buffer(headers.join('\r\n'), 'utf8');
},
_shutdown: function(code, reason) {
delete this._frame;
delete this._message;
this.readyState = 2;
this._stage = 5;
this._extensions.close(function() {
this.frame(reason, 'close', code);
this.readyState = 3;
this.emit('close', new Base.CloseEvent(code, reason));
}, this);
},
_fail: function(type, message) {
if (this.readyState > 1) return;
this.emit('error', new Error(message));
this._shutdown(this.ERRORS[type], message);
},
_parseOpcode: function(data) {
var rsvs = [this.RSV1, this.RSV2, this.RSV3].map(function(rsv) {
return (data & rsv) === rsv;
});
var frame = this._frame = new Frame();
frame.final = (data & this.FIN) === this.FIN;
frame.rsv1 = rsvs[0];
frame.rsv2 = rsvs[1];
frame.rsv3 = rsvs[2];
frame.opcode = (data & this.OPCODE);
if (!this._extensions.validFrameRsv(frame))
return this._fail('protocol_error',
'One or more reserved bits are on: reserved1 = ' + (frame.rsv1 ? 1 : 0) +
', reserved2 = ' + (frame.rsv2 ? 1 : 0) +
', reserved3 = ' + (frame.rsv3 ? 1 : 0));
if (this.OPCODE_CODES.indexOf(frame.opcode) < 0)
return this._fail('protocol_error', 'Unrecognized frame opcode: ' + frame.opcode);
if (this.MESSAGE_OPCODES.indexOf(frame.opcode) < 0 && !frame.final)
return this._fail('protocol_error', 'Received fragmented control frame: opcode = ' + frame.opcode);
if (this._message && this.OPENING_OPCODES.indexOf(frame.opcode) >= 0)
return this._fail('protocol_error', 'Received new data frame but previous continuous frame is unfinished');
this._stage = 1;
},
_parseLength: function(data) {
var frame = this._frame;
frame.masked = (data & this.MASK) === this.MASK;
if (this._requireMasking && !frame.masked)
return this._fail('unacceptable', 'Received unmasked frame but masking is required');
frame.length = (data & this.LENGTH);
if (frame.length >= 0 && frame.length <= 125) {
if (!this._checkFrameLength()) return;
this._stage = frame.masked ? 3 : 4;
} else {
frame.lengthBytes = (frame.length === 126 ? 2 : 8);
this._stage = 2;
}
},
_parseExtendedLength: function(buffer) {
var frame = this._frame;
frame.length = this._getInteger(buffer);
if (this.MESSAGE_OPCODES.indexOf(frame.opcode) < 0 && frame.length > 125)
return this._fail('protocol_error', 'Received control frame having too long payload: ' + frame.length);
if (!this._checkFrameLength()) return;
this._stage = frame.masked ? 3 : 4;
},
_checkFrameLength: function() {
var length = this._message ? this._message.length : 0;
if (length + this._frame.length > this._maxLength) {
this._fail('too_large', 'WebSocket frame length too large');
return false;
} else {
return true;
}
},
_emitFrame: function(buffer) {
var frame = this._frame,
payload = frame.payload = Hybi.mask(buffer, frame.maskingKey),
opcode = frame.opcode,
message,
code, reason,
callbacks, callback;
delete this._frame;
if (opcode === this.OPCODES.continuation) {
if (!this._message) return this._fail('protocol_error', 'Received unexpected continuation frame');
this._message.pushFrame(frame);
}
if (opcode === this.OPCODES.text || opcode === this.OPCODES.binary) {
this._message = new Message();
this._message.pushFrame(frame);
}
if (frame.final && this.MESSAGE_OPCODES.indexOf(opcode) >= 0)
return this._emitMessage(this._message);
if (opcode === this.OPCODES.close) {
code = (payload.length >= 2) ? 256 * payload[0] + payload[1] : null;
reason = (payload.length > 2) ? this._encode(payload.slice(2)) : null;
if (!(payload.length === 0) &&
!(code !== null && code >= this.MIN_RESERVED_ERROR && code <= this.MAX_RESERVED_ERROR) &&
this.ERROR_CODES.indexOf(code) < 0)
code = this.ERRORS.protocol_error;
if (payload.length > 125 || (payload.length > 2 && !reason))
code = this.ERRORS.protocol_error;
this._shutdown(code, reason || '');
}
if (opcode === this.OPCODES.ping) {
this.frame(payload, 'pong');
}
if (opcode === this.OPCODES.pong) {
callbacks = this._pingCallbacks;
message = this._encode(payload);
callback = callbacks[message];
delete callbacks[message];
if (callback) callback()
}
},
_emitMessage: function(message) {
var message = this._message;
message.read();
delete this._message;
this._extensions.processIncomingMessage(message, function(error, message) {
if (error) return this._fail('extension_error', error.message);
var payload = message.data;
if (message.opcode === this.OPCODES.text) payload = this._encode(payload);
if (payload === null)
return this._fail('encoding_error', 'Could not decode a text frame as UTF-8');
else
this.emit('message', new Base.MessageEvent(payload));
}, this);
},
_encode: function(buffer) {
try {
var string = buffer.toString('binary', 0, buffer.length);
if (!this.UTF8_MATCH.test(string)) return null;
} catch (e) {}
return buffer.toString('utf8', 0, buffer.length);
},
_getInteger: function(bytes) {
var number = 0;
for (var i = 0, n = bytes.length; i < n; i++)
number += bytes[i] * this.TWO_POWERS[n - 1 - i];
return number;
}
};
for (var key in instance)
Hybi.prototype[key] = instance[key];
module.exports = Hybi;
+21
View File
@@ -0,0 +1,21 @@
'use strict';
var Frame = function() {};
var instance = {
final: false,
rsv1: false,
rsv2: false,
rsv3: false,
opcode: null,
masked: false,
maskingKey: null,
lengthBytes: 1,
length: 0,
payload: null
};
for (var key in instance)
Frame.prototype[key] = instance[key];
module.exports = Frame;
+41
View File
@@ -0,0 +1,41 @@
'use strict';
var Message = function() {
this.rsv1 = false;
this.rsv2 = false;
this.rsv3 = false;
this.opcode = null
this.length = 0;
this._chunks = [];
};
var instance = {
read: function() {
if (this.data) return this.data;
this.data = new Buffer(this.length);
var offset = 0;
for (var i = 0, n = this._chunks.length; i < n; i++) {
this._chunks[i].copy(this.data, offset);
offset += this._chunks[i].length;
}
return this.data;
},
pushFrame: function(frame) {
this.rsv1 = this.rsv1 || frame.rsv1;
this.rsv2 = this.rsv2 || frame.rsv2;
this.rsv3 = this.rsv3 || frame.rsv3;
if (this.opcode === null) this.opcode = frame.opcode;
this._chunks.push(frame.payload);
this.length += frame.length;
}
};
for (var key in instance)
Message.prototype[key] = instance[key];
module.exports = Message;
@@ -0,0 +1,63 @@
'use strict';
var StreamReader = function() {
this._queue = [];
this._queueSize = 0;
};
StreamReader.prototype.put = function(buffer) {
if (!buffer || buffer.length === 0) return;
if (!buffer.copy) buffer = new Buffer(buffer);
this._queue.push(buffer);
this._queueSize += buffer.length;
};
StreamReader.prototype.read = function(length) {
if (length > this._queueSize) return null;
if (length === 0) return new Buffer(0);
var queue = this._queue,
first = queue[0],
buffer;
if (first.length >= length) {
this._queueSize -= length;
if (first.length === length) {
return queue.shift();
} else {
buffer = first.slice(0, length);
queue[0] = first.slice(length);
return buffer;
}
}
var remain = length, buffers;
for (var i=0, n = queue.length; i < n; i++) {
if (remain < queue[i].length) break;
remain -= queue[i].length;
}
buffers = queue.splice(0, i);
if (remain > 0 && queue.length > 0) {
buffers.push(queue[0].slice(0, remain));
queue[0] = queue[0].slice(remain);
}
this._queueSize -= length;
return this._concat(buffers, length);
};
StreamReader.prototype._concat = function(buffers, length) {
if (Buffer.concat) return Buffer.concat(buffers, length);
var buffer = new Buffer(length),
offset = 0;
for (var i = 0, n = buffers.length; i < n; i++) {
buffers[i].copy(buffer, offset);
offset += buffers[i].length;
}
return buffer;
};
module.exports = StreamReader;
+97
View File
@@ -0,0 +1,97 @@
'use strict';
var Stream = require('stream').Stream,
url = require('url'),
util = require('util'),
Headers = require('./headers'),
HttpParser = require('../http_parser');
var PORTS = {'ws:': 80, 'wss:': 443};
var Proxy = function(client, origin, options) {
this._client = client;
this._http = new HttpParser('response');
this._origin = (typeof client.url === 'object') ? client.url : url.parse(client.url);
this._url = (typeof origin === 'object') ? origin : url.parse(origin);
this._options = options || {};
this._state = 0;
this.readable = this.writable = true;
this._paused = false;
this._headers = new Headers();
this._headers.set('Host', this._origin.host);
this._headers.set('Connection', 'keep-alive');
this._headers.set('Proxy-Connection', 'keep-alive');
var auth = this._url.auth && new Buffer(this._url.auth, 'utf8').toString('base64');
if (auth) this._headers.set('Proxy-Authorization', 'Basic ' + auth);
};
util.inherits(Proxy, Stream);
var instance = {
setHeader: function(name, value) {
if (this._state !== 0) return false;
this._headers.set(name, value);
return true;
},
start: function() {
if (this._state !== 0) return false;
this._state = 1;
var origin = this._origin,
port = origin.port || PORTS[origin.protocol],
start = 'CONNECT ' + origin.hostname + ':' + port + ' HTTP/1.1';
var headers = [start, this._headers.toString(), ''];
this.emit('data', new Buffer(headers.join('\r\n'), 'utf8'));
return true;
},
pause: function() {
this._paused = true;
},
resume: function() {
this._paused = false;
this.emit('drain');
},
write: function(chunk) {
if (!this.writable) return false;
this._http.parse(chunk);
if (!this._http.isComplete()) return !this._paused;
this.statusCode = this._http.statusCode;
this.headers = this._http.headers;
if (this.statusCode === 200) {
this.emit('connect');
} else {
var message = "Can't establish a connection to the server at " + this._origin.href;
this.emit('error', new Error(message));
}
this.end();
return !this._paused;
},
end: function(chunk) {
if (!this.writable) return;
if (chunk !== undefined) this.write(chunk);
this.readable = this.writable = false;
this.emit('close');
this.emit('end');
},
destroy: function() {
this.end();
}
};
for (var key in instance)
Proxy.prototype[key] = instance[key];
module.exports = Proxy;
+108
View File
@@ -0,0 +1,108 @@
'use strict';
var util = require('util'),
HttpParser = require('../http_parser'),
Base = require('./base'),
Draft75 = require('./draft75'),
Draft76 = require('./draft76'),
Hybi = require('./hybi');
var Server = function(options) {
Base.call(this, null, null, options);
this._http = new HttpParser('request');
};
util.inherits(Server, Base);
var instance = {
EVENTS: ['open', 'message', 'error', 'close'],
_bindEventListeners: function() {
this.messages.on('error', function() {});
this.on('error', function() {});
},
parse: function(data) {
if (this._delegate) return this._delegate.parse(data);
this._http.parse(data);
if (!this._http.isComplete()) return;
this.method = this._http.method;
this.url = this._http.url;
this.headers = this._http.headers;
this.body = this._http.body;
var self = this;
this._delegate = Server.http(this, this._options);
this._delegate.messages = this.messages;
this._delegate.io = this.io;
this._open();
this.EVENTS.forEach(function(event) {
this._delegate.on(event, function(e) { self.emit(event, e) });
}, this);
this.protocol = this._delegate.protocol;
this.version = this._delegate.version;
this.parse(this._http.body);
this.emit('connect', new Base.ConnectEvent());
},
_open: function() {
this.__queue.forEach(function(msg) {
this._delegate[msg[0]].apply(this._delegate, msg[1]);
}, this);
this.__queue = [];
}
};
['addExtension', 'setHeader', 'start', 'frame', 'text', 'binary', 'ping', 'close'].forEach(function(method) {
instance[method] = function() {
if (this._delegate) {
return this._delegate[method].apply(this._delegate, arguments);
} else {
this.__queue.push([method, arguments]);
return true;
}
};
});
for (var key in instance)
Server.prototype[key] = instance[key];
Server.isSecureRequest = function(request) {
if (request.connection && request.connection.authorized !== undefined) return true;
if (request.socket && request.socket.secure) return true;
var headers = request.headers;
if (!headers) return false;
if (headers['https'] === 'on') return true;
if (headers['x-forwarded-ssl'] === 'on') return true;
if (headers['x-forwarded-scheme'] === 'https') return true;
if (headers['x-forwarded-proto'] === 'https') return true;
return false;
};
Server.determineUrl = function(request) {
var scheme = this.isSecureRequest(request) ? 'wss:' : 'ws:';
return scheme + '//' + request.headers.host + request.url;
};
Server.http = function(request, options) {
options = options || {};
if (options.requireMasking === undefined) options.requireMasking = true;
var headers = request.headers,
url = this.determineUrl(request);
if (headers['sec-websocket-version'])
return new Hybi(request, url, options);
else if (headers['sec-websocket-key1'])
return new Draft76(request, url, options);
else
return new Draft75(request, url, options);
};
module.exports = Server;
+100
View File
@@ -0,0 +1,100 @@
'use strict';
var NodeHTTPParser = process.binding('http_parser').HTTPParser,
version = NodeHTTPParser.RESPONSE ? 6 : 4;
var HttpParser = function(type) {
if (type === 'request')
this._parser = new NodeHTTPParser(NodeHTTPParser.REQUEST || 'request');
else
this._parser = new NodeHTTPParser(NodeHTTPParser.RESPONSE || 'response');
this._type = type;
this._complete = false;
this.headers = {};
var current = null,
self = this;
this._parser.onHeaderField = function(b, start, length) {
current = b.toString('utf8', start, start + length).toLowerCase();
};
this._parser.onHeaderValue = function(b, start, length) {
var value = b.toString('utf8', start, start + length);
if (self.headers.hasOwnProperty(current))
self.headers[current] += ', ' + value;
else
self.headers[current] = value;
};
this._parser.onHeadersComplete = this._parser[NodeHTTPParser.kOnHeadersComplete] =
function(majorVersion, minorVersion, headers, method, pathname, statusCode) {
var info = arguments[0];
if (typeof info === 'object') {
method = info.method;
pathname = info.url;
statusCode = info.statusCode;
headers = info.headers;
}
self.method = (typeof method === 'number') ? HttpParser.METHODS[method] : method;
self.statusCode = statusCode;
self.url = pathname;
if (!headers) return;
for (var i = 0, n = headers.length, key, value; i < n; i += 2) {
key = headers[i].toLowerCase();
value = headers[i+1];
if (self.headers.hasOwnProperty(key))
self.headers[key] += ', ' + value;
else
self.headers[key] = value;
}
self._complete = true;
};
};
HttpParser.METHODS = {
0: 'DELETE',
1: 'GET',
2: 'HEAD',
3: 'POST',
4: 'PUT',
5: 'CONNECT',
6: 'OPTIONS',
7: 'TRACE',
8: 'COPY',
9: 'LOCK',
10: 'MKCOL',
11: 'MOVE',
12: 'PROPFIND',
13: 'PROPPATCH',
14: 'SEARCH',
15: 'UNLOCK',
16: 'REPORT',
17: 'MKACTIVITY',
18: 'CHECKOUT',
19: 'MERGE',
24: 'PATCH'
};
HttpParser.prototype.isComplete = function() {
return this._complete;
};
HttpParser.prototype.parse = function(data) {
var offset = (version < 6) ? 1 : 0,
consumed = this._parser.execute(data, 0, data.length) + offset;
if (this._complete)
this.body = (consumed < data.length)
? data.slice(consumed)
: new Buffer(0);
};
module.exports = HttpParser;
+146
View File
@@ -0,0 +1,146 @@
'use strict';
/**
Streams in a WebSocket connection
---------------------------------
We model a WebSocket as two duplex streams: one stream is for the wire protocol
over an I/O socket, and the other is for incoming/outgoing messages.
+----------+ +---------+ +----------+
[1] write(chunk) -->| ~~~~~~~~ +----->| parse() +----->| ~~~~~~~~ +--> emit('data') [2]
| | +----+----+ | |
| | | | |
| IO | | [5] | Messages |
| | V | |
| | +---------+ | |
[4] emit('data') <--+ ~~~~~~~~ |<-----+ frame() |<-----+ ~~~~~~~~ |<-- write(chunk) [3]
+----------+ +---------+ +----------+
Message transfer in each direction is simple: IO receives a byte stream [1] and
sends this stream for parsing. The parser will periodically emit a complete
message text on the Messages stream [2]. Similarly, when messages are written
to the Messages stream [3], they are framed using the WebSocket wire format and
emitted via IO [4].
There is a feedback loop via [5] since some input from [1] will be things like
ping, pong and close frames. In these cases the protocol responds by emitting
responses directly back to [4] rather than emitting messages via [2].
For the purposes of flow control, we consider the sources of each Readable
stream to be as follows:
* [2] receives input from [1]
* [4] receives input from [1] and [3]
The classes below express the relationships described above without prescribing
anything about how parse() and frame() work, other than assuming they emit
'data' events to the IO and Messages streams. They will work with any protocol
driver having these two methods.
**/
var Stream = require('stream').Stream,
util = require('util');
var IO = function(driver) {
this.readable = this.writable = true;
this._paused = false;
this._driver = driver;
};
util.inherits(IO, Stream);
// The IO pause() and resume() methods will be called when the socket we are
// piping to gets backed up and drains. Since IO output [4] comes from IO input
// [1] and Messages input [3], we need to tell both of those to return false
// from write() when this stream is paused.
IO.prototype.pause = function() {
this._paused = true;
this._driver.messages._paused = true;
};
IO.prototype.resume = function() {
this._paused = false;
this.emit('drain');
var messages = this._driver.messages;
messages._paused = false;
messages.emit('drain');
};
// When we receive input from a socket, send it to the parser and tell the
// source whether to back off.
IO.prototype.write = function(chunk) {
if (!this.writable) return false;
this._driver.parse(chunk);
return !this._paused;
};
// The IO end() method will be called when the socket piping into it emits
// 'close' or 'end', i.e. the socket is closed. In this situation the Messages
// stream will not emit any more data so we emit 'end'.
IO.prototype.end = function(chunk) {
if (!this.writable) return;
if (chunk !== undefined) this.write(chunk);
this.writable = false;
var messages = this._driver.messages;
if (messages.readable) {
messages.readable = messages.writable = false;
messages.emit('end');
}
};
IO.prototype.destroy = function() {
this.end();
};
var Messages = function(driver) {
this.readable = this.writable = true;
this._paused = false;
this._driver = driver;
};
util.inherits(Messages, Stream);
// The Messages pause() and resume() methods will be called when the app that's
// processing the messages gets backed up and drains. If we're emitting
// messages too fast we should tell the source to slow down. Message output [2]
// comes from IO input [1].
Messages.prototype.pause = function() {
this._driver.io._paused = true;
};
Messages.prototype.resume = function() {
this._driver.io._paused = false;
this._driver.io.emit('drain');
};
// When we receive messages from the user, send them to the formatter and tell
// the source whether to back off.
Messages.prototype.write = function(message) {
if (!this.writable) return false;
if (typeof message === 'string') this._driver.text(message);
else this._driver.binary(message);
return !this._paused;
};
// The Messages end() method will be called when a stream piping into it emits
// 'end'. Many streams may be piped into the WebSocket and one of them ending
// does not mean the whole socket is done, so just process the input and move
// on leaving the socket open.
Messages.prototype.end = function(message) {
if (message !== undefined) this.write(message);
};
Messages.prototype.destroy = function() {};
exports.IO = IO;
exports.Messages = Messages;
+14 -17
View File
@@ -1,24 +1,21 @@
{ "name" : "faye-websocket"
, "description" : "Robust general-purpose WebSocket server and client"
, "homepage" : "http://github.com/jcoglan/faye-websocket-node"
{ "name" : "websocket-driver"
, "description" : "WebSocket protocol handler with pluggable I/O"
, "homepage" : "http://github.com/faye/websocket-driver-node"
, "author" : "James Coglan <jcoglan@gmail.com> (http://jcoglan.com/)"
, "keywords" : ["websocket"]
, "license" : "MIT"
, "version" : "0.1.2"
, "engines" : {"node": ">=0.4.0"}
, "main" : "./lib/faye/websocket"
, "devDependencies" : {"jsclass": ">=3.0.4"}
, "version" : "0.5.2"
, "engines" : {"node": ">=0.6.0"}
, "main" : "./lib/websocket/driver"
, "dependencies" : {"websocket-extensions": ">=0.1.1"}
, "devDependencies" : {"jstest": "", "permessage-deflate": ""}
, "bugs" : "http://github.com/jcoglan/faye-websocket-node/issues"
, "scripts" : {"test": "jstest spec/runner.js"}
, "licenses" : [ { "type" : "MIT"
, "url" : "http://www.opensource.org/licenses/mit-license.php"
}
]
, "repository" : { "type" : "git"
, "url" : "git://github.com/faye/websocket-driver-node.git"
}
, "repositories" : [ { "type" : "git"
, "url" : "git://github.com/jcoglan/faye-websocket-node.git"
}
]
, "bugs" : "http://github.com/faye/websocket-driver-node/issues"
}
-150
View File
@@ -1,150 +0,0 @@
var Client = require('../../../lib/faye/websocket/client')
JS.ENV.WebSocketSteps = JS.Test.asyncSteps({
server: function(port, secure, callback) {
this._adapter = new EchoServer()
this._adapter.listen(port, secure)
this._port = port
setTimeout(callback, 100)
},
stop: function(callback) {
this._adapter.stop()
setTimeout(callback, 100)
},
open_socket: function(url, callback) {
var done = false,
self = this,
resume = function(open) {
if (done) return
done = true
self._open = open
callback()
}
this._ws = new Client(url)
this._ws.onopen = function() { resume(true) }
this._ws.onclose = function() { resume(false) }
},
close_socket: function(callback) {
var self = this
this._ws.onclose = function() {
self._open = false
callback()
}
this._ws.close()
},
check_open: function(callback) {
this.assert( this._open )
callback()
},
check_closed: function(callback) {
this.assert( !this._open )
callback()
},
listen_for_message: function(callback) {
var self = this
this._ws.addEventListener('message', function(message) { self._message = message.data })
callback()
},
send_message: function(callback) {
this._ws.send("I expect this to be echoed")
setTimeout(callback, 100)
},
check_response: function(callback) {
this.assertEqual( "I expect this to be echoed", this._message )
callback()
},
check_no_response: function(callback) {
this.assert( !this._message )
callback()
}
})
JS.ENV.ClientSpec = JS.Test.describe("Client", function() { with(this) {
include(WebSocketSteps)
before(function() {
this.plain_text_url = "ws://localhost:8000/bayeux"
this.secure_url = "wss://localhost:8000/bayeux"
})
sharedBehavior("socket client", function() { with(this) {
it("can open a connection", function() { with(this) {
open_socket(socket_url)
check_open()
}})
it("cannot open a connection to the wrong host", function() { with(this) {
open_socket(blocked_url)
check_closed()
}})
it("can close the connection", function() { with(this) {
open_socket(socket_url)
close_socket()
check_closed()
}})
describe("in the OPEN state", function() { with(this) {
before(function() { with(this) {
open_socket(socket_url)
}})
it("can send and receive messages", function() { with(this) {
listen_for_message()
send_message()
check_response()
}})
}})
describe("in the CLOSED state", function() { with(this) {
before(function() { with(this) {
open_socket(socket_url)
close_socket()
}})
it("cannot send and receive messages", function() { with(this) {
listen_for_message()
send_message()
check_no_response()
}})
}})
}})
describe("with a plain-text server", function() { with(this) {
before(function() {
this.socket_url = this.plain_text_url
this.blocked_url = this.secure_url
})
before(function() { this.server(8000, false) })
after (function() { this.stop() })
behavesLike("socket client")
}})
describe("with a secure server", function() { with(this) {
before(function() {
this.socket_url = this.secure_url
this.blocked_url = this.plain_text_url
})
before(function() { this.server(8000, true) })
after (function() { this.stop() })
behavesLike("socket client")
}})
}})
-36
View File
@@ -1,36 +0,0 @@
var Draft75Parser = require('../../../lib/faye/websocket/draft75_parser')
JS.ENV.Draft75ParserSpec = JS.Test.describe("Draft75Parser", function() { with(this) {
before(function() { with(this) {
this.webSocket = {dispatchEvent: function() {}}
this.parser = new Draft75Parser(webSocket)
}})
describe("parse", function() { with(this) {
it("parses text frames", function() { with(this) {
expect(webSocket, "receive").given("Hello")
parser.parse([0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff])
}})
it("parses multibyte text frames", function() { with(this) {
expect(webSocket, "receive").given("Apple = ")
parser.parse([0x00, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff])
}})
it("parses fragmented frames", function() { with(this) {
expect(webSocket, "receive").given("Hello")
parser.parse([0x00, 0x48, 0x65, 0x6c])
parser.parse([0x6c, 0x6f, 0xff])
}})
}})
describe("frame", function() { with(this) {
it("returns the given string formatted as a WebSocket frame", function() { with(this) {
assertBufferEqual( [0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff], parser.frame("Hello") )
}})
it("encodes multibyte characters correctly", function() { with(this) {
assertBufferEqual( [0x00, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff], parser.frame("Apple = ") )
}})
}})
}})
-146
View File
@@ -1,146 +0,0 @@
var Protocol8Parser = require('../../../lib/faye/websocket/protocol8_parser')
JS.ENV.Protocol8ParserSpec = JS.Test.describe("Protocol8Parser", function() { with(this) {
before(function() { with(this) {
this.webSocket = {dispatchEvent: function() {}}
this.parser = new Protocol8Parser(webSocket)
}})
define("parse", function() {
var bytes = [];
for (var i = 0, n = arguments.length; i < n; i++) bytes = bytes.concat(arguments[i])
this.parser.parse(new Buffer(bytes))
})
define("buffer", function(string) {
return {
equals: function(buffer) {
return buffer.toString('utf8', 0, buffer.length) === string
}
}
})
describe("parse", function() { with(this) {
define("mask", function() {
return this._mask = this._mask || [1,2,3,4].map(function() { return Math.floor(Math.random() * 255) })
})
define("maskMessage", function(bytes) {
var output = []
Array.prototype.forEach.call(bytes, function(b, i) {
output[i] = bytes[i] ^ this.mask()[i % 4]
}, this)
return output
})
it("parses unmasked text frames", function() { with(this) {
expect(webSocket, "receive").given("Hello")
parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
}})
it("parses empty text frames", function() { with(this) {
expect(webSocket, "receive").given("")
parse([0x81, 0x00])
}})
it("parses fragmented text frames", function() { with(this) {
expect(webSocket, "receive").given("Hello")
parse([0x01, 0x03, 0x48, 0x65, 0x6c])
parse([0x80, 0x02, 0x6c, 0x6f])
}})
it("parses masked text frames", function() { with(this) {
expect(webSocket, "receive").given("Hello")
parse([0x81, 0x85], mask(), maskMessage([0x48, 0x65, 0x6c, 0x6c, 0x6f]))
}})
it("parses masked empty text frames", function() { with(this) {
expect(webSocket, "receive").given("")
parse([0x81, 0x80], mask(), maskMessage([]))
}})
it("parses masked fragmented text frames", function() { with(this) {
expect(webSocket, "receive").given("Hello")
parse([0x01, 0x81], mask(), maskMessage([0x48]))
parse([0x80, 0x84], mask(), maskMessage([0x65, 0x6c, 0x6c, 0x6f]))
}})
it("closes the socket if the frame has an unrecognized opcode", function() { with(this) {
expect(webSocket, "close").given(1002, null, false)
parse([0x83, 0x00])
}})
it("closes the socket if a close frame is received", function() { with(this) {
expect(webSocket, "close").given(1000, "Hello", false)
parse([0x88, 0x07, 0x03, 0xe8, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
}})
it("parses unmasked multibyte text frames", function() { with(this) {
expect(webSocket, "receive").given("Apple = ")
parse([0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf])
}})
it("parses fragmented multibyte text frames", function() { with(this) {
expect(webSocket, "receive").given("Apple = ")
parse([0x01, 0x0a, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3])
parse([0x80, 0x01, 0xbf])
}})
it("parses masked multibyte text frames", function() { with(this) {
expect(webSocket, "receive").given("Apple = ")
parse([0x81, 0x8b], mask(), maskMessage([0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf]))
}})
it("parses masked fragmented multibyte text frames", function() { with(this) {
expect(webSocket, "receive").given("Apple = ")
parse([0x01, 0x8a], mask(), maskMessage([0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3]))
parse([0x80, 0x81], mask(), maskMessage([0xbf]))
}})
it("parses unmasked medium-length text frames", function() { with(this) {
expect(webSocket, "receive").given("HelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHello")
parse([129, 126, 0, 200, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111])
}})
it("parses masked medium-length text frames", function() { with(this) {
expect(webSocket, "receive").given("HelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHello")
parse([129, 254, 0, 200], mask(), maskMessage([72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111]))
}})
it("replies to pings with a pong", function() { with(this) {
expect(webSocket, "send").given(buffer("OHAI"), "pong")
parse([0x89, 0x04, 0x4f, 0x48, 0x41, 0x49])
}})
}})
describe("frame", function() { with(this) {
it("returns the given string formatted as a WebSocket frame", function() { with(this) {
assertBufferEqual( [0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f], parser.frame("Hello") )
}})
it("encodes multibyte characters correctly", function() { with(this) {
assertBufferEqual( [0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf], parser.frame("Apple = ") )
}})
it("encodes medium-length strings using extra length bytes", function() { with(this) {
assertBufferEqual( [129, 126, 0, 200, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111], parser.frame("HelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHello") )
}})
it("encodes long strings using extra length bytes", function() { with(this) {
var reps = 13108, message = '', output = [0x81, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x04]
while (reps--) {
message += "Hello"
output = output.concat([0x48, 0x65, 0x6c, 0x6c, 0x6f])
}
assertBufferEqual( output, parser.frame(message) )
}})
it("encodes close frames with an error code", function() { with(this) {
assertBufferEqual( [0x88, 0x07, 0x03, 0xea, 0x48, 0x65, 0x6c, 0x6c, 0x6f], parser.frame("Hello", "close", 1002) )
}})
it("encodes pong frames", function() { with(this) {
assertBufferEqual( [0x8a, 0x00], parser.frame("", "pong") )
}})
}})
}})
+37 -46
View File
@@ -1,53 +1,44 @@
require('jsclass')
var test = require('jstest').Test,
Stream = require('stream').Stream,
util = require('util')
var WebSocket = require('../lib/faye/websocket'),
fs = require('fs'),
http = require('http'),
https = require('https')
JS.ENV.EchoServer = function() {}
EchoServer.prototype.listen = function(port, ssl) {
var server = ssl
? https.createServer({
key: fs.readFileSync(__dirname + '/server.key'),
cert: fs.readFileSync(__dirname + '/server.crt')
})
: http.createServer()
server.addListener('upgrade', function(request, socket, head) {
var ws = new WebSocket(request, socket, head)
ws.onmessage = function(event) {
ws.send(event.data)
}
})
this._httpServer = server
server.listen(port)
var BufferMatcher = function(data) {
this._data = (typeof data === 'string')
? new Buffer(data, 'utf8')
: new Buffer(data)
}
EchoServer.prototype.stop = function(callback, scope) {
this._httpServer.addListener('close', function() {
if (callback) callback.call(scope);
});
this._httpServer.close();
BufferMatcher.prototype.equals = function(other) {
if (this._data.length !== other.length) return false;
for (var i = 0, n = other.length; i < n; i++) {
if (other[i] !== this._data[i]) return false;
}
return true;
}
var Collector = function() {
this.bytes = []
this.writable = true
}
util.inherits(Collector, Stream)
Collector.prototype.write = function(buffer) {
this.bytes = []
for (var i = 0, n = buffer.length; i < n; i++) {
this.bytes[i] = buffer[i]
}
return true
}
JS.Packages(function() { with(this) {
autoload(/.*Spec/, {from: 'spec/faye/websocket'})
}})
JS.require('JS.Test', function() {
JS.Test.Unit.Assertions.define("assertBufferEqual", function(array, buffer) {
this.assertEqual(array.length, buffer.length);
var ary = [], n = buffer.length;
while (n--) ary[n] = buffer[n];
this.assertEqual(array, ary);
})
JS.require( 'ClientSpec',
'Draft75ParserSpec',
'Protocol8ParserSpec',
JS.Test.method('autorun'))
test.Unit.TestCase.include({
buffer: function(data) {
return new BufferMatcher(data)
},
collector: function() {
return this._collector = this._collector || new Collector()
}
})
require('./websocket/driver/draft75_examples')
require('./websocket/driver/draft75_spec')
require('./websocket/driver/draft76_spec')
require('./websocket/driver/hybi_spec')
require('./websocket/driver/client_spec')
-15
View File
@@ -1,15 +0,0 @@
-----BEGIN CERTIFICATE-----
MIICZTCCAc4CCQDxyrJZrFA0vjANBgkqhkiG9w0BAQUFADB3MQswCQYDVQQGEwJV
SzEPMA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xDTALBgNVBAoTBEZh
eWUxFTATBgNVBAMTDEphbWVzIENvZ2xhbjEgMB4GCSqGSIb3DQEJARYRamNvZ2xh
bkBnbWFpbC5jb20wHhcNMTEwODMwMTIzOTM2WhcNMTIwODI5MTIzOTM2WjB3MQsw
CQYDVQQGEwJVSzEPMA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xDTAL
BgNVBAoTBEZheWUxFTATBgNVBAMTDEphbWVzIENvZ2xhbjEgMB4GCSqGSIb3DQEJ
ARYRamNvZ2xhbkBnbWFpbC5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGB
AMDjU5fAK7fvUCZIYHcGXDZD/m9bY+B/UcwGcowk0hMQGYNlLKrpiK7xXBmZpDb6
r8n+7L/epBeSumbRIm4TDzeNHhuQGYLIeGQy7JNLoPBr6GxubjuJhKOOBnCqcupR
CLGG7Zw5oL4UvtZVH6kL9XnjyokQQbxxeoV9DqtqOaHHAgMBAAEwDQYJKoZIhvcN
AQEFBQADgYEAvQjSpzE1ahaeH1CmbLwckTxvWMZfxcZOrxTruK1po3cNnDOjGqFQ
KEkNj3K5WfwTBD4QgUdYDykhDX2m6HaMz4JEbgrwQv8M8FiswIA3dyGsbOifOk8H
r3GPNKMzm4o6vrn6RGOpt9q6bsWUBUHfNpP93uU2C9QEwDua3cFjDA0=
-----END CERTIFICATE-----
-15
View File
@@ -1,15 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQDA41OXwCu371AmSGB3Blw2Q/5vW2Pgf1HMBnKMJNITEBmDZSyq
6Yiu8VwZmaQ2+q/J/uy/3qQXkrpm0SJuEw83jR4bkBmCyHhkMuyTS6Dwa+hsbm47
iYSjjgZwqnLqUQixhu2cOaC+FL7WVR+pC/V548qJEEG8cXqFfQ6rajmhxwIDAQAB
AoGABlk1DiCQD8y7mZb2PdSiwlJ4lFewsNnf6lQn/v7TPzdfb5ir4LAxBHkDLACH
jBuyH3bZefMs+W2l3u5xMKhF7uJqYcUlJdH2UwRfNG54Hn4SGAjQOK3ONer99sUf
USlsWSX1HjAAFMCBwUfKxMZA3VNQfYKTPdm0jSVf85kHO1ECQQD3s6ksm3QpfD0L
eG9EoDrqmwnEfpKoWPpz1O0i5tY9VcmhmLwS5Zpd7lB1qjTqzZk4RygU73T/BseJ
azehIHK5AkEAx1mSXt+ec8RfzVi/io6oqi2vOcACXRbOG4NQmqUWPnumdwsJjsjR
RzEoDFC2lu6448p9sgEq+CkbmgVeiyp4fwJAQnmgySve/NMuvslPcyddKGD7OhSN
30ghzrwx98/jZwqC1i9bKeccimDOjwVitjD/Ea9m/ldVGqwDGMoBX+iJYQJAEIOO
CYfyw1pQKV2huGOq+zX/nwQV7go2lrbhFX55gkGR/6iNaSOfmosq6yJAje5GqLAc
i4NnQNl+7NpnA5ZIFwJBAI1+OsZyjbRI99pYkTdOpa5IPlIb3j3JbSfjAWHLxlRY
0HLvN3Q1mE9kbB+uKH6syF/S7nALgsLgq7eHYvIaE/A=
-----END RSA PRIVATE KEY-----
+323
View File
@@ -0,0 +1,323 @@
var Client = require("../../../lib/websocket/driver/client"),
test = require('jstest').Test
test.describe("Client", function() { with(this) {
define("options", function() {
return this._options = this._options || {protocols: this.protocols()}
})
define("protocols", function() {
null
})
define("url", function() {
return "ws://www.example.com/socket"
})
define("driver", function() {
if (this._driver) return this._driver
this._driver = new Client(this.url(), this.options())
var self = this
this._driver.on('open', function(e) { self.open = true })
this._driver.on('message', function(e) { self.message += e.data })
this._driver.on('error', function(e) { self.error = e })
this._driver.on('close', function(e) { self.close = [e.code, e.reason] })
var collector = this.collector()
this._driver.io.on("data", function(d) { collector.write(d) })
return this._driver
})
define("key", function() {
return "2vBVWg4Qyk3ZoM/5d3QD9Q=="
})
define("response", function() {
return "HTTP/1.1 101 Switching Protocols\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Accept: QV3I5XUXU2CdhtjixE7QCkCcMZM=\r\n" +
"\r\n"
})
before(function() {
this.stub(Client, "generateKey").returns(this.key())
this.open = this.error = this.close = false
this.message = ""
})
describe("in the beginning state", function() { with(this) {
it("starts in no state", function() { with(this) {
assertEqual( null, driver().getState() )
}})
describe("close", function() { with(this) {
it("changes the state to closed", function() { with(this) {
driver().close()
assertEqual( "closed", driver().getState() )
assertEqual( [1000, ''], close )
}})
}})
describe("start", function() { with(this) {
it("writes the handshake request to the socket", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
"GET /socket HTTP/1.1\r\n" +
"Host: www.example.com\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Key: 2vBVWg4Qyk3ZoM/5d3QD9Q==\r\n" +
"Sec-WebSocket-Version: 13\r\n" +
"\r\n"))
driver().start()
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().start() )
}})
describe("with subprotocols", function() { with(this) {
define("protocols", function() { return ["foo", "bar", "xmpp"] })
it("writes the handshake with Sec-WebSocket-Protocol", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
"GET /socket HTTP/1.1\r\n" +
"Host: www.example.com\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Key: 2vBVWg4Qyk3ZoM/5d3QD9Q==\r\n" +
"Sec-WebSocket-Version: 13\r\n" +
"Sec-WebSocket-Protocol: foo, bar, xmpp\r\n" +
"\r\n"))
driver().start()
}})
}})
describe("with basic auth", function() { with(this) {
define("url", function() { return "ws://user:pass@www.example.com/socket" })
it("writes the handshake with Authorization", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
"GET /socket HTTP/1.1\r\n" +
"Host: www.example.com\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Key: 2vBVWg4Qyk3ZoM/5d3QD9Q==\r\n" +
"Sec-WebSocket-Version: 13\r\n" +
"Authorization: Basic dXNlcjpwYXNz\r\n" +
"\r\n"))
driver().start()
}})
}})
describe("with custom headers", function() { with(this) {
before(function() { with(this) {
driver().setHeader("User-Agent", "Chrome")
}})
it("writes the handshake with custom headers", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
"GET /socket HTTP/1.1\r\n" +
"Host: www.example.com\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Key: 2vBVWg4Qyk3ZoM/5d3QD9Q==\r\n" +
"Sec-WebSocket-Version: 13\r\n" +
"User-Agent: Chrome\r\n" +
"\r\n"))
driver().start()
}})
}})
it("changes the state to connecting", function() { with(this) {
driver().start()
assertEqual( "connecting", driver().getState() )
}})
}})
}})
describe("using a proxy", function() { with(this) {
it("sends a CONNECT request", function() { with(this) {
var proxy = driver().proxy("http://proxy.example.com")
expect(proxy, "emit").given("data", buffer(
"CONNECT www.example.com:80 HTTP/1.1\r\n" +
"Host: www.example.com\r\n" +
"Connection: keep-alive\r\n" +
"Proxy-Connection: keep-alive\r\n" +
"\r\n"))
proxy.start()
}})
it("sends an authenticated CONNECT request", function() { with(this) {
var proxy = driver().proxy("http://user:pass@proxy.example.com")
expect(proxy, "emit").given("data", buffer(
"CONNECT www.example.com:80 HTTP/1.1\r\n" +
"Host: www.example.com\r\n" +
"Connection: keep-alive\r\n" +
"Proxy-Connection: keep-alive\r\n" +
"Proxy-Authorization: Basic dXNlcjpwYXNz\r\n" +
"\r\n"))
proxy.start()
}})
it("sends a CONNECT request with custom headers", function() { with(this) {
var proxy = driver().proxy("http://user:pass@proxy.example.com")
proxy.setHeader("User-Agent", "Chrome")
expect(proxy, "emit").given("data", buffer(
"CONNECT www.example.com:80 HTTP/1.1\r\n" +
"Host: www.example.com\r\n" +
"Connection: keep-alive\r\n" +
"Proxy-Connection: keep-alive\r\n" +
"Proxy-Authorization: Basic dXNlcjpwYXNz\r\n" +
"User-Agent: Chrome\r\n" +
"\r\n"))
proxy.start()
}})
describe("receiving a response", function() { with(this) {
before(function() { with(this) {
this.proxy = driver().proxy("http://proxy.example.com")
}})
it("returns true when the response is written", function() { with(this) {
// this prevents downstream connections suddenly closing for no reason
assertEqual( true, proxy.write(new Buffer("HTTP/1.1 200 OK\r\n\r\n")) )
}})
it("emits a 'connect' event when the proxy connects", function() { with(this) {
expect(proxy, "emit").given("connect")
expect(proxy, "emit").given("close")
expect(proxy, "emit").given("end")
proxy.write(new Buffer("HTTP/1.1 200 OK\r\n\r\n"))
}})
it("emits an 'error' event if the proxy does not connect", function() { with(this) {
expect(proxy, "emit").given("error", objectIncluding({message: "Can't establish a connection to the server at ws://www.example.com/socket"}))
expect(proxy, "emit").given("close")
expect(proxy, "emit").given("end")
proxy.write(new Buffer("HTTP/1.1 403 Forbidden\r\n\r\n"))
}})
}})
}})
describe("in the connecting state", function() { with(this) {
before(function() { this.driver().start() })
describe("with a valid response", function() { with(this) {
before(function() { this.driver().parse(new Buffer(this.response())) })
it("changes the state to open", function() { with(this) {
assertEqual( true, open )
assertEqual( false, close )
assertEqual( "open", driver().getState() )
}})
it("makes the response status available", function() { with(this) {
assertEqual( 101, driver().statusCode )
}})
it("makes the response headers available", function() { with(this) {
assertEqual( "websocket", driver().headers.upgrade )
}})
}})
describe("with a valid response followed by a frame", function() { with(this) {
before(function() { with(this) {
var resp = new Buffer(response().length + 4)
new Buffer(response()).copy(resp)
new Buffer([0x81, 0x02, 72, 105]).copy(resp, resp.length - 4)
driver().parse(resp)
}})
it("changes the state to open", function() { with(this) {
assertEqual( true, open )
assertEqual( false, close )
assertEqual( "open", driver().getState() )
}})
it("parses the frame", function() { with(this) {
assertEqual( "Hi", message )
}})
}})
describe("with a bad status line", function() { with(this) {
before(function() {
var resp = this.response().replace(/101/g, "4")
this.driver().parse(new Buffer(resp))
})
it("changes the state to closed", function() { with(this) {
assertEqual( false, open )
assertEqual( "Error during WebSocket handshake: Unexpected response code: 4", error.message )
assertEqual( [1002, "Error during WebSocket handshake: Unexpected response code: 4"], close )
assertEqual( "closed", driver().getState() )
}})
}})
describe("with a bad Upgrade header", function() { with(this) {
before(function() {
var resp = this.response().replace(/websocket/g, "wrong")
this.driver().parse(new Buffer(resp))
})
it("changes the state to closed", function() { with(this) {
assertEqual( false, open )
assertEqual( "Error during WebSocket handshake: 'Upgrade' header value is not 'WebSocket'", error.message )
assertEqual( [1002, "Error during WebSocket handshake: 'Upgrade' header value is not 'WebSocket'"], close )
assertEqual( "closed", driver().getState() )
}})
}})
describe("with a bad Accept header", function() { with(this) {
before(function() {
var resp = this.response().replace(/QV3/g, "wrong")
this.driver().parse(new Buffer(resp))
})
it("changes the state to closed", function() { with(this) {
assertEqual( false, open )
assertEqual( "Error during WebSocket handshake: Sec-WebSocket-Accept mismatch", error.message )
assertEqual( [1002, "Error during WebSocket handshake: Sec-WebSocket-Accept mismatch"], close )
assertEqual( "closed", driver().getState() )
}})
}})
describe("with valid subprotocols", function() { with(this) {
define("protocols", function() { return ["foo", "xmpp"] })
before(function() {
var resp = this.response().replace(/\r\n\r\n/, "\r\nSec-WebSocket-Protocol: xmpp\r\n\r\n")
this.driver().parse(new Buffer(resp))
})
it("changs the state to open", function() { with(this) {
assertEqual( true, open )
assertEqual( false, close )
assertEqual( "open", driver().getState() )
}})
it("selects the subprotocol", function() { with(this) {
assertEqual( "xmpp", driver().protocol )
}})
}})
describe("with invalid subprotocols", function() { with(this) {
define("protocols", function() { return ["foo", "xmpp"] })
before(function() {
var resp = this.response().replace(/\r\n\r\n/, "\r\nSec-WebSocket-Protocol: irc\r\n\r\n")
this.driver().parse(new Buffer(resp))
})
it("changs the state to closed", function() { with(this) {
assertEqual( false, open )
assertEqual( "Error during WebSocket handshake: Sec-WebSocket-Protocol mismatch", error.message )
assertEqual( [1002, "Error during WebSocket handshake: Sec-WebSocket-Protocol mismatch"], close )
assertEqual( "closed", driver().getState() )
}})
it("selects no subprotocol", function() { with(this) {
assertEqual( null, driver().protocol )
}})
}})
}})
}})
+114
View File
@@ -0,0 +1,114 @@
var test = require('jstest').Test
test.describe("draft-75", function() { with(this) {
sharedExamplesFor("draft-75 protocol", function() { with(this) {
describe("in the open state", function() { with(this) {
before(function() { this.driver().start() })
describe("parse", function() { with(this) {
it("parses text frames", function() { with(this) {
driver().parse([0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff])
assertEqual( "Hello", message )
}})
it("parses multiple frames from the same packet", function() { with(this) {
driver().parse([0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff])
assertEqual( "HelloHello", message )
}})
it("parses text frames beginning 0x00-0x7F", function() { with(this) {
driver().parse([0x66, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff])
assertEqual( "Hello", message )
}})
it("ignores frames with a length header", function() { with(this) {
driver().parse([0x80, 0x02, 0x48, 0x65, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff])
assertEqual( "Hello", message )
}})
it("parses multibyte text frames", function() { with(this) {
driver().parse([0x00, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff])
assertEqual( "Apple = ", message )
}})
it("parses frames received in several packets", function() { with(this) {
driver().parse([0x00, 0x41, 0x70, 0x70, 0x6c, 0x65])
driver().parse([0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff])
assertEqual( "Apple = ", message )
}})
it("parses fragmented frames", function() { with(this) {
driver().parse([0x00, 0x48, 0x65, 0x6c])
driver().parse([0x6c, 0x6f, 0xff])
assertEqual( "Hello", message )
}})
}})
describe("frame", function() { with(this) {
it("formats the given string as a WebSocket frame", function() { with(this) {
driver().frame("Hello")
assertEqual( [0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff], collector().bytes )
}})
it("encodes multibyte characters correctly", function() { with(this) {
driver().frame("Apple = ")
assertEqual( [0x00, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff], collector().bytes )
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().frame("lol") )
}})
}})
describe("ping", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().ping()
}})
it("returns false", function() { with(this) {
assertEqual( false, driver().ping() )
}})
}})
describe("close", function() { with(this) {
it("triggers the onclose event", function() { with(this) {
driver().close()
assertEqual( true, close )
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().close() )
}})
it("changes the state to closed", function() { with(this) {
driver().close()
assertEqual( "closed", driver().getState() )
}})
}})
}})
describe("in the closed state", function() { with(this) {
before(function() {
this.driver().start()
this.driver().close()
})
describe("close", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().close()
}})
it("returns false", function() { with(this) {
assertEqual( false, driver().close() )
}})
it("leaves the protocol in the closed state", function() { with(this) {
driver().close()
assertEqual( "closed", driver().getState() )
}})
}})
}})
}})
}})
+99
View File
@@ -0,0 +1,99 @@
var Draft75 = require("../../../lib/websocket/driver/draft75"),
test = require('jstest').Test
test.describe("Draft75", function() { with(this) {
define("request", function() {
return this._request = this._request || {
headers: {
"connection": "Upgrade",
"upgrade": "WebSocket",
"origin": "http://www.example.com"
}
}
})
define("options", function() {
return this._options = this._options || {masking: false}
})
define("driver", function() {
if (this._driver) return this._driver
this._driver = new Draft75(this.request(), "ws://www.example.com/socket", this.options())
var self = this
this._driver.on('open', function(e) { self.open = true })
this._driver.on('message', function(e) { self.message += e.data })
this._driver.on('close', function(e) { self.close = true })
this._driver.io.pipe(this.collector())
return this._driver
})
before(function() {
this.open = this.close = false
this.message = ""
})
describe("in the connecting state", function() { with(this) {
it("starts in the connecting state", function() { with(this) {
assertEqual( "connecting", driver().getState() )
}})
describe("start", function() { with(this) {
it("writes the handshake response to the socket", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
"HTTP/1.1 101 Web Socket Protocol Handshake\r\n" +
"Upgrade: WebSocket\r\n" +
"Connection: Upgrade\r\n" +
"WebSocket-Origin: http://www.example.com\r\n" +
"WebSocket-Location: ws://www.example.com/socket\r\n" +
"\r\n"))
driver().start()
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().start() )
}})
it("triggers the onopen event", function() { with(this) {
driver().start()
assertEqual( true, open )
}})
it("changes the state to open", function() { with(this) {
driver().start()
assertEqual( "open", driver().getState() )
}})
it("sets the protocol version", function() { with(this) {
driver().start()
assertEqual( "hixie-75", driver().version )
}})
}})
describe("frame", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().frame("Hello, world")
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().frame("whatever") )
}})
it("queues the frames until the handshake has been sent", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
"HTTP/1.1 101 Web Socket Protocol Handshake\r\n" +
"Upgrade: WebSocket\r\n" +
"Connection: Upgrade\r\n" +
"WebSocket-Origin: http://www.example.com\r\n" +
"WebSocket-Location: ws://www.example.com/socket\r\n" +
"\r\n"))
expect(driver().io, "emit").given("data", buffer([0x00, 0x48, 0x69, 0xff]))
driver().frame("Hi")
driver().start()
}})
}})
}})
itShouldBehaveLike("draft-75 protocol")
}})
+186
View File
@@ -0,0 +1,186 @@
var Draft76 = require("../../../lib/websocket/driver/draft76"),
test = require('jstest').Test
test.describe("Draft76", function() { with(this) {
BODY = new Buffer([0x91, 0x25, 0x3e, 0xd3, 0xa9, 0xe7, 0x6a, 0x88])
define("body", function() {
return BODY
})
define("response", function() {
return [0xb4, 0x9c, 0x6e, 0x40, 0x53, 0x04, 0x04, 0x26, 0xe5, 0x1b, 0xbf, 0x6c, 0xb7, 0x9f, 0x1d, 0xf9]
})
define("request", function() {
return this._request = this._request || {
headers: {
"connection": "Upgrade",
"upgrade": "WebSocket",
"origin": "http://www.example.com",
"sec-websocket-key1": "1 38 wZ3f9 23O0 3l 0r",
"sec-websocket-key2": "27 0E 6 2 1665:< ;U 1H"
}
}
})
define("options", function() {
return this._options = this._options || {masking: false}
})
define("driver", function() {
if (this._driver) return this._driver
this._driver = new Draft76(this.request(), "ws://www.example.com/socket", this.options())
var self = this
this._driver.on('open', function(e) { self.open = true })
this._driver.on('message', function(e) { self.message += e.data })
this._driver.on('close', function(e) { self.close = true })
this._driver.io.pipe(this.collector())
this._driver.io.write(this.body())
return this._driver
})
before(function() {
this.open = this.close = false
this.message = ""
})
describe("in the connecting state", function() { with(this) {
it("starts in the connecting state", function() { with(this) {
assertEqual( "connecting", driver().getState() )
}})
describe("start", function() { with(this) {
it("writes the handshake response to the socket", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
"HTTP/1.1 101 WebSocket Protocol Handshake\r\n" +
"Upgrade: WebSocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Origin: http://www.example.com\r\n" +
"Sec-WebSocket-Location: ws://www.example.com/socket\r\n" +
"\r\n"))
expect(driver().io, "emit").given("data", buffer(response()))
driver().start()
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().start() )
}})
it("triggers the onopen event", function() { with(this) {
driver().start()
assertEqual( true, open )
}})
it("changes the state to open", function() { with(this) {
driver().start()
assertEqual( "open", driver().getState() )
}})
it("sets the protocol version", function() { with(this) {
driver().start()
assertEqual( "hixie-76", driver().version )
}})
}})
describe("frame", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().frame("Hello, world")
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().frame("whatever") )
}})
it("queues the frames until the handshake has been sent", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
"HTTP/1.1 101 WebSocket Protocol Handshake\r\n" +
"Upgrade: WebSocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Origin: http://www.example.com\r\n" +
"Sec-WebSocket-Location: ws://www.example.com/socket\r\n" +
"\r\n"))
expect(driver().io, "emit").given("data", buffer(response()))
expect(driver().io, "emit").given("data", buffer([0x00, 72, 105, 0xff]))
driver().frame("Hi")
driver().start()
}})
}})
describe("with no request body", function() { with(this) {
define("body", function() {
return new Buffer([])
})
it("writes the handshake response with no body", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
"HTTP/1.1 101 WebSocket Protocol Handshake\r\n" +
"Upgrade: WebSocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Origin: http://www.example.com\r\n" +
"Sec-WebSocket-Location: ws://www.example.com/socket\r\n" +
"\r\n"))
driver().start()
}})
it("does not trigger the onopen event", function() { with(this) {
driver().start()
assertEqual( false, open )
}})
it("leaves the protocol in the connecting state", function() { with(this) {
driver().start()
assertEqual( "connecting", driver().getState() )
}})
describe("when the request body is received", function() { with(this) {
before(function() { this.driver().start() })
it("sends the response body", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(response()))
driver().parse(BODY)
}})
it("triggers the onopen event", function() { with(this) {
driver().parse(BODY)
assertEqual( true, open )
}})
it("changes the state to open", function() { with(this) {
driver().parse(BODY)
assertEqual( "open", driver().getState() )
}})
it("sends any frames queued before the handshake was complete", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(response()))
expect(driver().io, "emit").given("data", buffer([0x00, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0xff]))
driver().frame("hello")
driver().parse(BODY)
}})
}})
}})
}})
itShouldBehaveLike("draft-75 protocol")
describe("in the open state", function() { with(this) {
before(function() { this.driver().start() })
describe("parse", function() { with(this) {
it("closes the socket if a close frame is received", function() { with(this) {
driver().parse([0xff, 0x00])
assertEqual( true, close )
assertEqual( "closed", driver().getState() )
}})
}})
describe("close", function() { with(this) {
it("writes a close message to the socket", function() { with(this) {
expect(driver().io, "emit").given("data", buffer([0xff, 0x00]))
driver().close()
}})
}})
}})
}})
+541
View File
@@ -0,0 +1,541 @@
var Hybi = require("../../../lib/websocket/driver/hybi"),
test = require('jstest').Test
test.describe("Hybi", function() { with(this) {
define("request", function() {
return this._request = this._request || {
headers: {
"connection": "Upgrade",
"upgrade": "websocket",
"origin": "http://www.example.com",
// "sec-websocket-extensions": "x-webkit-deflate-frame",
"sec-websocket-key": "JFBCWHksyIpXV+6Wlq/9pw==",
"sec-websocket-version": "13"
}
}
})
define("options", function() {
return this._options = this._options || {masking: false}
})
define("driver", function() {
if (this._driver) return this._driver
this._driver = new Hybi(this.request(), "ws://www.example.com/socket", this.options())
var self = this
this._driver.on('open', function(e) { self.open = true })
this._driver.on('message', function(e) { self.message += e.data })
this._driver.on('error', function(e) { self.error = e })
this._driver.on('close', function(e) { self.close = [e.code, e.reason] })
this._driver.io.pipe(this.collector())
return this._driver
})
before(function() {
this.open = this.error = this.close = false
this.message = ""
})
describe("in the connecting state", function() { with(this) {
it("starts in the connecting state", function() { with(this) {
assertEqual( "connecting", driver().getState() )
}})
describe("start", function() { with(this) {
it("writes the handshake response to the socket", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
"HTTP/1.1 101 Switching Protocols\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Accept: JdiiuafpBKRqD7eol0y4vJDTsTs=\r\n" +
"\r\n"))
driver().start()
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().start() )
}})
describe("with subprotocols", function() { with(this) {
before(function() { with(this) {
request().headers["sec-websocket-protocol"] = "foo, bar, xmpp"
options().protocols = ["xmpp"]
}})
it("writes the handshake with Sec-WebSocket-Protocol", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
"HTTP/1.1 101 Switching Protocols\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Accept: JdiiuafpBKRqD7eol0y4vJDTsTs=\r\n" +
"Sec-WebSocket-Protocol: xmpp\r\n" +
"\r\n"))
driver().start()
}})
it("sets the subprotocol", function() { with(this) {
driver().start()
assertEqual( "xmpp", driver().protocol )
}})
}})
describe("with custom headers", function() { with(this) {
before(function() { with(this) {
driver().setHeader("Authorization", "Bearer WAT")
}})
it("writes the handshake with Sec-WebSocket-Protocol", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
"HTTP/1.1 101 Switching Protocols\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Accept: JdiiuafpBKRqD7eol0y4vJDTsTs=\r\n" +
"Authorization: Bearer WAT\r\n" +
"\r\n"))
driver().start()
}})
}})
it("triggers the onopen event", function() { with(this) {
driver().start()
assertEqual( true, open )
}})
it("changes the state to open", function() { with(this) {
driver().start()
assertEqual( "open", driver().getState() )
}})
it("sets the protocol version", function() { with(this) {
driver().start()
assertEqual( "hybi-13", driver().version )
}})
}})
describe("frame", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().frame("Hello, world")
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().frame("whatever") )
}})
it("queues the frames until the handshake has been send", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
"HTTP/1.1 101 Switching Protocols\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Accept: JdiiuafpBKRqD7eol0y4vJDTsTs=\r\n" +
"\r\n"))
expect(driver().io, "emit").given("data", buffer([0x81, 0x02, 72, 105]))
driver().frame("Hi")
driver().start()
}})
}})
describe("ping", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().ping()
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().ping() )
}})
it("queues the ping until the handshake has been send", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
"HTTP/1.1 101 Switching Protocols\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Accept: JdiiuafpBKRqD7eol0y4vJDTsTs=\r\n" +
"\r\n"))
expect(driver().io, "emit").given("data", buffer([0x89, 0]))
driver().ping()
driver().start()
}})
}})
describe("close", function() { with(this) {
it("does not write anything to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().close()
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().close() )
}})
it("triggers the onclose event", function() { with(this) {
driver().close()
assertEqual( [1000, ""], close )
}})
it("changes the state to closed", function() { with(this) {
driver().close()
assertEqual( "closed", driver().getState() )
}})
}})
}})
describe("in the open state", function() { with(this) {
before(function() { this.driver().start() })
describe("parse", function() { with(this) {
define("mask", function() {
return this._mask = this._mask ||
[1,2,3,4].map(function() { return Math.floor(Math.random() * 256) })
})
define("maskMessage", function(bytes) {
var output = []
for (var i = 0, n = bytes.length; i < n; i++) {
output[i] = bytes[i] ^ this.mask()[i % 4]
}
return output
})
it("parses unmasked text frames", function() { with(this) {
driver().parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
assertEqual( "Hello", message )
}})
it("parses multiple frames from the same packet", function() { with(this) {
driver().parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
assertEqual( "HelloHello", message )
}})
it("parses empty text frames", function() { with(this) {
driver().parse([0x81, 0x00])
assertEqual( "", message )
}})
it("parses fragmented text frames", function() { with(this) {
driver().parse([0x01, 0x03, 0x48, 0x65, 0x6c])
driver().parse([0x80, 0x02, 0x6c, 0x6f])
assertEqual( "Hello", message )
}})
it("parses masked text frames", function() { with(this) {
driver().parse([0x81, 0x85])
driver().parse(mask())
driver().parse(maskMessage([0x48, 0x65, 0x6c, 0x6c, 0x6f]))
assertEqual( "Hello", message )
}})
it("parses masked empty text frames", function() { with(this) {
driver().parse([0x81, 0x80])
driver().parse(mask())
driver().parse(maskMessage([]))
assertEqual( "", message )
}})
it("parses masked fragmented text frames", function() { with(this) {
driver().parse([0x01, 0x81])
driver().parse(mask())
driver().parse(maskMessage([0x48]))
driver().parse([0x80, 0x84])
driver().parse(mask())
driver().parse(maskMessage([0x65, 0x6c, 0x6c, 0x6f]))
assertEqual( "Hello", message )
}})
it("closes the socket if the frame has an unrecognized opcode", function() { with(this) {
driver().parse([0x83, 0x00])
assertEqual( [0x88, 0x1e, 0x03, 0xea], collector().bytes.slice(0,4) )
assertEqual( "Unrecognized frame opcode: 3", error.message )
assertEqual( [1002, "Unrecognized frame opcode: 3"], close )
assertEqual( "closed", driver().getState() )
}})
it("closes the socket if a close frame is received", function() { with(this) {
driver().parse([0x88, 0x07, 0x03, 0xe8, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
assertEqual( [0x88, 0x07, 0x03, 0xe8, 0x48, 0x65, 0x6c, 0x6c, 0x6f], collector().bytes )
assertEqual( [1000, "Hello"], close )
assertEqual( "closed", driver().getState() )
}})
it("parses unmasked multibyte text frames", function() { with(this) {
driver().parse([0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf])
assertEqual( "Apple = ", message )
}})
it("parses frames received in several packets", function() { with(this) {
driver().parse([0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c])
driver().parse([0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf])
assertEqual( "Apple = ", message )
}})
it("parses fragmented multibyte text frames", function() { with(this) {
driver().parse([0x01, 0x0a, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3])
driver().parse([0x80, 0x01, 0xbf])
assertEqual( "Apple = ", message )
}})
it("parse masked multibyte text frames", function() { with(this) {
driver().parse([0x81, 0x8b])
driver().parse(mask())
driver().parse(maskMessage([0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf]))
assertEqual( "Apple = ", message )
}})
it("parses masked fragmented multibyte text frames", function() { with(this) {
driver().parse([0x01, 0x8a])
driver().parse(mask())
driver().parse(maskMessage([0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3]))
driver().parse([0x80, 0x81])
driver().parse(mask())
driver().parse(maskMessage([0xbf]))
assertEqual( "Apple = ", message )
}})
it("parses unmasked medium-length text frames", function() { with(this) {
driver().parse([0x81, 0x7e, 0x00, 0xc8])
var i = 40, result = ""
while (i--) {
driver().parse([0x48, 0x65, 0x6c, 0x6c, 0x6f])
result += "Hello"
}
assertEqual( result, message )
}})
it("returns an error for too-large frames", function() { with(this) {
driver().parse([0x81, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00])
assertEqual( "WebSocket frame length too large", error.message )
assertEqual( [1009, "WebSocket frame length too large"], close )
assertEqual( "closed", driver().getState() )
}})
it("parses masked medium-length text frames", function() { with(this) {
driver().parse([0x81, 0xfe, 0x00, 0xc8])
driver().parse(mask())
var i = 40, result = "", packet = []
while (i--) {
packet = packet.concat([0x48, 0x65, 0x6c, 0x6c, 0x6f])
result += "Hello"
}
driver().parse(maskMessage(packet))
assertEqual( result, message )
}})
it("replies to pings with a pong", function() { with(this) {
driver().parse([0x89, 0x04, 0x4f, 0x48, 0x41, 0x49])
assertEqual( [0x8a, 0x04, 0x4f, 0x48, 0x41, 0x49], collector().bytes )
}})
}})
describe("frame", function() { with(this) {
it("formats the given string as a WebSocket frame", function() { with(this) {
driver().frame("Hello")
assertEqual( [0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f], collector().bytes )
}})
it("formats a byte array as a binary WebSocket frame", function() { with(this) {
driver().frame([0x48, 0x65, 0x6c])
assertEqual( [0x82, 0x03, 0x48, 0x65, 0x6c], collector().bytes )
}})
it("encodes multibyte characters correctly", function() { with(this) {
driver().frame("Apple = ")
assertEqual( [0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf], collector().bytes )
}})
it("encodes medium-length strings using extra length bytes", function() { with(this) {
var i = 40, frame = [0x81, 0x7e, 0x00, 0xc8], string = ""
while (i--) {
string += "Hello"
frame = frame.concat([0x48, 0x65, 0x6c, 0x6c, 0x6f])
}
driver().frame(string)
assertEqual( frame, collector(). bytes )
}})
it("encodes close frames with an error code", function() { with(this) {
driver().frame("Hello", "close", 1002)
assertEqual( [0x88, 0x07, 0x03, 0xea, 0x48, 0x65, 0x6c, 0x6c, 0x6f], collector().bytes )
}})
it("encodes pong frames", function() { with(this) {
driver().frame("", "pong")
assertEqual( [0x8a, 0x00], collector().bytes )
}})
}})
describe("ping", function() { with(this) {
it("writes a ping frame to the socket", function() { with(this) {
driver().ping("mic check")
assertEqual( [0x89, 0x09, 0x6d, 0x69, 0x63, 0x20, 0x63, 0x68, 0x65, 0x63, 0x6b], collector().bytes )
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().ping() )
}})
it("runs the given callback on mathing pong", function() { with(this) {
var reply = null
driver().ping("Hi", function() { reply = true })
driver().parse([0x8a, 0x02, 72, 105])
assert( reply )
}})
it("does not run the callback on non-matching pong", function() { with(this) {
var reply = null
driver().ping("Hi", function() { reply = true })
driver().parse([0x8a, 0x03, 119, 97, 116])
assert( !reply )
}})
}})
describe("close", function() { with(this) {
it("writes a close frame to the socket", function() { with(this) {
driver().close("<%= reasons %>", 1003)
assertEqual( [0x88, 0x10, 0x03, 0xeb, 0x3c, 0x25, 0x3d, 0x20, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x73, 0x20, 0x25, 0x3e], collector().bytes )
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().close() )
}})
it("does not trigger the close event", function() { with(this) {
driver().close()
assertEqual( false, close )
}})
it("does not trigger the onerror event", function() { with(this) {
driver().close()
assertEqual( false, error )
}})
it("changes the state to closing", function() { with(this) {
driver().close()
assertEqual( "closing", driver().getState() )
}})
}})
}})
describe("when masking is required", function() { with(this) {
before(function() {
this.options().requireMasking = true
this.driver().start()
})
it("does not emit a message", function() { with(this) {
driver().parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
assertEqual( "", message )
}})
it("returns an error", function() { with(this) {
driver().parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
assertEqual( "Received unmasked frame but masking is required", error.message )
assertEqual( [1003, "Received unmasked frame but masking is required"], close )
}})
}})
describe("in the closing state", function() { with(this) {
before(function() {
this.driver().start()
this.driver().close()
})
describe("frame", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().frame("dropped")
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().frame("wut") )
}})
}})
describe("ping", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().ping()
}})
it("returns false", function() { with(this) {
assertEqual( false, driver().ping() )
}})
}})
describe("close", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().close()
}})
it("returns false", function() { with(this) {
assertEqual( false, driver().close() )
}})
}})
describe("receiving a close frame", function() { with(this) {
before(function() {
this.driver().parse([0x88, 0x04, 0x03, 0xe9, 0x4f, 0x4b])
})
it("triggers the onclose event", function() { with(this) {
assertEqual( [1001, "OK"], close )
}})
it("changes the state to closed", function() { with(this) {
assertEqual( "closed", driver().getState() )
}})
}})
}})
describe("in the closed state", function() { with(this) {
before(function() {
this.driver().start()
this.driver().close()
this.driver().parse([0x88, 0x02, 0x03, 0xea])
})
describe("frame", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().frame("dropped")
}})
it("returns false", function() { with(this) {
assertEqual( false, driver().frame("wut") )
}})
}})
describe("ping", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().ping()
}})
it("returns false", function() { with(this) {
assertEqual( false, driver().ping() )
}})
}})
describe("close", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().close()
}})
it("returns false", function() { with(this) {
assertEqual( false, driver().close() )
}})
it("leaves the state as closed", function() { with(this) {
driver().close()
assertEqual( "closed", driver().getState() )
}})
}})
}})
}})