Added a websocket demo

master
Cyrille Médard de Chardon 2013-09-01 22:54:51 +02:00
parent e827fdecad
commit c519459aa8
35 changed files with 5338 additions and 0 deletions

Binary file not shown.

View File

@ -0,0 +1,81 @@
Changelog
=========
Version 1.0.8
-------------
*Released 2012-12-26*
- Fixed remaining naming inconsistency of "websocketVersion" as opposed to "webSocketVersion" throughout the code, and added deprecation warnings for use of the old casing throughout.
- Fixed an issue with our case-insensitive handling of WebSocket subprotocols. Clients that requested a mixed-case subprotocol would end up failing the connection when the server accepted the connection, returning a lower-case version of the subprotocol name. Now we return the subprotocol name in the exact casing that was requested by the client, while still maintaining the case-insensitive verification logic for convenience and practicality.
- Making sure that any socket-level activity timeout that may have been set on a TCP socket is removed when initializing a connection.
- Added support for native TCP Keep-Alive instead of using the WebSocket ping/pong packets to serve that function.
- Fixed cookie parsing to be compliant with RFC 2109
Version 1.0.7
-------------
*Released 2012-08-12*
- ***Native modules are now optional!*** If they fail to compile, WebSocket-Node will still work but will not verify that received UTF-8 data is valid, and xor masking/unmasking of payload data for security purposes will not be as efficient as it is performed in JavaScript instead of native code.
- Reduced Node.JS version requirement back to v0.6.10
Version 1.0.6
-------------
*Released 2012-05-22*
- Now requires Node v0.6.13 since that's the first version that I can manage to successfully build the native UTF-8 validator with node-gyp through npm.
Version 1.0.5
-------------
*Released 2012-05-21*
- Fixes the issues that users were having building the native UTF-8 validator on Windows platforms. Special Thanks to:
- [zerodivisi0n](https://github.com/zerodivisi0n)
- [andreasbotsikas](https://github.com/andreasbotsikas)
- Fixed accidental global variable usage (Thanks, [hakobera](https://github.com/hakobera)!)
- Added callbacks to the send* methods that provide notification of messages being sent on the wire and any socket errors that may occur when sending a message. (Thanks, [zerodivisi0n](https://github.com/zerodivisi0n)!)
- Added option to disable logging in the echo-server in the test folder (Thanks, [oberstet](https://github.com/oberstet)!)
Version 1.0.4
-------------
*Released 2011-12-18*
- Now validates that incoming UTF-8 messages do, in fact, contain valid UTF-8 data. The connection is dropped with prejudice if invalid data is received. This strict behavior conforms to the WebSocket RFC and is verified by the Autobahn Test Suite. This is accomplished in a performant way by using a native C++ Node module created by [einaros](https://github.com/einaros).
- Updated handling of connection closure to pass more of the Autobahn Test Suite.
Version 1.0.3
-------------
*Released 2011-12-18*
- Substantial speed increase (~150% on my machine, depending on the circumstances) due to an optimization in FastBufferList.js that drastically reduces the number of memory alloctions and buffer copying. ([kazuyukitanimura](https://github.com/kazuyukitanimura))
Version 1.0.2
-------------
*Released 2011-11-28*
- Fixing whiteboard example to work under Node 0.6.x ([theturtle32](https://github.com/theturtle32))
- Now correctly emitting a `close` event with a 1006 error code if there is a TCP error while writing to the socket during the handshake. ([theturtle32](https://github.com/theturtle32))
- Catching errors when writing to the TCP socket during the handshake. ([justoneplanet](https://github.com/justoneplanet))
- No longer outputting console.warn messages when there is an error writing to the TCP socket ([justoneplanet](https://github.com/justoneplanet))
- Fixing some formatting errors, commas, semicolons, etc. ([kaisellgren](https://github.com/kaisellgren))
Version 1.0.1
-------------
*Released 2011-11-21*
- Now works with Node 0.6.2 as well as 0.4.12
- Support TLS in WebSocketClient
- Added support for setting and reading cookies
- Added WebSocketServer.prototype.broadcast(data) convenience method
- Added `resourceURL` property to WebSocketRequest objects. It is a Node URL object with the `resource` and any query string params already parsed.
- The WebSocket request router no longer includes the entire query string when trying to match the path name of the request.
- WebSocketRouterRequest objects now include all the properties and events of WebSocketRequest objects.
- Removed more console.log statements. Please rely on the various events emitted to be notified of error conditions. I decided that it is not a library's place to spew information to the console.
- Renamed the `websocketVersion` property to `webSocketVersion` throughout the code to fix inconsistent capitalization. `websocketVersion` has been kept for compatibility but is deprecated and may be removed in the future.
- Now outputting the sanitized version of custom header names rather than the raw value. This prevents invalid HTTP from being put onto the wire if given an illegal header name.
I decided it's time to start maintaining a changelog now, starting with version 1.0.1.

View File

@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View File

@ -0,0 +1,11 @@
all:
node-gyp configure build
clean:
node-gyp clean
autobahn:
@NODE_PATH=lib node test/autobahn-test-client.js --host=127.0.0.1 --port=9000
autobahn-server:
@NODE_PATH=lib node test/echo-server.js

View File

@ -0,0 +1,245 @@
WebSocket Client & Server Implementation for Node
=================================================
Overview
--------
This is a (mostly) pure JavaScript implementation of the WebSocket protocol versions 8 and 13 for Node. There are some example client and server applications that implement various interoperability testing protocols in the "test" folder.
Current News
------------
- As of version 1.0.7, ***Native modules are now optional.*** If they fail to compile, WebSocket-Node will still work but will not verify that received UTF-8 data is valid, and xor masking/unmasking of payload data for security purposes will not be as efficient as it is performed in JavaScript instead of native code.
- Version 1.0.7 requires node v0.6.10, since that's the first version that I can manage to successfully build the native extensions with node-gyp through npm. If anyone can figure out how to build native extensions in a way that works with both older and newer versions of Node, I'm happy to accept a patch!
- If you want to support Unicode characters outside the Basic Multilingual Plane (BMP) you must use Node v0.8.x, which added support for representing these characters as surrogate pairs inside JavaScript strings. Under Node v0.6.x, characters with code points greater than 65535 (greater than a 16-bit unsigned value) will have their code point truncated, resulting in seemingly unpredictable characters being returned.
- WebSocket-Node was already [one of the fastest WebSocket libraries for Node](http://hobbycoding.posterous.com/websockt-binary-data-transfer-benchmark-rsult), and thanks to a small patch from [kazuyukitanimura](https://github.com/kazuyukitanimura), this library is now [up to 200% faster](http://hobbycoding.posterous.com/how-to-make-websocket-work-2x-faster-on-nodej) as of version 1.0.3!
Changelog
---------
Current Version: 1.0.7
[View the changelog](https://github.com/Worlize/WebSocket-Node/blob/master/CHANGELOG.md)
Browser Support
---------------
* Firefox 7-9 (Old) (Protocol Version 8)
* Firefox 10+ (Protocol Version 13)
* Chrome 14,15 (Old) (Protocol Version 8)
* Chrome 16+ (Protocol Version 13)
* Internet Explorer 10 (Preview) (Protocol Version 13)
* Safari 6 (Protocol Version 13)
***Safari older than 6.0 is not supported since it uses a very old draft of WebSockets***
I made a decision early on to explicitly avoid maintaining multiple slightly different copies of the same code just to support the browsers currently in the wild. The major browsers that support WebSocket are on a rapid-release schedule (with the exception of Safari) and now that the final version of the protocol has been [published as an official RFC](http://datatracker.ietf.org/doc/rfc6455/), it won't be long before support in the wild stabilizes on that version. My client application is in Flash/ActionScript 3, so for my purposes I'm not dependent on the browser implementations. *I made an exception to my stated intention here to support protocol version 8 along with 13, since only one minor thing changed and it was trivial to handle conditionally.* The library now interoperates with other clients and servers implementing draft -08 all the way up through the final RFC.
***If you need to simultaneously support legacy browser versions that had implemented draft-75/draft-76/draft-00, take a look here: https://gist.github.com/1428579***
For a WebSocket client written in ActionScript 3, see my [AS3WebScocket](https://github.com/Worlize/AS3WebSocket) project.
Benchmarks
----------
There are some basic benchmarking sections in the Autobahn test suite. I've put up a [benchmark page](http://worlize.github.com/WebSocket-Node/benchmarks/) that shows the results from the Autobahn tests run against AutobahnServer 0.4.10, WebSocket-Node 1.0.2, WebSocket-Node 1.0.4, and ws 0.3.4.
Autobahn Tests
--------------
The very complete [Autobahn Test Suite](http://www.tavendo.de/autobahn/testsuite.html) is used by most WebSocket implementations to test spec compliance and interoperability.
**Note about failing UTF-8 tests:** There are some UTF-8 validation tests that fail due to the fact that according to the ECMAScript spec, V8 and subsequently Node cannot support Unicode characters outside the BMP (Basic Multilingual Plane.) JavaScript's String.fromCharCode() function truncates all code points to 16-bit, so you cannot decode higher plane code points in JavaScript. Google's V8 uses UCS-2 as its internal string representation, and [they have no intention to change that any time soon](http://code.google.com/p/v8/issues/detail?id=761), so it is not possible to decode higher plane code points in C++, to the best of my knowledge, because those characters are not representable in UCS-2 anyway. The Autobahn Test Suite requires that all valid Unicode code points survive a complete round trip, including code points that represent non-existent characters and characters above the BMP. Since JavaScript cannot represent any characters with a code point >= 65535, no JavaScript implementation of WebSockets can pass these UTF-8 tests without using a cheat, such as echoing back the original binary data without decoding and re-encoding the UTF-8 data, which is not representative of real-world practical application. ***I do not consider this to be a problem in the majority of circumstances*** since it is very unlikely to cause major issues in any real-world application as long as you don't need to use characters outside the BMP.
**Update:** This issue seems to have been resolved in the version of V8 used in Node 0.8.x. I believe they are using surrogate-pairs to accommodate characters that are outside the BMP, but I haven't looked into it.
- [View Server Test Results](http://worlize.github.com/WebSocket-Node/test-report/servers/)
- [View Client Test Results](http://worlize.github.com/WebSocket-Node/test-report/clients/)
Notes
-----
This library has been used in production on [worlize.com](https://www.worlize.com) since April 2011 and seems to be stable. Your mileage may vary.
**Tested with the following node versions:**
- 0.6.18
- 0.8.6
It may work in earlier or later versions but I'm not actively testing it outside of the listed versions. YMMV.
Documentation
=============
For more complete documentation, see the [Documentation Wiki](https://github.com/Worlize/WebSocket-Node/wiki/Documentation).
Installation
------------
A few users have reported difficulties building the native extensions without first manually installing node-gyp. If you have trouble building the native extensions, make sure you've got a C++ compiler, and have done `npm install -g node-gyp` first.
Native extensions are optional, however, and WebSocket-Node will work even if the extensions cannot be compiled.
In your project root:
$ npm install websocket
Then in your code:
```javascript
var WebSocketServer = require('websocket').server;
var WebSocketClient = require('websocket').client;
var WebSocketFrame = require('websocket').frame;
var WebSocketRouter = require('websocket').router;
```
Note for Windows Users
----------------------
Because there is a small C++ component used for validating UTF-8 data, you will need to install a few other software packages in addition to Node to be able to build this module:
- [Microsoft Visual C++](http://www.microsoft.com/visualstudio/en-us/products/2010-editions/visual-cpp-express)
- [Python 2.7](http://www.python.org/download/) (NOT Python 3.x)
Current Features:
-----------------
- Licensed under the Apache License, Version 2.0
- Protocol version "8" and "13" (Draft-08 through the final RFC) framing and handshake
- Can handle/aggregate received fragmented messages
- Can fragment outgoing messages
- Router to mount multiple applications to various path and protocol combinations
- TLS supported for outbound connections via WebSocketClient
- TLS supported for server connections (use https.createServer instead of http.createServer)
- Thanks to [pors](https://github.com/pors) for confirming this!
- Cookie setting and parsing
- Tunable settings
- Max Receivable Frame Size
- Max Aggregate ReceivedMessage Size
- Whether to fragment outgoing messages
- Fragmentation chunk size for outgoing messages
- Whether to automatically send ping frames for the purposes of keepalive
- Keep-alive ping interval
- Whether or not to automatically assemble received fragments (allows application to handle individual fragments directly)
- How long to wait after sending a close frame for acknowledgment before closing the socket.
Known Issues/Missing Features:
------------------------------
- No API for user-provided protocol extensions.
Usage Examples
==============
Server Example
--------------
Here's a short example showing a server that echos back anything sent to it, whether utf-8 or binary.
```javascript
#!/usr/bin/env node
var WebSocketServer = require('websocket').server;
var http = require('http');
var server = http.createServer(function(request, response) {
console.log((new Date()) + ' Received request for ' + request.url);
response.writeHead(404);
response.end();
});
server.listen(8080, function() {
console.log((new Date()) + ' Server is listening on port 8080');
});
wsServer = new WebSocketServer({
httpServer: server,
// You should not use autoAcceptConnections for production
// applications, as it defeats all standard cross-origin protection
// facilities built into the protocol and the browser. You should
// *always* verify the connection's origin and decide whether or not
// to accept it.
autoAcceptConnections: false
});
function originIsAllowed(origin) {
// put logic here to detect whether the specified origin is allowed.
return true;
}
wsServer.on('request', function(request) {
if (!originIsAllowed(request.origin)) {
// Make sure we only accept requests from an allowed origin
request.reject();
console.log((new Date()) + ' Connection from origin ' + request.origin + ' rejected.');
return;
}
var connection = request.accept('echo-protocol', request.origin);
console.log((new Date()) + ' Connection accepted.');
connection.on('message', function(message) {
if (message.type === 'utf8') {
console.log('Received Message: ' + message.utf8Data);
connection.sendUTF(message.utf8Data);
}
else if (message.type === 'binary') {
console.log('Received Binary Message of ' + message.binaryData.length + ' bytes');
connection.sendBytes(message.binaryData);
}
});
connection.on('close', function(reasonCode, description) {
console.log((new Date()) + ' Peer ' + connection.remoteAddress + ' disconnected.');
});
});
```
Client Example
--------------
This is a simple example client that will print out any utf-8 messages it receives on the console, and periodically sends a random number.
*This code demonstrates a client in Node.js, not in the browser*
```javascript
#!/usr/bin/env node
var WebSocketClient = require('websocket').client;
var client = new WebSocketClient();
client.on('connectFailed', function(error) {
console.log('Connect Error: ' + error.toString());
});
client.on('connect', function(connection) {
console.log('WebSocket client connected');
connection.on('error', function(error) {
console.log("Connection Error: " + error.toString());
});
connection.on('close', function() {
console.log('echo-protocol Connection Closed');
});
connection.on('message', function(message) {
if (message.type === 'utf8') {
console.log("Received: '" + message.utf8Data + "'");
}
});
function sendNumber() {
if (connection.connected) {
var number = Math.round(Math.random() * 0xFFFFFF);
connection.sendUTF(number.toString());
setTimeout(sendNumber, 1000);
}
}
sendNumber();
});
client.connect('ws://localhost:8080/', 'echo-protocol');
```
Request Router Example
----------------------
For an example of using the request router, see `libwebsockets-test-server.js` in the `test` folder.
Resources
---------
A presentation on the state of the WebSockets protocol that I gave on July 23, 2011 at the LA Hacker News meetup. [WebSockets: The Real-Time Web, Delivered](http://www.scribd.com/doc/60898569/WebSockets-The-Real-Time-Web-Delivered)

View File

@ -0,0 +1,14 @@
{
'targets': [
{
'target_name': 'validation',
'cflags': [ '-O3' ],
'sources': [ 'src/validation.cc' ]
},
{
'target_name': 'xor',
'cflags': [ '-O3' ],
'sources': [ 'src/xor.cpp' ]
}
]
}

View File

@ -0,0 +1,332 @@
# We borrow heavily from the kernel build setup, though we are simpler since
# we don't have Kconfig tweaking settings on us.
# The implicit make rules have it looking for RCS files, among other things.
# We instead explicitly write all the rules we care about.
# It's even quicker (saves ~200ms) to pass -r on the command line.
MAKEFLAGS=-r
# The source directory tree.
srcdir := ..
abs_srcdir := $(abspath $(srcdir))
# The name of the builddir.
builddir_name ?= .
# The V=1 flag on command line makes us verbosely print command lines.
ifdef V
quiet=
else
quiet=quiet_
endif
# Specify BUILDTYPE=Release on the command line for a release build.
BUILDTYPE ?= Release
# Directory all our build output goes into.
# Note that this must be two directories beneath src/ for unit tests to pass,
# as they reach into the src/ directory for data with relative paths.
builddir ?= $(builddir_name)/$(BUILDTYPE)
abs_builddir := $(abspath $(builddir))
depsdir := $(builddir)/.deps
# Object output directory.
obj := $(builddir)/obj
abs_obj := $(abspath $(obj))
# We build up a list of every single one of the targets so we can slurp in the
# generated dependency rule Makefiles in one pass.
all_deps :=
# C++ apps need to be linked with g++.
#
# Note: flock is used to seralize linking. Linking is a memory-intensive
# process so running parallel links can often lead to thrashing. To disable
# the serialization, override LINK via an envrionment variable as follows:
#
# export LINK=g++
#
# This will allow make to invoke N linker processes as specified in -jN.
LINK ?= ./gyp-mac-tool flock $(builddir)/linker.lock $(CXX.target)
CC.target ?= $(CC)
CFLAGS.target ?= $(CFLAGS)
CXX.target ?= $(CXX)
CXXFLAGS.target ?= $(CXXFLAGS)
LINK.target ?= $(LINK)
LDFLAGS.target ?= $(LDFLAGS)
AR.target ?= $(AR)
# TODO(evan): move all cross-compilation logic to gyp-time so we don't need
# to replicate this environment fallback in make as well.
CC.host ?= gcc
CFLAGS.host ?=
CXX.host ?= g++
CXXFLAGS.host ?=
LINK.host ?= g++
LDFLAGS.host ?=
AR.host ?= ar
# Define a dir function that can handle spaces.
# http://www.gnu.org/software/make/manual/make.html#Syntax-of-Functions
# "leading spaces cannot appear in the text of the first argument as written.
# These characters can be put into the argument value by variable substitution."
empty :=
space := $(empty) $(empty)
# http://stackoverflow.com/questions/1189781/using-make-dir-or-notdir-on-a-path-with-spaces
replace_spaces = $(subst $(space),?,$1)
unreplace_spaces = $(subst ?,$(space),$1)
dirx = $(call unreplace_spaces,$(dir $(call replace_spaces,$1)))
# Flags to make gcc output dependency info. Note that you need to be
# careful here to use the flags that ccache and distcc can understand.
# We write to a dep file on the side first and then rename at the end
# so we can't end up with a broken dep file.
depfile = $(depsdir)/$(call replace_spaces,$@).d
DEPFLAGS = -MMD -MF $(depfile).raw
# We have to fixup the deps output in a few ways.
# (1) the file output should mention the proper .o file.
# ccache or distcc lose the path to the target, so we convert a rule of
# the form:
# foobar.o: DEP1 DEP2
# into
# path/to/foobar.o: DEP1 DEP2
# (2) we want missing files not to cause us to fail to build.
# We want to rewrite
# foobar.o: DEP1 DEP2 \
# DEP3
# to
# DEP1:
# DEP2:
# DEP3:
# so if the files are missing, they're just considered phony rules.
# We have to do some pretty insane escaping to get those backslashes
# and dollar signs past make, the shell, and sed at the same time.
# Doesn't work with spaces, but that's fine: .d files have spaces in
# their names replaced with other characters.
define fixup_dep
# The depfile may not exist if the input file didn't have any #includes.
touch $(depfile).raw
# Fixup path as in (1).
sed -e "s|^$(notdir $@)|$@|" $(depfile).raw >> $(depfile)
# Add extra rules as in (2).
# We remove slashes and replace spaces with new lines;
# remove blank lines;
# delete the first line and append a colon to the remaining lines.
sed -e 's|\\||' -e 'y| |\n|' $(depfile).raw |\
grep -v '^$$' |\
sed -e 1d -e 's|$$|:|' \
>> $(depfile)
rm $(depfile).raw
endef
# Command definitions:
# - cmd_foo is the actual command to run;
# - quiet_cmd_foo is the brief-output summary of the command.
quiet_cmd_cc = CC($(TOOLSET)) $@
cmd_cc = $(CC.$(TOOLSET)) $(GYP_CFLAGS) $(DEPFLAGS) $(CFLAGS.$(TOOLSET)) -c -o $@ $<
quiet_cmd_cxx = CXX($(TOOLSET)) $@
cmd_cxx = $(CXX.$(TOOLSET)) $(GYP_CXXFLAGS) $(DEPFLAGS) $(CXXFLAGS.$(TOOLSET)) -c -o $@ $<
quiet_cmd_objc = CXX($(TOOLSET)) $@
cmd_objc = $(CC.$(TOOLSET)) $(GYP_OBJCFLAGS) $(DEPFLAGS) -c -o $@ $<
quiet_cmd_objcxx = CXX($(TOOLSET)) $@
cmd_objcxx = $(CXX.$(TOOLSET)) $(GYP_OBJCXXFLAGS) $(DEPFLAGS) -c -o $@ $<
# Commands for precompiled header files.
quiet_cmd_pch_c = CXX($(TOOLSET)) $@
cmd_pch_c = $(CC.$(TOOLSET)) $(GYP_PCH_CFLAGS) $(DEPFLAGS) $(CXXFLAGS.$(TOOLSET)) -c -o $@ $<
quiet_cmd_pch_cc = CXX($(TOOLSET)) $@
cmd_pch_cc = $(CC.$(TOOLSET)) $(GYP_PCH_CXXFLAGS) $(DEPFLAGS) $(CXXFLAGS.$(TOOLSET)) -c -o $@ $<
quiet_cmd_pch_m = CXX($(TOOLSET)) $@
cmd_pch_m = $(CC.$(TOOLSET)) $(GYP_PCH_OBJCFLAGS) $(DEPFLAGS) -c -o $@ $<
quiet_cmd_pch_mm = CXX($(TOOLSET)) $@
cmd_pch_mm = $(CC.$(TOOLSET)) $(GYP_PCH_OBJCXXFLAGS) $(DEPFLAGS) -c -o $@ $<
# gyp-mac-tool is written next to the root Makefile by gyp.
# Use $(4) for the command, since $(2) and $(3) are used as flag by do_cmd
# already.
quiet_cmd_mac_tool = MACTOOL $(4) $<
cmd_mac_tool = ./gyp-mac-tool $(4) $< "$@"
quiet_cmd_mac_package_framework = PACKAGE FRAMEWORK $@
cmd_mac_package_framework = ./gyp-mac-tool package-framework "$@" $(4)
quiet_cmd_infoplist = INFOPLIST $@
cmd_infoplist = $(CC.$(TOOLSET)) -E -P -Wno-trigraphs -x c $(INFOPLIST_DEFINES) "$<" -o "$@"
quiet_cmd_touch = TOUCH $@
cmd_touch = touch $@
quiet_cmd_copy = COPY $@
# send stderr to /dev/null to ignore messages when linking directories.
cmd_copy = rm -rf "$@" && cp -af "$<" "$@"
quiet_cmd_alink = LIBTOOL-STATIC $@
cmd_alink = rm -f $@ && ./gyp-mac-tool filter-libtool libtool $(GYP_LIBTOOLFLAGS) -static -o $@ $(filter %.o,$^)
quiet_cmd_link = LINK($(TOOLSET)) $@
cmd_link = $(LINK.$(TOOLSET)) $(GYP_LDFLAGS) $(LDFLAGS.$(TOOLSET)) -o "$@" $(LD_INPUTS) $(LIBS)
# TODO(thakis): Find out and document the difference between shared_library and
# loadable_module on mac.
quiet_cmd_solink = SOLINK($(TOOLSET)) $@
cmd_solink = $(LINK.$(TOOLSET)) -shared $(GYP_LDFLAGS) $(LDFLAGS.$(TOOLSET)) -o "$@" $(LD_INPUTS) $(LIBS)
# TODO(thakis): The solink_module rule is likely wrong. Xcode seems to pass
# -bundle -single_module here (for osmesa.so).
quiet_cmd_solink_module = SOLINK_MODULE($(TOOLSET)) $@
cmd_solink_module = $(LINK.$(TOOLSET)) -shared $(GYP_LDFLAGS) $(LDFLAGS.$(TOOLSET)) -o $@ $(filter-out FORCE_DO_CMD, $^) $(LIBS)
# Define an escape_quotes function to escape single quotes.
# This allows us to handle quotes properly as long as we always use
# use single quotes and escape_quotes.
escape_quotes = $(subst ','\'',$(1))
# This comment is here just to include a ' to unconfuse syntax highlighting.
# Define an escape_vars function to escape '$' variable syntax.
# This allows us to read/write command lines with shell variables (e.g.
# $LD_LIBRARY_PATH), without triggering make substitution.
escape_vars = $(subst $$,$$$$,$(1))
# Helper that expands to a shell command to echo a string exactly as it is in
# make. This uses printf instead of echo because printf's behaviour with respect
# to escape sequences is more portable than echo's across different shells
# (e.g., dash, bash).
exact_echo = printf '%s\n' '$(call escape_quotes,$(1))'
# Helper to compare the command we're about to run against the command
# we logged the last time we ran the command. Produces an empty
# string (false) when the commands match.
# Tricky point: Make has no string-equality test function.
# The kernel uses the following, but it seems like it would have false
# positives, where one string reordered its arguments.
# arg_check = $(strip $(filter-out $(cmd_$(1)), $(cmd_$@)) \
# $(filter-out $(cmd_$@), $(cmd_$(1))))
# We instead substitute each for the empty string into the other, and
# say they're equal if both substitutions produce the empty string.
# .d files contain ? instead of spaces, take that into account.
command_changed = $(or $(subst $(cmd_$(1)),,$(cmd_$(call replace_spaces,$@))),\
$(subst $(cmd_$(call replace_spaces,$@)),,$(cmd_$(1))))
# Helper that is non-empty when a prerequisite changes.
# Normally make does this implicitly, but we force rules to always run
# so we can check their command lines.
# $? -- new prerequisites
# $| -- order-only dependencies
prereq_changed = $(filter-out FORCE_DO_CMD,$(filter-out $|,$?))
# Helper that executes all postbuilds until one fails.
define do_postbuilds
@E=0;\
for p in $(POSTBUILDS); do\
eval $$p;\
E=$$?;\
if [ $$E -ne 0 ]; then\
break;\
fi;\
done;\
if [ $$E -ne 0 ]; then\
rm -rf "$@";\
exit $$E;\
fi
endef
# do_cmd: run a command via the above cmd_foo names, if necessary.
# Should always run for a given target to handle command-line changes.
# Second argument, if non-zero, makes it do asm/C/C++ dependency munging.
# Third argument, if non-zero, makes it do POSTBUILDS processing.
# Note: We intentionally do NOT call dirx for depfile, since it contains ? for
# spaces already and dirx strips the ? characters.
define do_cmd
$(if $(or $(command_changed),$(prereq_changed)),
@$(call exact_echo, $($(quiet)cmd_$(1)))
@mkdir -p "$(call dirx,$@)" "$(dir $(depfile))"
$(if $(findstring flock,$(word 2,$(cmd_$1))),
@$(cmd_$(1))
@echo " $(quiet_cmd_$(1)): Finished",
@$(cmd_$(1))
)
@$(call exact_echo,$(call escape_vars,cmd_$(call replace_spaces,$@) := $(cmd_$(1)))) > $(depfile)
@$(if $(2),$(fixup_dep))
$(if $(and $(3), $(POSTBUILDS)),
$(call do_postbuilds)
)
)
endef
# Declare the "all" target first so it is the default,
# even though we don't have the deps yet.
.PHONY: all
all:
# make looks for ways to re-generate included makefiles, but in our case, we
# don't have a direct way. Explicitly telling make that it has nothing to do
# for them makes it go faster.
%.d: ;
# Use FORCE_DO_CMD to force a target to run. Should be coupled with
# do_cmd.
.PHONY: FORCE_DO_CMD
FORCE_DO_CMD:
TOOLSET := target
# Suffix rules, putting all outputs into $(obj).
$(obj).$(TOOLSET)/%.o: $(srcdir)/%.c FORCE_DO_CMD
@$(call do_cmd,cc,1)
$(obj).$(TOOLSET)/%.o: $(srcdir)/%.cc FORCE_DO_CMD
@$(call do_cmd,cxx,1)
$(obj).$(TOOLSET)/%.o: $(srcdir)/%.cpp FORCE_DO_CMD
@$(call do_cmd,cxx,1)
$(obj).$(TOOLSET)/%.o: $(srcdir)/%.cxx FORCE_DO_CMD
@$(call do_cmd,cxx,1)
$(obj).$(TOOLSET)/%.o: $(srcdir)/%.m FORCE_DO_CMD
@$(call do_cmd,objc,1)
$(obj).$(TOOLSET)/%.o: $(srcdir)/%.mm FORCE_DO_CMD
@$(call do_cmd,objcxx,1)
$(obj).$(TOOLSET)/%.o: $(srcdir)/%.S FORCE_DO_CMD
@$(call do_cmd,cc,1)
$(obj).$(TOOLSET)/%.o: $(srcdir)/%.s FORCE_DO_CMD
@$(call do_cmd,cc,1)
# Try building from generated source, too.
$(obj).$(TOOLSET)/%.o: $(obj).$(TOOLSET)/%.c FORCE_DO_CMD
@$(call do_cmd,cc,1)
$(obj).$(TOOLSET)/%.o: $(obj).$(TOOLSET)/%.cc FORCE_DO_CMD
@$(call do_cmd,cxx,1)
$(obj).$(TOOLSET)/%.o: $(obj).$(TOOLSET)/%.cpp FORCE_DO_CMD
@$(call do_cmd,cxx,1)
$(obj).$(TOOLSET)/%.o: $(obj).$(TOOLSET)/%.cxx FORCE_DO_CMD
@$(call do_cmd,cxx,1)
$(obj).$(TOOLSET)/%.o: $(obj).$(TOOLSET)/%.m FORCE_DO_CMD
@$(call do_cmd,objc,1)
$(obj).$(TOOLSET)/%.o: $(obj).$(TOOLSET)/%.mm FORCE_DO_CMD
@$(call do_cmd,objcxx,1)
$(obj).$(TOOLSET)/%.o: $(obj).$(TOOLSET)/%.S FORCE_DO_CMD
@$(call do_cmd,cc,1)
$(obj).$(TOOLSET)/%.o: $(obj).$(TOOLSET)/%.s FORCE_DO_CMD
@$(call do_cmd,cc,1)
$(obj).$(TOOLSET)/%.o: $(obj)/%.c FORCE_DO_CMD
@$(call do_cmd,cc,1)
$(obj).$(TOOLSET)/%.o: $(obj)/%.cc FORCE_DO_CMD
@$(call do_cmd,cxx,1)
$(obj).$(TOOLSET)/%.o: $(obj)/%.cpp FORCE_DO_CMD
@$(call do_cmd,cxx,1)
$(obj).$(TOOLSET)/%.o: $(obj)/%.cxx FORCE_DO_CMD
@$(call do_cmd,cxx,1)
$(obj).$(TOOLSET)/%.o: $(obj)/%.m FORCE_DO_CMD
@$(call do_cmd,objc,1)
$(obj).$(TOOLSET)/%.o: $(obj)/%.mm FORCE_DO_CMD
@$(call do_cmd,objcxx,1)
$(obj).$(TOOLSET)/%.o: $(obj)/%.S FORCE_DO_CMD
@$(call do_cmd,cc,1)
$(obj).$(TOOLSET)/%.o: $(obj)/%.s FORCE_DO_CMD
@$(call do_cmd,cc,1)

View File

@ -0,0 +1,112 @@
# Do not edit. File was generated by node-gyp's "configure" step
{
"target_defaults": {
"cflags": [],
"default_configuration": "Release",
"defines": [],
"include_dirs": [],
"libraries": []
},
"variables": {
"clang": 1,
"host_arch": "x64",
"node_install_npm": "true",
"node_prefix": "",
"node_shared_cares": "false",
"node_shared_http_parser": "false",
"node_shared_libuv": "false",
"node_shared_openssl": "false",
"node_shared_v8": "false",
"node_shared_zlib": "false",
"node_tag": "",
"node_unsafe_optimizations": 0,
"node_use_dtrace": "true",
"node_use_etw": "false",
"node_use_openssl": "true",
"node_use_perfctr": "false",
"python": "/usr/bin/python",
"target_arch": "x64",
"v8_enable_gdbjit": 0,
"v8_no_strict_aliasing": 1,
"v8_use_snapshot": "false",
"nodedir": "/Users/cyrille/.node-gyp/0.10.17",
"copy_dev_lib": "true",
"standalone_static_library": 1,
"save_dev": "",
"viewer": "man",
"browser": "",
"rollback": "true",
"usage": "",
"globalignorefile": "/usr/local/etc/npmignore",
"shell": "/bin/bash",
"init_author_url": "",
"shrinkwrap": "true",
"parseable": "",
"userignorefile": "/Users/cyrille/.npmignore",
"sign_git_tag": "",
"init_author_email": "",
"cache_max": "null",
"long": "",
"ignore": "",
"npat": "",
"fetch_retries": "2",
"registry": "https://registry.npmjs.org/",
"versions": "",
"message": "%s",
"globalconfig": "/usr/local/etc/npmrc",
"always_auth": "",
"cache_lock_retries": "10",
"proprietary_attribs": "true",
"fetch_retry_mintimeout": "10000",
"json": "",
"coverage": "",
"pre": "",
"https_proxy": "",
"engine_strict": "",
"description": "true",
"userconfig": "/Users/cyrille/.npmrc",
"init_module": "/Users/cyrille/.npm-init.js",
"npaturl": "http://npat.npmjs.org/",
"user": "",
"node_version": "v0.10.17",
"save": "",
"editor": "vi",
"tag": "latest",
"global": "",
"username": "",
"optional": "true",
"force": "",
"bin_links": "true",
"searchopts": "",
"depth": "null",
"searchsort": "name",
"rebuild_bundle": "true",
"yes": "",
"unicode": "true",
"fetch_retry_maxtimeout": "60000",
"strict_ssl": "true",
"group": "20",
"fetch_retry_factor": "10",
"dev": "",
"version": "",
"cache_lock_stale": "60000",
"cache_min": "10",
"searchexclude": "",
"cache": "/Users/cyrille/.npm",
"color": "true",
"save_optional": "",
"user_agent": "node/v0.10.17 darwin x64",
"cache_lock_wait": "10000",
"production": "",
"save_bundle": "",
"umask": "18",
"init_version": "0.0.0",
"init_author_name": "",
"git": "git",
"unsafe_perm": "true",
"tmp": "/var/folders/3v/_hrjdsfs18qcs3y4hzh2y73c0000gn/T/",
"onload_script": "",
"prefix": "/usr/local",
"link": ""
}
}

View File

@ -0,0 +1,224 @@
#!/usr/bin/env python
# Generated by gyp. Do not edit.
# Copyright (c) 2012 Google Inc. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Utility functions to perform Xcode-style build steps.
These functions are executed via gyp-mac-tool when using the Makefile generator.
"""
import fcntl
import os
import plistlib
import re
import shutil
import string
import subprocess
import sys
def main(args):
executor = MacTool()
exit_code = executor.Dispatch(args)
if exit_code is not None:
sys.exit(exit_code)
class MacTool(object):
"""This class performs all the Mac tooling steps. The methods can either be
executed directly, or dispatched from an argument list."""
def Dispatch(self, args):
"""Dispatches a string command to a method."""
if len(args) < 1:
raise Exception("Not enough arguments")
method = "Exec%s" % self._CommandifyName(args[0])
return getattr(self, method)(*args[1:])
def _CommandifyName(self, name_string):
"""Transforms a tool name like copy-info-plist to CopyInfoPlist"""
return name_string.title().replace('-', '')
def ExecCopyBundleResource(self, source, dest):
"""Copies a resource file to the bundle/Resources directory, performing any
necessary compilation on each resource."""
extension = os.path.splitext(source)[1].lower()
if os.path.isdir(source):
# Copy tree.
if os.path.exists(dest):
shutil.rmtree(dest)
shutil.copytree(source, dest)
elif extension == '.xib':
return self._CopyXIBFile(source, dest)
elif extension == '.strings':
self._CopyStringsFile(source, dest)
else:
shutil.copyfile(source, dest)
def _CopyXIBFile(self, source, dest):
"""Compiles a XIB file with ibtool into a binary plist in the bundle."""
tools_dir = os.environ.get('DEVELOPER_BIN_DIR', '/usr/bin')
args = [os.path.join(tools_dir, 'ibtool'), '--errors', '--warnings',
'--notices', '--output-format', 'human-readable-text', '--compile',
dest, source]
ibtool_section_re = re.compile(r'/\*.*\*/')
ibtool_re = re.compile(r'.*note:.*is clipping its content')
ibtoolout = subprocess.Popen(args, stdout=subprocess.PIPE)
current_section_header = None
for line in ibtoolout.stdout:
if ibtool_section_re.match(line):
current_section_header = line
elif not ibtool_re.match(line):
if current_section_header:
sys.stdout.write(current_section_header)
current_section_header = None
sys.stdout.write(line)
return ibtoolout.returncode
def _CopyStringsFile(self, source, dest):
"""Copies a .strings file using iconv to reconvert the input into UTF-16."""
input_code = self._DetectInputEncoding(source) or "UTF-8"
# Xcode's CpyCopyStringsFile / builtin-copyStrings seems to call
# CFPropertyListCreateFromXMLData() behind the scenes; at least it prints
# CFPropertyListCreateFromXMLData(): Old-style plist parser: missing
# semicolon in dictionary.
# on invalid files. Do the same kind of validation.
import CoreFoundation
s = open(source).read()
d = CoreFoundation.CFDataCreate(None, s, len(s))
_, error = CoreFoundation.CFPropertyListCreateFromXMLData(None, d, 0, None)
if error:
return
fp = open(dest, 'w')
args = ['/usr/bin/iconv', '--from-code', input_code, '--to-code',
'UTF-16', source]
subprocess.call(args, stdout=fp)
fp.close()
def _DetectInputEncoding(self, file_name):
"""Reads the first few bytes from file_name and tries to guess the text
encoding. Returns None as a guess if it can't detect it."""
fp = open(file_name, 'rb')
try:
header = fp.read(3)
except e:
fp.close()
return None
fp.close()
if header.startswith("\xFE\xFF"):
return "UTF-16BE"
elif header.startswith("\xFF\xFE"):
return "UTF-16LE"
elif header.startswith("\xEF\xBB\xBF"):
return "UTF-8"
else:
return None
def ExecCopyInfoPlist(self, source, dest):
"""Copies the |source| Info.plist to the destination directory |dest|."""
# Read the source Info.plist into memory.
fd = open(source, 'r')
lines = fd.read()
fd.close()
# Go through all the environment variables and replace them as variables in
# the file.
for key in os.environ:
if key.startswith('_'):
continue
evar = '${%s}' % key
lines = string.replace(lines, evar, os.environ[key])
# Write out the file with variables replaced.
fd = open(dest, 'w')
fd.write(lines)
fd.close()
# Now write out PkgInfo file now that the Info.plist file has been
# "compiled".
self._WritePkgInfo(dest)
def _WritePkgInfo(self, info_plist):
"""This writes the PkgInfo file from the data stored in Info.plist."""
plist = plistlib.readPlist(info_plist)
if not plist:
return
# Only create PkgInfo for executable types.
package_type = plist['CFBundlePackageType']
if package_type != 'APPL':
return
# The format of PkgInfo is eight characters, representing the bundle type
# and bundle signature, each four characters. If that is missing, four
# '?' characters are used instead.
signature_code = plist.get('CFBundleSignature', '????')
if len(signature_code) != 4: # Wrong length resets everything, too.
signature_code = '?' * 4
dest = os.path.join(os.path.dirname(info_plist), 'PkgInfo')
fp = open(dest, 'w')
fp.write('%s%s' % (package_type, signature_code))
fp.close()
def ExecFlock(self, lockfile, *cmd_list):
"""Emulates the most basic behavior of Linux's flock(1)."""
# Rely on exception handling to report errors.
fd = os.open(lockfile, os.O_RDONLY|os.O_NOCTTY|os.O_CREAT, 0o666)
fcntl.flock(fd, fcntl.LOCK_EX)
return subprocess.call(cmd_list)
def ExecFilterLibtool(self, *cmd_list):
"""Calls libtool and filters out 'libtool: file: foo.o has no symbols'."""
libtool_re = re.compile(r'^libtool: file: .* has no symbols$')
libtoolout = subprocess.Popen(cmd_list, stderr=subprocess.PIPE)
_, err = libtoolout.communicate()
for line in err.splitlines():
if not libtool_re.match(line):
print >>sys.stderr, line
return libtoolout.returncode
def ExecPackageFramework(self, framework, version):
"""Takes a path to Something.framework and the Current version of that and
sets up all the symlinks."""
# Find the name of the binary based on the part before the ".framework".
binary = os.path.basename(framework).split('.')[0]
CURRENT = 'Current'
RESOURCES = 'Resources'
VERSIONS = 'Versions'
if not os.path.exists(os.path.join(framework, VERSIONS, version, binary)):
# Binary-less frameworks don't seem to contain symlinks (see e.g.
# chromium's out/Debug/org.chromium.Chromium.manifest/ bundle).
return
# Move into the framework directory to set the symlinks correctly.
pwd = os.getcwd()
os.chdir(framework)
# Set up the Current version.
self._Relink(version, os.path.join(VERSIONS, CURRENT))
# Set up the root symlinks.
self._Relink(os.path.join(VERSIONS, CURRENT, binary), binary)
self._Relink(os.path.join(VERSIONS, CURRENT, RESOURCES), RESOURCES)
# Back to where we were before!
os.chdir(pwd)
def _Relink(self, dest, link):
"""Creates a symlink to |dest| named |link|. If |link| already exists,
it is overwritten."""
if os.path.lexists(link):
os.remove(link)
os.symlink(dest, link)
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))

View File

@ -0,0 +1,12 @@
# This file is generated by gyp; do not edit.
TOOLSET := target
TARGET := validation
DEFS_Debug := \
'-D_DARWIN_USE_64_BIT_INODE=1' \
'-D_LARGEFILE_SOURCE' \
'-D_FILE_OFFSET_BITS=64' \
'-DBUILDING_NODE_EXTENSION' \
'-DDEBUG' \
'-D_DEBUG'

View File

@ -0,0 +1 @@
module.exports = require('./lib/websocket');

View File

@ -0,0 +1,31 @@
var spawn = require('child_process').spawn
, exec = require('child_process').exec
, fs = require('fs')
, version = JSON.parse(fs.readFileSync(__dirname + '/package.json', 'utf8')).version
, verbose = process.env['npm_package_config_verbose'] != null ? process.env['npm_package_config_verbose'] === 'true' : false;
console.log('[websocket v%s] Attempting to compile native extensions.', version);
var gyp = exec('node-gyp rebuild', {cwd: __dirname});
gyp.stdout.on('data', function(data) {
if (verbose) process.stdout.write(data);
});
gyp.stderr.on('data', function(data) {
if (verbose) process.stdout.write(data);
});
gyp.on('exit', function(code) {
if (code !== 0) {
console.log("[websocket v%s]", version);
console.log(' Native code compile failed!!');
console.log(' Please note that this module DOES NOT REQUIRE the native components');
console.log(' and will still work without them, though not quite as efficiently.');
console.log('');
console.log(' On Windows, native extensions require Visual Studio and Python.');
console.log(' On Unix, native extensions require Python, make and a C++ compiler.');
console.log(' Start npm with --websocket:verbose to show compilation output (if any).');
}
else {
console.log('[websocket v%s] Native extension compilation successful!', version);
}
process.exit();
});

View File

@ -0,0 +1,23 @@
/************************************************************************
* Copyright 2010-2011 Worlize Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***********************************************************************/
var extend = require('./utils').extend;
var util = require('util');
var Constants = {
"DEBUG" : false
};
module.exports = Constants;

View File

@ -0,0 +1,38 @@
/************************************************************************
* Copyright 2010-2011 Worlize Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***********************************************************************/
var extend = require('./utils').extend;
var util = require('util');
var Deprecation = {
disableWarnings: false,
deprecationWarningMap: {
websocketVersion:
"The websocketVersion property will be removed in a future version of WebSocket-Node. It was renamed to webSocketVersion for naming consistency.",
websocketVersionConfig:
"The websocketVersion config value will be removed in a future version of WebSocket-Node. It was renamed to webSocketVersion for naming consistency."
},
warn: function(deprecationName) {
if (!this.disableWarnings && this.deprecationWarningMap[deprecationName]) {
console.warn("DEPRECATION WARNING: " + this.deprecationWarningMap[deprecationName]);
this.deprecationWarningMap[deprecationName] = false;
}
}
};
module.exports = Deprecation;

View File

@ -0,0 +1,12 @@
/*!
* UTF-8 Validation Fallback Code originally from:
* ws: a node.js websocket client
* Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
* MIT Licensed
*/
module.exports.Validation = {
isValidUTF8: function(buffer) {
return true;
}
};

View File

@ -0,0 +1,18 @@
/*!
* UTF-8 Validation Code originally from:
* ws: a node.js websocket client
* Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
* MIT Licensed
*/
try {
module.exports = require('../build/Release/validation');
} catch (e) { try {
module.exports = require('../build/default/validation');
} catch (e) { try {
module.exports = require('./Validation.fallback');
console.warn("Warning: Native modules not compiled. UTF-8 validation disabled.")
} catch (e) {
console.error("validation.node seems not to have been built. Run npm install.")
throw e;
}}}

View File

@ -0,0 +1,359 @@
/************************************************************************
* Copyright 2010-2011 Worlize Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***********************************************************************/
var deprecation = require('./Deprecation');
var nodeVersion = process.version.slice(1).split('.').map(function(item) { return parseInt(item, 10); });
var isNode0_4_x = (nodeVersion[0] === 0 && nodeVersion[1] === 4);
var isGreaterThanNode0_4_x = (nodeVersion[0] > 0 || (nodeVersion[0] === 0 && nodeVersion[1] > 4));
var extend = require('./utils').extend;
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var http = require('http');
var https = require('https');
var url = require('url');
var crypto = require('crypto');
var WebSocketConnection = require('./WebSocketConnection');
const INIT = -1;
const CONNECTING = 0;
const OPEN = 1;
const CLOSING = 2;
const CLOSED = 3;
var ID_COUNTER = 0;
var protocolSeparators = [
"(", ")", "<", ">", "@",
",", ";", ":", "\\", "\"",
"/", "[", "]", "?", "=",
"{", "}", " ", String.fromCharCode(9)
];
function WebSocketClient(config) {
// TODO: Implement extensions
this.config = {
// 1MiB max frame size.
maxReceivedFrameSize: 0x100000,
// 8MiB max message size, only applicable if
// assembleFragments is true
maxReceivedMessageSize: 0x800000,
// Outgoing messages larger than fragmentationThreshold will be
// split into multiple fragments.
fragmentOutgoingMessages: true,
// Outgoing frames are fragmented if they exceed this threshold.
// Default is 16KiB
fragmentationThreshold: 0x4000,
// Which version of the protocol to use for this session. This
// option will be removed once the protocol is finalized by the IETF
// It is only available to ease the transition through the
// intermediate draft protocol versions.
// At present, it only affects the name of the Origin header.
webSocketVersion: 13,
// If true, fragmented messages will be automatically assembled
// and the full message will be emitted via a 'message' event.
// If false, each frame will be emitted via a 'frame' event and
// the application will be responsible for aggregating multiple
// fragmented frames. Single-frame messages will emit a 'message'
// event in addition to the 'frame' event.
// Most users will want to leave this set to 'true'
assembleFragments: true,
// The Nagle Algorithm makes more efficient use of network resources
// by introducing a small delay before sending small packets so that
// multiple messages can be batched together before going onto the
// wire. This however comes at the cost of latency, so the default
// is to disable it. If you don't need low latency and are streaming
// lots of small messages, you can change this to 'false'
disableNagleAlgorithm: true,
// The number of milliseconds to wait after sending a close frame
// for an acknowledgement to come back before giving up and just
// closing the socket.
closeTimeout: 5000,
// Options to pass to https.connect if connecting via TLS
tlsOptions: {}
};
if (config) {
extend(this.config, config);
}
if ('websocketVersion' in this.config) {
console.dir(this.config);
deprecation.warn('websocketVersionConfig');
this.config.webSocketVersion = this.config.websocketVersion;
}
Object.defineProperty(this.config, 'websocketVersion', {
set: function(value) {
deprecation.warn('websocketVersionConfig');
this.webSocketVersion = value;
},
get: function() {
deprecation.warn('websocketVersionConfig');
return this.webSocketVersion;
}
});
switch (this.config.webSocketVersion) {
case 8:
case 13:
break;
default:
throw new Error("Requested webSocketVersion is not supported. " +
"Allowed values are 8 and 13.");
}
this.readyState = INIT;
}
util.inherits(WebSocketClient, EventEmitter);
WebSocketClient.prototype.connect = function(requestUrl, protocols, origin, headers) {
var self = this;
if (typeof(protocols) === 'string') {
protocols = [protocols];
}
if (!(protocols instanceof Array)) {
protocols = [];
}
this.protocols = protocols;
this.origin = origin;
if (typeof(requestUrl) === 'string') {
this.url = url.parse(requestUrl);
}
else {
this.url = requestUrl; // in case an already parsed url is passed in.
}
if (!this.url.protocol) {
throw new Error("You must specify a full WebSocket URL, including protocol.");
}
if (!this.url.host) {
throw new Error("You must specify a full WebSocket URL, including hostname. Relative URLs are not supported.");
}
this.secure = (this.url.protocol === 'wss:');
// validate protocol characters:
this.protocols.forEach(function(protocol, index, array) {
for (var i=0; i < protocol.length; i ++) {
var charCode = protocol.charCodeAt(i);
var character = protocol.charAt(i);
if (charCode < 0x0021 || charCode > 0x007E || protocolSeparators.indexOf(character) !== -1) {
throw new Error("Protocol list contains invalid character '" + String.fromCharCode(charCode) + "'");
}
}
});
var defaultPorts = {
'ws:': '80',
'wss:': '443'
};
if (!this.url.port) {
this.url.port = defaultPorts[this.url.protocol];
}
var nonce = new Buffer(16);
for (var i=0; i < 16; i++) {
nonce[i] = Math.round(Math.random()*0xFF);
}
this.base64nonce = nonce.toString('base64');
var hostHeaderValue = this.url.hostname;
if ((this.url.protocol === 'ws:' && this.url.port !== '80') ||
(this.url.protocol === 'wss:' && this.url.port !== '443')) {
hostHeaderValue += (":" + this.url.port)
}
var reqHeaders = {};
extend(reqHeaders, headers || {});
extend(reqHeaders, {
'Upgrade': 'websocket',
'Connection': 'Upgrade',
'Sec-WebSocket-Version': this.config.webSocketVersion.toString(10),
'Sec-WebSocket-Key': this.base64nonce,
'Host': hostHeaderValue
});
if (this.protocols.length > 0) {
reqHeaders['Sec-WebSocket-Protocol'] = this.protocols.join(', ');
}
if (this.origin) {
if (this.config.webSocketVersion === 13) {
reqHeaders['Origin'] = this.origin;
}
else if (this.config.webSocketVersion === 8) {
reqHeaders['Sec-WebSocket-Origin'] = this.origin;
}
}
// TODO: Implement extensions
var pathAndQuery = this.url.pathname;
if (this.url.search) {
pathAndQuery += this.url.search;
}
function handleRequestError(error) {
self.emit('connectFailed', error);
}
if (isNode0_4_x) {
// Using old http.createClient interface since the new Agent-based API
// is buggy in Node 0.4.x.
if (this.secure) {
throw new Error("TLS connections are not supported under Node 0.4.x. Please use 0.6.2 or newer.");
}
var client = http.createClient(this.url.port, this.url.hostname);
client.on('error', handleRequestError);
client.on('upgrade', function handleClientUpgrade(response, socket, head) {
client.removeListener('error', handleRequestError);
self.socket = socket;
self.response = response;
self.firstDataChunk = head;
self.validateHandshake();
});
var req = client.request(pathAndQuery, reqHeaders);
}
else if (isGreaterThanNode0_4_x) {
var requestOptions = {
hostname: this.url.hostname,
port: this.url.port,
method: 'GET',
path: pathAndQuery,
headers: reqHeaders,
agent: false
};
if (this.secure) {
['key','passphrase','cert','ca'].forEach(function(key) {
if (self.config.tlsOptions.hasOwnProperty(key)) {
requestOptions[key] = self.config.tlsOptions[key];
}
});
var req = https.request(requestOptions);
}
else {
var req = http.request(requestOptions);
}
req.on('upgrade', function handleRequestUpgrade(response, socket, head) {
req.removeListener('error', handleRequestError);
self.socket = socket;
self.response = response;
self.firstDataChunk = head;
self.validateHandshake();
});
req.on('error', handleRequestError);
}
else {
throw new Error("Unsupported Node version " + process.version);
}
req.on('response', function(response) {
var headerDumpParts = [];
for (var headerName in response.headers) {
headerDumpParts.push(headerName + ": " + response.headers[headerName]);
}
self.failHandshake(
"Server responded with a non-101 status: " +
response.statusCode +
"\nResponse Headers Follow:\n" +
headerDumpParts.join('\n') + "\n"
);
});
req.end();
};
WebSocketClient.prototype.validateHandshake = function() {
var headers = this.response.headers;
if (this.protocols.length > 0) {
this.protocol = headers['sec-websocket-protocol'];
if (this.protocol) {
if (this.protocols.indexOf(this.protocol) === -1) {
this.failHandshake("Server did not respond with a requested protocol.");
return;
}
}
else {
this.failHandshake("Expected a Sec-WebSocket-Protocol header.");
return;
}
}
if (!(headers['connection'] && headers['connection'].toLocaleLowerCase() === 'upgrade')) {
this.failHandshake("Expected a Connection: Upgrade header from the server");
return;
}
if (!(headers['upgrade'] && headers['upgrade'].toLocaleLowerCase() === 'websocket')) {
this.failHandshake("Expected an Upgrade: websocket header from the server");
return;
}
var sha1 = crypto.createHash('sha1');
sha1.update(this.base64nonce + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
var expectedKey = sha1.digest('base64');
if (!headers['sec-websocket-accept']) {
this.failHandshake("Expected Sec-WebSocket-Accept header from server");
return;
}
if (!(headers['sec-websocket-accept'] === expectedKey)) {
this.failHandshake("Sec-WebSocket-Accept header from server didn't match expected value of " + expectedKey);
return;
}
// TODO: Support extensions
this.succeedHandshake();
};
WebSocketClient.prototype.failHandshake = function(errorDescription) {
if (this.socket && this.socket.writable) {
this.socket.end();
}
this.emit('connectFailed', errorDescription);
};
WebSocketClient.prototype.succeedHandshake = function() {
var connection = new WebSocketConnection(this.socket, [], this.protocol, true, this.config);
connection.webSocketVersion = this.config.webSocketVersion;
// Deprecated websocketVersion (proper casing...)
Object.defineProperty(connection, "websocketVersion", {
get: function() {
deprecation.warn('websocketVersion');
return this.webSocketVersion;
}
});
this.emit('connect', connection);
if (this.firstDataChunk.length > 0) {
connection.handleSocketData(this.firstDataChunk);
}
this.firstDataChunk = null;
};
module.exports = WebSocketClient;

View File

@ -0,0 +1,717 @@
/************************************************************************
* Copyright 2010-2011 Worlize Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***********************************************************************/
var crypto = require('crypto');
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var WebSocketFrame = require('./WebSocketFrame');
var BufferList = require('../vendor/FastBufferList');
var Constants = require('./Constants');
var Validation = require('./Validation').Validation;
const STATE_OPEN = "open";
const STATE_CLOSING = "closing";
const STATE_CLOSED = "closed";
function WebSocketConnection(socket, extensions, protocol, maskOutgoingPackets, config) {
this.config = config;
this.socket = socket;
this.protocol = protocol;
this.extensions = extensions;
this.remoteAddress = socket.remoteAddress;
this.closeReasonCode = -1;
this.closeDescription = null;
// We have to mask outgoing packets if we're acting as a WebSocket client.
this.maskOutgoingPackets = maskOutgoingPackets;
// We re-use the same buffers for the mask and frame header for all frames
// received on each connection to avoid a small memory allocation for each
// frame.
this.maskBytes = new Buffer(4);
this.frameHeader = new Buffer(10);
// the BufferList will handle the data streaming in
this.bufferList = new BufferList();
// Prepare for receiving first frame
this.currentFrame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config);
this.fragmentationSize = 0; // data received so far...
this.frameQueue = [];
// Various bits of connection state
this.connected = true;
this.state = STATE_OPEN;
this.waitingForCloseResponse = false;
this.closeTimeout = this.config.closeTimeout;
this.assembleFragments = this.config.assembleFragments;
this.maxReceivedMessageSize = this.config.maxReceivedMessageSize;
// The HTTP Client seems to subscribe to socket error events
// and re-dispatch them in such a way that doesn't make sense
// for users of our client, so we want to make sure nobody
// else is listening for error events on the socket besides us.
this.socket.removeAllListeners('error');
this.socket.on('error', this.handleSocketError.bind(this));
this.socket.on('data', this.handleSocketData.bind(this));
this.socket.on('end', this.handleSocketEnd.bind(this));
this.socket.on('close', this.handleSocketClose.bind(this));
this.socket.on('drain', this.handleSocketDrain.bind(this));
// Disable nagle algorithm?
this.socket.setNoDelay(this.config.disableNagleAlgorithm);
// Make sure there is no socket inactivity timeout
this.socket.setTimeout(0);
this.outgoingFrameQueue = [];
this.outputPaused = false;
this.outgoingFrameQueueHandler = this.processOutgoingFrameQueue.bind(this);
this.bytesWaitingToFlush = 0;
this._closeTimerHandler = this.handleCloseTimer.bind(this);
if (this.config.keepalive && !this.config.useNativeKeepalive) {
if (typeof(this.config.keepaliveInterval) !== 'number') {
throw new Error("keepaliveInterval must be specified and numeric " +
"if keepalive is true.");
}
this._keepaliveTimerHandler = this.handleKeepaliveTimer.bind(this);
this.setKeepaliveTimer();
if (this.config.dropConnectionOnKeepaliveTimeout) {
if (typeof(this.config.keepaliveGracePeriod) !== 'number') {
throw new Error("keepaliveGracePeriod must be specified and " +
"numeric if dropConnectionOnKeepaliveTimeout " +
"is true.")
}
this._gracePeriodTimerHandler = this.handleGracePeriodTimer.bind(this);
}
}
else if (this.config.keepalive && this.config.useNativeKeepalive) {
if (!('setKeepAlive' in this.socket)) {
throw new Error("Unable to use native keepalive: unsupported by " +
"this version of Node.");
}
this.socket.setKeepAlive(true, this.config.keepaliveInterval);
}
}
WebSocketConnection.CLOSE_REASON_NORMAL = 1000;
WebSocketConnection.CLOSE_REASON_GOING_AWAY = 1001;
WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR = 1002;
WebSocketConnection.CLOSE_REASON_UNPROCESSABLE_INPUT = 1003;
WebSocketConnection.CLOSE_REASON_RESERVED = 1004; // Reserved value. Undefined meaning.
WebSocketConnection.CLOSE_REASON_NOT_PROVIDED = 1005; // Not to be used on the wire
WebSocketConnection.CLOSE_REASON_ABNORMAL = 1006; // Not to be used on the wire
WebSocketConnection.CLOSE_REASON_INVALID_DATA = 1007;
WebSocketConnection.CLOSE_REASON_POLICY_VIOLATION = 1008;
WebSocketConnection.CLOSE_REASON_MESSAGE_TOO_BIG = 1009;
WebSocketConnection.CLOSE_REASON_EXTENSION_REQUIRED = 1010;
WebSocketConnection.CLOSE_REASON_INTERNAL_SERVER_ERROR = 1011;
WebSocketConnection.CLOSE_REASON_TLS_HANDSHAKE_FAILED = 1015; // Not to be used on the wire
WebSocketConnection.CLOSE_DESCRIPTIONS = {
1000: "Normal connection closure",
1001: "Remote peer is going away",
1002: "Protocol error",
1003: "Unprocessable input",
1004: "Reserved",
1005: "Reason not provided",
1006: "Abnormal closure, no further detail available",
1007: "Invalid data received",
1008: "Policy violation",
1009: "Message too big",
1010: "Extension requested by client is required",
1011: "Internal Server Error",
1015: "TLS Handshake Failed"
};
function validateReceivedCloseReason(code) {
if (code < 1000) {
// Status codes in the range 0-999 are not used
return false;
}
if (code >= 1000 && code <= 2999) {
// Codes from 1000 - 2999 are reserved for use by the protocol. Only
// a few codes are defined, all others are currently illegal.
return [1000, 1001, 1002, 1003, 1007, 1008, 1009, 1010, 1011].indexOf(code) !== -1
}
if (code >= 3000 && code <= 3999) {
// Reserved for use by libraries, frameworks, and applications.
// Should be registered with IANA. Interpretation of these codes is
// undefined by the WebSocket protocol.
return true;
}
if (code >= 4000 && code <= 4999) {
// Reserved for private use. Interpretation of these codes is
// undefined by the WebSocket protocol.
return true;
}
if (code >= 5000) {
return false;
}
}
util.inherits(WebSocketConnection, EventEmitter);
// set or reset the keepalive timer when data is received.
WebSocketConnection.prototype.setKeepaliveTimer = function() {
if (!this.config.keepalive) { return; }
if (this._keepaliveTimeoutID) {
clearTimeout(this._keepaliveTimeoutID);
}
if (this._gracePeriodTimeoutID) {
clearTimeout(this._gracePeriodTimeoutID);
}
this._keepaliveTimeoutID = setTimeout(this._keepaliveTimerHandler, this.config.keepaliveInterval);
};
// No data has been received within config.keepaliveTimeout ms.
WebSocketConnection.prototype.handleKeepaliveTimer = function() {
this._keepaliveTimeoutID = null;
this.ping();
// If we are configured to drop connections if the client doesn't respond
// then set the grace period timer.
if (this.config.dropConnectionOnKeepaliveTimeout) {
this.setGracePeriodTimer();
}
else {
// Otherwise reset the keepalive timer to send the next ping.
this.setKeepaliveTimer();
}
};
WebSocketConnection.prototype.setGracePeriodTimer = function() {
if (this._gracePeriodTimeoutID) {
clearTimeout(this._gracePeriodTimeoutID);
}
this._gracePeriodTimeoutID = setTimeout(this._gracePeriodTimerHandler, this.config.keepaliveGracePeriod);
};
WebSocketConnection.prototype.handleGracePeriodTimer = function() {
// If this is called, the client has not responded and is assumed dead.
this._gracePeriodTimeoutID = null;
this.drop(WebSocketConnection.CLOSE_REASON_ABNORMAL, "Peer not responding.", true);
};
WebSocketConnection.prototype.handleSocketData = function(data) {
// Reset the keepalive timer when receiving data of any kind.
this.setKeepaliveTimer();
// Add received data to our bufferList, which efficiently holds received
// data chunks in a linked list of Buffer objects.
this.bufferList.write(data);
// currentFrame.addData returns true if all data necessary to parse
// the frame was available. It returns false if we are waiting for
// more data to come in on the wire.
while (this.connected && this.currentFrame.addData(this.bufferList)) {
// Handle possible parsing errors
if (this.currentFrame.protocolError) {
// Something bad happened.. get rid of this client.
this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR, this.currentFrame.dropReason);
return;
}
else if (this.currentFrame.frameTooLarge) {
this.drop(WebSocketConnection.CLOSE_REASON_MESSAGE_TOO_BIG, this.currentFrame.dropReason);
return;
}
// For now since we don't support extensions, all RSV bits are illegal
if (this.currentFrame.rsv1 || this.currentFrame.rsv2 || this.currentFrame.rsv3) {
this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR,
"Unsupported usage of rsv bits without negotiated extension.");
}
if (!this.assembleFragments) {
this.emit('frame', this.currentFrame);
}
this.processFrame(this.currentFrame);
this.currentFrame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config);
}
};
WebSocketConnection.prototype.handleSocketError = function(error) {
// console.log((new Date()) + " - Socket Error - Closing Connection: " + error);
if (this.listeners('error').length > 0) {
this.emit('error', error);
}
this.socket.end();
};
WebSocketConnection.prototype.handleSocketEnd = function() {
// console.log((new Date()) + " - Socket End");
this.socket.end();
this.frameQueue = null;
this.outgoingFrameQueue = [];
this.fragmentationSize = 0;
this.bufferList = null;
};
WebSocketConnection.prototype.handleSocketClose = function(hadError) {
// console.log((new Date()) + " - Socket Close");
this.socketHadError = hadError;
this.connected = false;
this.state = STATE_CLOSED;
// If closeReasonCode is still set to -1 at this point then we must
// not have received a close frame!!
if (this.closeReasonCode === -1) {
this.closeReasonCode = WebSocketConnection.CLOSE_REASON_ABNORMAL;
this.closeDescription = "Connection dropped by remote peer.";
}
if (!this.closeEventEmitted) {
this.closeEventEmitted = true;
// console.log((new Date()) + " - Emitting WebSocketConnection close event");
this.emit('close', this.closeReasonCode, this.closeDescription);
}
this.clearCloseTimer();
if (this._keepaliveTimeoutID) {
clearTimeout(this._keepaliveTimeoutID);
}
};
WebSocketConnection.prototype.handleSocketDrain = function() {
this.outputPaused = false;
this.processOutgoingFrameQueue();
};
WebSocketConnection.prototype.close = function() {
// console.log((new Date()) + " - Initating clean WebSocket close sequence.");
if (this.connected) {
this.closeReasonCode = WebSocketConnection.CLOSE_REASON_NORMAL;
this.closeDescription = WebSocketConnection.CLOSE_DESCRIPTIONS[this.closeReasonCode];
this.setCloseTimer();
this.sendCloseFrame();
this.state = STATE_CLOSING;
this.connected = false;
}
};
WebSocketConnection.prototype.drop = function(reasonCode, description, skipCloseFrame) {
if (typeof(reasonCode) !== 'number') {
reasonCode = WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR;
}
var logText = "WebSocket: Dropping Connection. Code: " + reasonCode.toString(10);
if (typeof(description) !== 'string') {
// If no description is provided, try to look one up based on the
// specified reasonCode.
description = WebSocketConnection.CLOSE_DESCRIPTIONS[reasonCode];
}
if (description) {
logText += (" - " + description);
}
// console.error((new Date()) + " " + logText);
this.closeReasonCode = reasonCode;
this.closeDescription = description;
this.outgoingFrameQueue = [];
this.frameQueue = [];
this.fragmentationSize = 0;
if (!skipCloseFrame) {
this.sendCloseFrame(reasonCode, description, true);
}
this.connected = false;
this.state = STATE_CLOSED;
this.closeEventEmitted = true;
this.emit('close', reasonCode, description);
this.socket.destroy();
};
WebSocketConnection.prototype.setCloseTimer = function() {
this.clearCloseTimer();
// console.log((new Date()) + " - Setting close timer");
this.waitingForCloseResponse = true;
this.closeTimer = setTimeout(this._closeTimerHandler, this.closeTimeout);
};
WebSocketConnection.prototype.clearCloseTimer = function() {
if (this.closeTimer) {
// console.log((new Date()) + " - Clearing close timer");
clearTimeout(this.closeTimer);
this.waitingForCloseResponse = false;
this.closeTimer = null;
}
};
WebSocketConnection.prototype.handleCloseTimer = function() {
this.closeTimer = null;
if (this.waitingForCloseResponse) {
// console.log((new Date()) + " - Close response not received from client. Forcing socket end.");
this.waitingForCloseResponse = false;
this.socket.end();
}
};
WebSocketConnection.prototype.processFrame = function(frame) {
var i;
var message;
// Any non-control opcode besides 0x00 (continuation) received in the
// middle of a fragmented message is illegal.
if (this.frameQueue.length !== 0 && (frame.opcode > 0x00 && frame.opcode < 0x08)) {
this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR,
"Illegal frame opcode 0x" + frame.opcode.toString(16) + " " +
"received in middle of fragmented message.");
return;
}
switch(frame.opcode) {
case 0x02: // WebSocketFrame.BINARY_FRAME
if (this.assembleFragments) {
if (frame.fin) {
// Complete single-frame message received
this.emit('message', {
type: 'binary',
binaryData: frame.binaryPayload
});
}
else {
// beginning of a fragmented message
this.frameQueue.push(frame);
this.fragmentationSize = frame.length;
}
}
break;
case 0x01: // WebSocketFrame.TEXT_FRAME
if (this.assembleFragments) {
if (frame.fin) {
if (!Validation.isValidUTF8(frame.binaryPayload)) {
this.drop(WebSocketConnection.CLOSE_REASON_INVALID_DATA,
"Invalid UTF-8 Data Received");
return;
}
// Complete single-frame message received
this.emit('message', {
type: 'utf8',
utf8Data: frame.binaryPayload.toString('utf8')
});
}
else {
// beginning of a fragmented message
this.frameQueue.push(frame);
this.fragmentationSize = frame.length;
}
}
break;
case 0x00: // WebSocketFrame.CONTINUATION
if (this.assembleFragments) {
if (this.frameQueue.length === 0) {
this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR,
"Unexpected Continuation Frame");
return;
}
this.fragmentationSize += frame.length;
if (this.fragmentationSize > this.maxReceivedMessageSize) {
this.drop(WebSocketConnection.CLOSE_REASON_MESSAGE_TOO_BIG,
"Maximum message size exceeded.");
return;
}
this.frameQueue.push(frame);
if (frame.fin) {
// end of fragmented message, so we process the whole
// message now. We also have to decode the utf-8 data
// for text frames after combining all the fragments.
var bytesCopied = 0;
var binaryPayload = new Buffer(this.fragmentationSize);
this.frameQueue.forEach(function (currentFrame) {
currentFrame.binaryPayload.copy(binaryPayload, bytesCopied);
bytesCopied += currentFrame.binaryPayload.length;
});
switch (this.frameQueue[0].opcode) {
case 0x02: // WebSocketOpcode.BINARY_FRAME
this.emit('message', {
type: 'binary',
binaryData: binaryPayload
});
break;
case 0x01: // WebSocketOpcode.TEXT_FRAME
if (!Validation.isValidUTF8(binaryPayload)) {
this.drop(WebSocketConnection.CLOSE_REASON_INVALID_DATA,
"Invalid UTF-8 Data Received");
return;
}
this.emit('message', {
type: 'utf8',
utf8Data: binaryPayload.toString('utf8')
});
break;
default:
this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR,
"Unexpected first opcode in fragmentation sequence: 0x" + this.frameQueue[0].opcode.toString(16));
return;
}
this.frameQueue = [];
this.fragmentationSize = 0;
}
}
break;
case 0x09: // WebSocketFrame.PING
this.pong(frame.binaryPayload);
break;
case 0x0A: // WebSocketFrame.PONG
break;
case 0x08: // WebSocketFrame.CONNECTION_CLOSE
// console.log((new Date()) + " - Received close frame");
if (this.waitingForCloseResponse) {
// Got response to our request to close the connection.
// Close is complete, so we just hang up.
// console.log((new Date()) + " - Got close response from peer.");
this.clearCloseTimer();
this.waitingForCloseResponse = false;
this.state = STATE_CLOSED;
this.socket.end();
}
else {
// Got request from other party to close connection.
// Send back acknowledgement and then hang up.
this.state = STATE_CLOSING;
var respondCloseReasonCode;
// Make sure the close reason provided is legal according to
// the protocol spec. Providing no close status is legal.
// WebSocketFrame sets closeStatus to -1 by default, so if it
// is still -1, then no status was provided.
if (frame.invalidCloseFrameLength) {
this.closeReasonCode = 1005; // 1005 = No reason provided.
respondCloseReasonCode = WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR;
}
else if (frame.closeStatus === -1 || validateReceivedCloseReason(frame.closeStatus)) {
this.closeReasonCode = frame.closeStatus;
respondCloseReasonCode = WebSocketConnection.CLOSE_REASON_NORMAL;
}
else {
this.closeReasonCode = frame.closeStatus;
respondCloseReasonCode = WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR;
}
// If there is a textual description in the close frame, extract it.
if (frame.binaryPayload.length > 1) {
if (!Validation.isValidUTF8(frame.binaryPayload)) {
this.drop(WebSocketConnection.CLOSE_REASON_INVALID_DATA,
"Invalid UTF-8 Data Received");
return;
}
this.closeDescription = frame.binaryPayload.toString('utf8');
}
else {
this.closeDescription = WebSocketConnection.CLOSE_DESCRIPTIONS[this.closeReasonCode];
}
// console.log((new Date()) + " Remote peer " + this.remoteAddress +
// " requested disconnect, code: " + this.closeReasonCode + " - " + this.closeDescription +
// " - close frame payload length: " + frame.length);
this.sendCloseFrame(respondCloseReasonCode);
this.socket.end();
this.connected = false;
}
break;
default:
this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR,
"Unrecognized Opcode: 0x" + frame.opcode.toString(16));
break;
}
};
WebSocketConnection.prototype.send = function(data, cb) {
if (Buffer.isBuffer(data)) {
this.sendBytes(data, cb);
}
else if (typeof(data['toString']) === 'function') {
this.sendUTF(data, cb);
}
else {
throw new Error("Data provided must either be a Node Buffer or implement toString()")
}
};
WebSocketConnection.prototype.sendUTF = function(data, cb) {
var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config);
frame.opcode = 0x01; // WebSocketOpcode.TEXT_FRAME
frame.binaryPayload = new Buffer(data.toString(), 'utf8');
this.fragmentAndSend(frame, cb);
};
WebSocketConnection.prototype.sendBytes = function(data, cb) {
if (!Buffer.isBuffer(data)) {
throw new Error("You must pass a Node Buffer object to WebSocketConnection.prototype.sendBytes()");
}
var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config);
frame.opcode = 0x02; // WebSocketOpcode.BINARY_FRAME
frame.binaryPayload = data;
this.fragmentAndSend(frame, cb);
};
WebSocketConnection.prototype.ping = function(data) {
var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config);
frame.opcode = 0x09; // WebSocketOpcode.PING
frame.fin = true;
if (data) {
if (!Buffer.isBuffer(data)) {
data = new Buffer(data.toString(), 'utf8')
}
if (data.length > 125) {
// console.warn("WebSocket: Data for ping is longer than 125 bytes. Truncating.");
data = data.slice(0,124);
}
frame.binaryPayload = data;
}
this.sendFrame(frame);
};
// Pong frames have to echo back the contents of the data portion of the
// ping frame exactly, byte for byte.
WebSocketConnection.prototype.pong = function(binaryPayload) {
var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config);
frame.opcode = 0x0A; // WebSocketOpcode.PONG
if (Buffer.isBuffer(binaryPayload) && binaryPayload.length > 125) {
// console.warn("WebSocket: Data for pong is longer than 125 bytes. Truncating.");
binaryPayload = binaryPayload.slice(0,124);
}
frame.binaryPayload = binaryPayload;
frame.fin = true;
this.sendFrame(frame);
};
WebSocketConnection.prototype.fragmentAndSend = function(frame, cb) {
if (frame.opcode > 0x07) {
throw new Error("You cannot fragment control frames.");
}
var threshold = this.config.fragmentationThreshold;
var length = frame.binaryPayload.length;
if (this.config.fragmentOutgoingMessages && frame.binaryPayload && length > threshold) {
var numFragments = Math.ceil(length / threshold);
var sentFragments = 0;
var sentCallback = function (err) {
if (err) {
if (typeof cb === 'function') {
// pass only the first error
cb(err);
cb = null;
}
return;
}
++sentFragments;
if ((typeof cb === 'function') && (sentFragments === numFragments)) {
cb();
}
}
for (var i=1; i <= numFragments; i++) {
var currentFrame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config);
// continuation opcode except for first frame.
currentFrame.opcode = (i === 1) ? frame.opcode : 0x00;
// fin set on last frame only
currentFrame.fin = (i === numFragments);
// length is likely to be shorter on the last fragment
var currentLength = (i === numFragments) ? length - (threshold * (i-1)) : threshold;
var sliceStart = threshold * (i-1);
// Slice the right portion of the original payload
currentFrame.binaryPayload = frame.binaryPayload.slice(sliceStart, sliceStart + currentLength);
this.sendFrame(currentFrame, sentCallback);
}
}
else {
frame.fin = true;
this.sendFrame(frame, cb);
}
};
WebSocketConnection.prototype.sendCloseFrame = function(reasonCode, reasonText, force) {
if (typeof(reasonCode) !== 'number') {
reasonCode = WebSocketConnection.CLOSE_REASON_NORMAL;
}
var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config);
frame.fin = true;
frame.opcode = 0x08; // WebSocketOpcode.CONNECTION_CLOSE
frame.closeStatus = reasonCode;
if (typeof(reasonText) === 'string') {
frame.binaryPayload = new Buffer(reasonText, 'utf8');
}
this.sendFrame(frame, force);
};
WebSocketConnection.prototype.sendFrame = function(frame, force, cb) {
if (typeof force === 'function') {
cb = force;
force = false;
}
frame.mask = this.maskOutgoingPackets;
var buffer = frame.toBuffer();
this.outgoingFrameQueue.unshift([buffer, cb]);
this.bytesWaitingToFlush += buffer.length;
if (!this.outputPaused || force) {
this.processOutgoingFrameQueue();
}
};
WebSocketConnection.prototype.processOutgoingFrameQueue = function() {
if (this.outputPaused) { return; }
if (this.outgoingFrameQueue.length > 0) {
var current = this.outgoingFrameQueue.pop();
var buffer = current[0];
var cb = current[1];
// there is no need to accumulate messages in the queue if connection closed
// connection will not be restored and messages will never be sent
// therefore, notify callbacks about it
if (!this.connected && (typeof cb === 'function')) {
cb("Connection closed");
return;
}
try {
var flushed = this.socket.write(buffer, cb);
}
catch(e) {
if (typeof cb === 'function') {
cb(e.toString());
}
if (this.listeners('error').length > 0) {
this.emit("error", "Error while writing to socket: " + e.toString());
}
else {
if (Constants.DEBUG) {
console.warn("Error while writing to socket: " + e.toString());
}
}
return;
}
this.bytesWaitingToFlush -= buffer.length;
if (!flushed) {
this.outputPaused = true;
return;
}
process.nextTick(this.outgoingFrameQueueHandler);
}
};
module.exports = WebSocketConnection;

View File

@ -0,0 +1,282 @@
/************************************************************************
* Copyright 2010-2011 Worlize Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***********************************************************************/
var ctio = require('../vendor/node-ctype/ctio-faster');
var xor = require('./xor').xor;
const DECODE_HEADER = 1;
const WAITING_FOR_16_BIT_LENGTH = 2;
const WAITING_FOR_64_BIT_LENGTH = 3;
const WAITING_FOR_MASK_KEY = 4;
const WAITING_FOR_PAYLOAD = 5;
const COMPLETE = 6;
// WebSocketConnection will pass shared buffer objects for maskBytes and
// frameHeader into the constructor to avoid tons of small memory allocations
// for each frame we have to parse. This is only used for parsing frames
// we receive off the wire.
function WebSocketFrame(maskBytes, frameHeader, config) {
this.maskBytes = maskBytes;
this.frameHeader = frameHeader;
this.config = config;
this.maxReceivedFrameSize = config.maxReceivedFrameSize;
this.protocolError = false;
this.frameTooLarge = false;
this.invalidCloseFrameLength = false;
this.parseState = DECODE_HEADER;
this.closeStatus = -1;
}
WebSocketFrame.prototype.addData = function(bufferList, fragmentationType) {
var temp;
if (this.parseState === DECODE_HEADER) {
if (bufferList.length >= 2) {
bufferList.joinInto(this.frameHeader, 0, 0, 2);
bufferList.advance(2);
var firstByte = this.frameHeader[0];
var secondByte = this.frameHeader[1];
this.fin = Boolean(firstByte & 0x80);
this.rsv1 = Boolean(firstByte & 0x40);
this.rsv2 = Boolean(firstByte & 0x20);
this.rsv3 = Boolean(firstByte & 0x10);
this.mask = Boolean(secondByte & 0x80);
this.opcode = firstByte & 0x0F;
this.length = secondByte & 0x7F;
// Control frame sanity check
if (this.opcode >= 0x08) {
if (this.length > 125) {
this.protocolError = true;
this.dropReason = "Illegal control frame longer than 125 bytes.";
return true;
}
if (!this.fin) {
this.protocolError = true;
this.dropReason = "Control frames must not be fragmented.";
return true;
}
}
if (this.length === 126) {
this.parseState = WAITING_FOR_16_BIT_LENGTH;
}
else if (this.length === 127) {
this.parseState = WAITING_FOR_64_BIT_LENGTH;
}
else {
this.parseState = WAITING_FOR_MASK_KEY;
}
}
}
if (this.parseState === WAITING_FOR_16_BIT_LENGTH) {
if (bufferList.length >= 2) {
bufferList.joinInto(this.frameHeader, 2, 0, 2);
bufferList.advance(2);
this.length = ctio.ruint16(this.frameHeader, 'big', 2);
this.parseState = WAITING_FOR_MASK_KEY;
}
}
else if (this.parseState === WAITING_FOR_64_BIT_LENGTH) {
if (bufferList.length >= 8) {
bufferList.joinInto(this.frameHeader, 2, 0, 8);
bufferList.advance(8);
var lengthPair = ctio.ruint64(this.frameHeader, 'big', 2);
if (lengthPair[0] !== 0) {
this.protocolError = true;
this.dropReason = "Unsupported 64-bit length frame received";
return true;
}
this.length = lengthPair[1];
this.parseState = WAITING_FOR_MASK_KEY;
}
}
if (this.parseState === WAITING_FOR_MASK_KEY) {
if (this.mask) {
if (bufferList.length >= 4) {
bufferList.joinInto(this.maskBytes, 0, 0, 4);
bufferList.advance(4);
this.parseState = WAITING_FOR_PAYLOAD;
}
}
else {
this.parseState = WAITING_FOR_PAYLOAD;
}
}
if (this.parseState === WAITING_FOR_PAYLOAD) {
if (this.length > this.maxReceivedFrameSize) {
this.frameTooLarge = true;
this.dropReason = "Frame size of " + this.length.toString(10) +
" bytes exceeds maximum accepted frame size";
return true;
}
if (this.length === 0) {
this.binaryPayload = new Buffer(0);
this.parseState = COMPLETE;
return true;
}
if (bufferList.length >= this.length) {
this.binaryPayload = bufferList.take(this.length);
bufferList.advance(this.length);
if (this.mask) {
xor(this.binaryPayload, this.maskBytes, 0);
}
if (this.opcode === 0x08) { // WebSocketOpcode.CONNECTION_CLOSE
if (this.length === 1) {
// Invalid length for a close frame. Must be zero or at least two.
this.binaryPayload = new Buffer(0);
this.invalidCloseFrameLength = true;
}
if (this.length >= 2) {
this.closeStatus = ctio.ruint16(this.binaryPayload, 'big', 0);
this.binaryPayload = this.binaryPayload.slice(2);
}
}
this.parseState = COMPLETE;
return true;
}
}
return false;
};
WebSocketFrame.prototype.throwAwayPayload = function(bufferList) {
if (bufferList.length >= this.length) {
bufferList.advance(this.length);
this.parseState = COMPLETE;
return true;
}
return false;
};
WebSocketFrame.prototype.toBuffer = function(nullMask) {
var maskKey;
var headerLength = 2;
var data;
var outputPos;
var firstByte = 0x00;
var secondByte = 0x00;
if (this.fin) {
firstByte |= 0x80;
}
if (this.rsv1) {
firstByte |= 0x40;
}
if (this.rsv2) {
firstByte |= 0x20;
}
if (this.rsv3) {
firstByte |= 0x10;
}
if (this.mask) {
secondByte |= 0x80;
}
firstByte |= (this.opcode & 0x0F);
// the close frame is a special case because the close reason is
// prepended to the payload data.
if (this.opcode === 0x08) {
this.length = 2;
if (this.binaryPayload) {
this.length += this.binaryPayload.length;
}
data = new Buffer(this.length);
ctio.wuint16(this.closeStatus, 'big', data, 0);
if (this.length > 2) {
this.binaryPayload.copy(data, 2);
}
}
else if (this.binaryPayload) {
data = this.binaryPayload;
this.length = data.length;
}
else {
this.length = 0;
}
if (this.length <= 125) {
// encode the length directly into the two-byte frame header
secondByte |= (this.length & 0x7F);
}
else if (this.length > 125 && this.length <= 0xFFFF) {
// Use 16-bit length
secondByte |= 126;
headerLength += 2;
}
else if (this.length > 0xFFFF) {
// Use 64-bit length
secondByte |= 127;
headerLength += 8;
}
var output = new Buffer(this.length + headerLength + (this.mask ? 4 : 0));
// write the frame header
output[0] = firstByte;
output[1] = secondByte;
outputPos = 2;
if (this.length > 125 && this.length <= 0xFFFF) {
// write 16-bit length
ctio.wuint16(this.length, 'big', output, outputPos);
outputPos += 2;
}
else if (this.length > 0xFFFF) {
// write 64-bit length
ctio.wuint64([0x00000000, this.length], 'big', output, outputPos);
outputPos += 8;
}
if (this.length > 0) {
if (this.mask) {
if (!nullMask) {
// Generate a mask key
maskKey = parseInt(Math.random()*0xFFFFFFFF);
}
else {
maskKey = 0x00000000;
}
ctio.wuint32(maskKey, 'big', this.maskBytes, 0);
// write the mask key
this.maskBytes.copy(output, outputPos);
outputPos += 4;
data.copy(output, outputPos);
xor(output.slice(outputPos), this.maskBytes, 0);
}
else {
data.copy(output, outputPos);
}
}
return output;
};
WebSocketFrame.prototype.toString = function() {
return "Opcode: " + this.opcode + ", fin: " + this.fin + ", length: " + this.length + ", hasPayload: " + Boolean(this.binaryPayload) + ", masked: " + this.mask;
};
module.exports = WebSocketFrame;

View File

@ -0,0 +1,478 @@
/************************************************************************
* Copyright 2010-2011 Worlize Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***********************************************************************/
var deprecation = require('./Deprecation');
var crypto = require('crypto');
var util = require('util');
var url = require('url');
var EventEmitter = require('events').EventEmitter;
var WebSocketConnection = require('./WebSocketConnection');
var Constants = require('./Constants');
var headerValueSplitRegExp = /,\s*/;
var headerParamSplitRegExp = /;\s*/;
var headerSanitizeRegExp = /[\r\n]/g;
var separators = [
"(", ")", "<", ">", "@",
",", ";", ":", "\\", "\"",
"/", "[", "]", "?", "=",
"{", "}", " ", String.fromCharCode(9)
];
var controlChars = [String.fromCharCode(127) /* DEL */];
for (var i=0; i < 31; i ++) {
/* US-ASCII Control Characters */
controlChars.push(String.fromCharCode(i));
}
var cookieNameValidateRegEx = /([\x00-\x20\x22\x28\x29\x2c\x2f\x3a-\x3f\x40\x5b-\x5e\x7b\x7d\x7f])/;
var cookieValueValidateRegEx = /[^\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]/;
var cookieValueDQuoteValidateRegEx = /^"[^"]*"$/;
var controlCharsAndSemicolonRegEx = /[\x00-\x20\x3b]/g;
var cookieSeparatorRegEx = /[;,] */;
var httpStatusDescriptions = {
100: "Continue",
101: "Switching Protocols",
200: "OK",
201: "Created",
203: "Non-Authoritative Information",
204: "No Content",
205: "Reset Content",
206: "Partial Content",
300: "Multiple Choices",
301: "Moved Permanently",
302: "Found",
303: "See Other",
304: "Not Modified",
305: "Use Proxy",
307: "Temporary Redirect",
400: "Bad Request",
401: "Unauthorized",
402: "Payment Required",
403: "Forbidden",
404: "Not Found",
406: "Not Acceptable",
407: "Proxy Authorization Required",
408: "Request Timeout",
409: "Conflict",
410: "Gone",
411: "Length Required",
412: "Precondition Failed",
413: "Request Entity Too Long",
414: "Request-URI Too Long",
415: "Unsupported Media Type",
416: "Requested Range Not Satisfiable",
417: "Expectation Failed",
426: "Upgrade Required",
500: "Internal Server Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Timeout",
505: "HTTP Version Not Supported"
};
function WebSocketRequest(socket, httpRequest, serverConfig) {
this.socket = socket;
this.httpRequest = httpRequest;
this.resource = httpRequest.url;
this.remoteAddress = socket.remoteAddress;
this.serverConfig = serverConfig;
}
util.inherits(WebSocketRequest, EventEmitter);
WebSocketRequest.prototype.readHandshake = function() {
var self = this;
var request = this.httpRequest;
// Decode URL
this.resourceURL = url.parse(this.resource, true);
this.host = request.headers['host'];
if (!this.host) {
throw new Error("Client must provide a Host header.");
}
this.key = request.headers['sec-websocket-key'];
if (!this.key) {
throw new Error("Client must provide a value for Sec-WebSocket-Key.");
}
this.webSocketVersion = parseInt(request.headers['sec-websocket-version'], 10);
// Deprecated websocketVersion (proper casing...)
Object.defineProperty(this, "websocketVersion", {
get: function() {
deprecation.warn('websocketVersion');
return this.webSocketVersion;
}
});
if (!this.webSocketVersion || isNaN(this.webSocketVersion)) {
throw new Error("Client must provide a value for Sec-WebSocket-Version.");
}
switch (this.webSocketVersion) {
case 8:
case 13:
break;
default:
var e = new Error("Unsupported websocket client version: " + this.webSocketVersion +
"Only versions 8 and 13 are supported.");
e.httpCode = 426;
e.headers = {
"Sec-WebSocket-Version": "13"
};
throw e;
}
if (this.webSocketVersion === 13) {
this.origin = request.headers['origin'];
}
else if (this.webSocketVersion === 8) {
this.origin = request.headers['sec-websocket-origin'];
}
// Protocol is optional.
var protocolString = request.headers['sec-websocket-protocol'];
this.protocolFullCaseMap = {};
this.requestedProtocols = [];
if (protocolString) {
var requestedProtocolsFullCase = protocolString.split(headerValueSplitRegExp);
requestedProtocolsFullCase.forEach(function(protocol) {
var lcProtocol = protocol.toLocaleLowerCase();
self.requestedProtocols.push(lcProtocol);
self.protocolFullCaseMap[lcProtocol] = protocol;
});
}
if (request.headers['x-forwarded-for']) {
this.remoteAddress = request.headers['x-forwarded-for'].split(', ')[0];
}
// Extensions are optional.
var extensionsString = request.headers['sec-websocket-extensions'];
this.requestedExtensions = this.parseExtensions(extensionsString);
// Cookies are optional
var cookieString = request.headers['cookie'];
this.cookies = this.parseCookies(cookieString);
};
WebSocketRequest.prototype.parseExtensions = function(extensionsString) {
if (!extensionsString || extensionsString.length === 0) {
return [];
}
extensions = extensionsString.toLocaleLowerCase().split(headerValueSplitRegExp);
extensions.forEach(function(extension, index, array) {
var params = extension.split(headerParamSplitRegExp);
var extensionName = params[0];
var extensionParams = params.slice(1);
extensionParams.forEach(function(rawParam, index, array) {
var arr = rawParam.split('=');
var obj = {
name: arr[0],
value: arr[1]
};
array.splice(index, 1, obj);
});
var obj = {
name: extensionName,
params: extensionParams
};
array.splice(index, 1, obj);
});
return extensions;
};
// This function adapted from node-cookie
// https://github.com/shtylman/node-cookie
WebSocketRequest.prototype.parseCookies = function(str) {
// Sanity Check
if (!str || typeof(str) !== 'string') {
return [];
}
var cookies = [];
var pairs = str.split(cookieSeparatorRegEx);
pairs.forEach(function(pair) {
var eq_idx = pair.indexOf('=');
if (eq_idx === -1) {
cookies.push({
name: pair,
value: null
});
return;
}
var key = pair.substr(0, eq_idx).trim();
var val = pair.substr(++eq_idx, pair.length).trim();
// quoted values
if ('"' == val[0]) {
val = val.slice(1, -1);
}
cookies.push({
name: key,
value: decodeURIComponent(val)
});
});
return cookies;
};
WebSocketRequest.prototype.accept = function(acceptedProtocol, allowedOrigin, cookies) {
// TODO: Handle extensions
if (acceptedProtocol) {
var protocolFullCase = this.protocolFullCaseMap[acceptedProtocol.toLocaleLowerCase()];
if (typeof(protocolFullCase) === 'undefined') {
protocolFullCase = acceptedProtocol;
}
}
else {
protocolFullCase = acceptedProtocol;
}
this.protocolFullCaseMap = null;
var connection = new WebSocketConnection(this.socket, [], acceptedProtocol, false, this.serverConfig);
connection.webSocketVersion = this.webSocketVersion;
// Deprecated websocketVersion (proper casing...)
Object.defineProperty(connection, "websocketVersion", {
get: function() {
deprecation.warn('websocketVersion');
return this.webSocketVersion;
}
});
connection.remoteAddress = this.remoteAddress;
// Create key validation hash
var sha1 = crypto.createHash('sha1');
sha1.update(this.key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
var acceptKey = sha1.digest('base64');
var response = "HTTP/1.1 101 Switching Protocols\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Accept: " + acceptKey + "\r\n";
if (protocolFullCase) {
// validate protocol
for (var i=0; i < protocolFullCase.length; i++) {
var charCode = protocolFullCase.charCodeAt(i);
var character = protocolFullCase.charAt(i);
if (charCode < 0x21 || charCode > 0x7E || separators.indexOf(character) !== -1) {
this.reject(500);
throw new Error("Illegal character '" + String.fromCharCode(character) + "' in subprotocol.");
}
}
if (this.requestedProtocols.indexOf(acceptedProtocol) === -1) {
this.reject(500);
throw new Error("Specified protocol was not requested by the client.");
}
protocolFullCase = protocolFullCase.replace(headerSanitizeRegExp, '');
response += "Sec-WebSocket-Protocol: " + protocolFullCase + "\r\n";
}
this.requestedProtocols = null;
if (allowedOrigin) {
allowedOrigin = allowedOrigin.replace(headerSanitizeRegExp, '');
if (this.webSocketVersion === 13) {
response += "Origin: " + allowedOrigin + "\r\n";
}
else if (this.webSocketVersion === 8) {
response += "Sec-WebSocket-Origin: " + allowedOrigin + "\r\n";
}
}
if (cookies) {
if (!Array.isArray(cookies)) {
this.reject(500);
throw new Error("Value supplied for 'cookies' argument must be an array.");
}
var seenCookies = {};
cookies.forEach(function(cookie) {
if (!cookie.name || !cookie.value) {
this.reject(500);
throw new Error("Each cookie to set must at least provide a 'name' and 'value'");
}
// Make sure there are no \r\n sequences inserted
cookie.name = cookie.name.replace(controlCharsAndSemicolonRegEx, '');
cookie.value = cookie.value.replace(controlCharsAndSemicolonRegEx, '');
if (seenCookies[cookie.name]) {
this.reject(500);
throw new Error("You may not specify the same cookie name twice.");
}
seenCookies[cookie.name] = true;
// token (RFC 2616, Section 2.2)
var invalidChar = cookie.name.match(cookieNameValidateRegEx);
if (invalidChar) {
this.reject(500);
throw new Error("Illegal character " + invalidChar[0] + " in cookie name");
}
// RFC 6265, Section 4.1.1
// *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) | %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
if (cookie.value.match(cookieValueDQuoteValidateRegEx)) {
invalidChar = cookie.value.slice(1, -1).match(cookieValueValidateRegEx);
} else {
invalidChar = cookie.value.match(cookieValueValidateRegEx);
}
if (invalidChar) {
this.reject(500);
throw new Error("Illegal character " + invalidChar[0] + " in cookie value");
}
var cookieParts = [cookie.name + "=" + cookie.value];
// RFC 6265, Section 4.1.1
// "Path=" path-value | <any CHAR except CTLs or ";">
if(cookie.path){
invalidChar = cookie.path.match(controlCharsAndSemicolonRegEx);
if (invalidChar) {
this.reject(500);
throw new Error("Illegal character " + invalidChar[0] + " in cookie path");
}
cookieParts.push("Path=" + cookie.path);
}
// RFC 6265, Section 4.1.2.3
// "Domain=" subdomain
if (cookie.domain) {
if (typeof(cookie.domain) !== 'string') {
this.reject(500);
throw new Error("Domain must be specified and must be a string.");
}
var domain = cookie.domain.toLowerCase();
invalidChar = cookie.domain.match(controlCharsAndSemicolonRegEx);
if (invalidChar) {
this.reject(500);
throw new Error("Illegal character " + invalidChar[0] + " in cookie domain");
}
cookieParts.push("Domain=" + cookie.domain.toLowerCase());
}
// RFC 6265, Section 4.1.1
//"Expires=" sane-cookie-date | Force Date object requirement by using only epoch
if (cookie.expires) {
if (!(cookie.expires instanceof Date)){
this.reject(500);
throw new Error("Value supplied for cookie 'expires' must be a vaild date object");
}
cookieParts.push("Expires=" + cookie.expires.toGMTString());
}
// RFC 6265, Section 4.1.1
//"Max-Age=" non-zero-digit *DIGIT
if (cookie.maxage) {
var maxage = cookie.maxage;
if (typeof(maxage) === 'string') {
maxage = parseInt(maxage, 10);
}
if (isNaN(maxage) || maxage <= 0 ) {
this.reject(500);
throw new Error("Value supplied for cookie 'maxage' must be a non-zero number");
}
maxage = Math.round(maxage);
cookieParts.push("Max-Age=" + maxage.toString(10));
}
// RFC 6265, Section 4.1.1
//"Secure;"
if (cookie.secure) {
if (typeof(cookie.secure) !== "boolean") {
this.reject(500);
throw new Error("Value supplied for cookie 'secure' must be of type boolean");
}
cookieParts.push("Secure");
}
// RFC 6265, Section 4.1.1
//"HttpOnly;"
if (cookie.httponly) {
if (typeof(cookie.httponly) !== "boolean") {
this.reject(500);
throw new Error("Value supplied for cookie 'httponly' must be of type boolean");
}
cookieParts.push("HttpOnly");
}
response += ("Set-Cookie: " + cookieParts.join(';') + "\r\n");
}.bind(this));
}
// TODO: handle negotiated extensions
// if (negotiatedExtensions) {
// response += "Sec-WebSocket-Extensions: " + negotiatedExtensions.join(", ") + "\r\n";
// }
response += "\r\n";
try {
this.socket.write(response, 'ascii');
}
catch(e) {
if (Constants.DEBUG) {
console.log("Error Writing to Socket: " + e.toString());
}
// Since we have to return a connection object even if the socket is
// already dead in order not to break the API, we schedule a 'close'
// event on the connection object to occur immediately.
process.nextTick(function() {
// WebSocketConnection.CLOSE_REASON_ABNORMAL = 1006
// Third param: Skip sending the close frame to a dead socket
connection.drop(1006, "TCP connection lost before handshake completed.", true);
});
}
this.emit('requestAccepted', connection);
return connection;
};
WebSocketRequest.prototype.reject = function(status, reason, extraHeaders) {
if (typeof(status) !== 'number') {
status = 403;
}
var response = "HTTP/1.1 " + status + " " + httpStatusDescriptions[status] + "\r\n" +
"Connection: close\r\n";
if (reason) {
reason = reason.replace(headerSanitizeRegExp, '');
response += "X-WebSocket-Reject-Reason: " + reason + "\r\n";
}
if (extraHeaders) {
for (var key in extraHeaders) {
var sanitizedValue = extraHeaders[key].toString().replace(headerSanitizeRegExp, '');
var sanitizedKey = key.replace(headerSanitizeRegExp, '');
response += (sanitizedKey + ": " + sanitizedValue + "\r\n");
}
}
response += "\r\n";
this.socket.end(response, 'ascii');
this.emit('requestRejected', this);
};
module.exports = WebSocketRequest;

View File

@ -0,0 +1,154 @@
/************************************************************************
* Copyright 2010-2011 Worlize Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***********************************************************************/
var extend = require('./utils').extend;
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var WebSocketRouterRequest = require('./WebSocketRouterRequest');
function WebSocketRouter(config) {
this.config = {
// The WebSocketServer instance to attach to.
server: null
};
if (config) {
extend(this.config, config);
}
if (this.config.server) {
this.attachServer(this.config.server);
}
this.handlers = [];
this._requestHandler = this.handleRequest.bind(this);
}
util.inherits(WebSocketRouter, EventEmitter);
WebSocketRouter.prototype.attachServer = function(server) {
if (server) {
this.server = server;
this.server.on('request', this._requestHandler);
}
else {
throw new Error("You must specify a WebSocketServer instance to attach to.");
}
};
WebSocketRouter.prototype.detachServer = function() {
if (this.server) {
this.server.removeListener('request', this._requestHandler);
this.server = null;
}
else {
throw new Error("Cannot detach from server: not attached.")
}
};
WebSocketRouter.prototype.mount = function(path, protocol, callback) {
if (!path) {
throw new Error("You must specify a path for this handler.");
}
if (!protocol) {
protocol = "____no_protocol____";
}
if (!callback) {
throw new Error("You must specify a callback for this handler.");
}
path = this.pathToRegExp(path);
if (!(path instanceof RegExp)) {
throw new Error("Path must be specified as either a string or a RegExp.");
}
var pathString = path.toString();
// normalize protocol to lower-case
protocol = protocol.toLocaleLowerCase();
if (this.findHandlerIndex(pathString, protocol) !== -1) {
throw new Error("You may only mount one handler per path/protocol combination.");
}
this.handlers.push({
'path': path,
'pathString': pathString,
'protocol': protocol,
'callback': callback
});
};
WebSocketRouter.prototype.unmount = function(path, protocol) {
var index = this.findHandlerIndex(this.pathToRegExp(path).toString(), protocol);
if (index !== -1) {
this.handlers.splice(index, 1);
}
else {
throw new Error("Unable to find a route matching the specified path and protocol.");
}
};
WebSocketRouter.prototype.findHandlerIndex = function(pathString, protocol) {
protocol = protocol.toLocaleLowerCase();
for (var i=0, len=this.handlers.length; i < len; i++) {
var handler = this.handlers[i];
if (handler.pathString === pathString && handler.protocol === protocol) {
return i;
}
}
return -1;
};
WebSocketRouter.prototype.pathToRegExp = function(path) {
if (typeof(path) === 'string') {
if (path === '*') {
path = /^.*$/;
}
else {
path = path.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
path = new RegExp('^' + path + '$');
}
}
return path;
};
WebSocketRouter.prototype.handleRequest = function(request) {
var requestedProtocols = request.requestedProtocols;
if (requestedProtocols.length === 0) {
requestedProtocols = ['____no_protocol____'];
}
// Find a handler with the first requested protocol first
for (var i=0; i < requestedProtocols.length; i++) {
var requestedProtocol = requestedProtocols[i].toLocaleLowerCase();
// find the first handler that can process this request
for (var j=0, len=this.handlers.length; j < len; j++) {
var handler = this.handlers[j];
if (handler.path.test(request.resourceURL.pathname)) {
if (requestedProtocol === handler.protocol ||
handler.protocol === '*')
{
var routerRequest = new WebSocketRouterRequest(request, requestedProtocol);
handler.callback(routerRequest);
return;
}
}
}
}
// If we get here we were unable to find a suitable handler.
request.reject(404, "No handler is available for the given request.");
};
module.exports = WebSocketRouter;

View File

@ -0,0 +1,59 @@
/************************************************************************
* Copyright 2010-2011 Worlize Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***********************************************************************/
var deprecation = require('./Deprecation');
var util = require('util');
var EventEmitter = require('events').EventEmitter;
function WebSocketRouterRequest(webSocketRequest, resolvedProtocol) {
this.webSocketRequest = webSocketRequest;
if (resolvedProtocol === '____no_protocol____') {
this.protocol = null;
}
else {
this.protocol = resolvedProtocol;
}
this.origin = webSocketRequest.origin;
this.resource = webSocketRequest.resource;
this.resourceURL = webSocketRequest.resourceURL;
this.httpRequest = webSocketRequest.httpRequest;
this.remoteAddress = webSocketRequest.remoteAddress;
this.webSocketVersion = webSocketRequest.webSocketVersion;
// Deprecated websocketVersion (proper casing...)
Object.defineProperty(this, "websocketVersion", {
get: function() {
deprecation.warn('websocketVersion');
return this.webSocketVersion;
}
});
this.requestedExtensions = webSocketRequest.requestedExtensions;
this.cookies = webSocketRequest.cookies;
}
util.inherits(WebSocketRouterRequest, EventEmitter);
WebSocketRouterRequest.prototype.accept = function(origin, cookies) {
var connection = this.webSocketRequest.accept(this.protocol, origin, cookies);
this.emit('requestAccepted', connection);
return connection;
};
WebSocketRouterRequest.prototype.reject = function(status, reason, extraHeaders) {
this.webSocketRequest.reject(status, reason, extraHeaders);
this.emit('requestRejected', this);
};
module.exports = WebSocketRouterRequest;

View File

@ -0,0 +1,216 @@
/************************************************************************
* Copyright 2010-2011 Worlize Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***********************************************************************/
var extend = require('./utils').extend;
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var WebSocketRequest = require('./WebSocketRequest');
var Constants = require('./Constants');
var WebSocketServer = function WebSocketServer(config) {
this._handlers = {
upgrade: this.handleUpgrade.bind(this),
requestAccepted: this.handleRequestAccepted.bind(this)
};
this.connections = [];
if (config) {
this.mount(config);
}
};
util.inherits(WebSocketServer, EventEmitter);
WebSocketServer.prototype.mount = function(config) {
this.config = {
// The http server instance to attach to. Required.
httpServer: null,
// 64KiB max frame size.
maxReceivedFrameSize: 0x10000,
// 1MiB max message size, only applicable if
// assembleFragments is true
maxReceivedMessageSize: 0x100000,
// Outgoing messages larger than fragmentationThreshold will be
// split into multiple fragments.
fragmentOutgoingMessages: true,
// Outgoing frames are fragmented if they exceed this threshold.
// Default is 16KiB
fragmentationThreshold: 0x4000,
// If true, the server will automatically send a ping to all
// clients every 'keepaliveInterval' milliseconds. The timer is
// reset on any received data from the client.
keepalive: true,
// The interval to send keepalive pings to connected clients if the
// connection is idle. Any received data will reset the counter.
keepaliveInterval: 20000,
// If true, the server will consider any connection that has not
// received any data within the amount of time specified by
// 'keepaliveGracePeriod' after a keepalive ping has been sent to
// be dead, and will drop the connection.
// Ignored if keepalive is false.
dropConnectionOnKeepaliveTimeout: true,
// The amount of time to wait after sending a keepalive ping before
// closing the connection if the connected peer does not respond.
// Ignored if keepalive is false.
keepaliveGracePeriod: 10000,
// Whether to use native TCP keep-alive instead of WebSockets ping
// and pong packets. Native TCP keep-alive sends smaller packets
// on the wire and so uses bandwidth more efficiently. This may
// be more important when talking to mobile devices.
// If this value is set to true, then these values will be ignored:
// keepaliveGracePeriod
// dropConnectionOnKeepaliveTimeout
useNativeKeepalive: false,
// If true, fragmented messages will be automatically assembled
// and the full message will be emitted via a 'message' event.
// If false, each frame will be emitted via a 'frame' event and
// the application will be responsible for aggregating multiple
// fragmented frames. Single-frame messages will emit a 'message'
// event in addition to the 'frame' event.
// Most users will want to leave this set to 'true'
assembleFragments: true,
// If this is true, websocket connections will be accepted
// regardless of the path and protocol specified by the client.
// The protocol accepted will be the first that was requested
// by the client. Clients from any origin will be accepted.
// This should only be used in the simplest of cases. You should
// probably leave this set to 'false' and inspect the request
// object to make sure it's acceptable before accepting it.
autoAcceptConnections: false,
// The Nagle Algorithm makes more efficient use of network resources
// by introducing a small delay before sending small packets so that
// multiple messages can be batched together before going onto the
// wire. This however comes at the cost of latency, so the default
// is to disable it. If you don't need low latency and are streaming
// lots of small messages, you can change this to 'false'
disableNagleAlgorithm: true,
// The number of milliseconds to wait after sending a close frame
// for an acknowledgement to come back before giving up and just
// closing the socket.
closeTimeout: 5000
};
extend(this.config, config);
// this.httpServer = httpServer;
// if (typeof(pathRegExp) === 'string') {
// pathRegExp = new RegExp('^' + pathRegExp + '$');
// }
// this.pathRegExp = pathRegExp;
// this.protocol = protocol;
if (this.config.httpServer) {
this.config.httpServer.on('upgrade', this._handlers.upgrade);
}
else {
throw new Error("You must specify an httpServer on which to mount the WebSocket server.")
}
};
WebSocketServer.prototype.unmount = function() {
this.config.httpServer.removeListener('upgrade', this._handlers.upgrade);
};
WebSocketServer.prototype.closeAllConnections = function() {
this.connections.forEach(function(connection) {
connection.close();
});
};
WebSocketServer.prototype.broadcast = function(data) {
if (Buffer.isBuffer(data)) {
this.broadcastBytes(data);
}
else if (typeof(data.toString) === 'function') {
this.broadcastUTF(data);
}
};
WebSocketServer.prototype.broadcastUTF = function(utfData) {
this.connections.forEach(function(connection) {
connection.sendUTF(utfData);
});
};
WebSocketServer.prototype.broadcastBytes = function(binaryData) {
this.connections.forEach(function(connection) {
connection.sendBytes(binaryData);
});
};
WebSocketServer.prototype.shutDown = function() {
this.unmount();
this.closeAllConnections();
};
WebSocketServer.prototype.handleUpgrade = function(request, socket, head) {
var wsRequest = new WebSocketRequest(socket, request, this.config);
try {
wsRequest.readHandshake();
}
catch(e) {
wsRequest.reject(
e.httpCode ? e.httpCode : 400,
e.message,
e.headers
);
if (Constants.DEBUG) {
console.error((new Date()) + " WebSocket: Invalid handshake: " + e.message);
}
return;
}
wsRequest.once('requestAccepted', this._handlers.requestAccepted);
if (!this.config.autoAcceptConnections && this.listeners('request').length > 0) {
this.emit('request', wsRequest);
}
else if (this.config.autoAcceptConnections) {
wsRequest.accept(wsRequest.requestedProtocols[0], wsRequest.origin);
}
else {
wsRequest.reject(404, "No handler is configured to accept the connection.");
}
};
WebSocketServer.prototype.handleRequestAccepted = function(connection) {
var self = this;
connection.once('close', function(closeReason, description) {
self.handleConnectionClose(connection, closeReason, description);
});
this.connections.push(connection);
this.emit('connect', connection);
};
WebSocketServer.prototype.handleConnectionClose = function(connection, closeReason, description) {
var index = this.connections.indexOf(connection);
if (index !== -1) {
this.connections.splice(index, 1);
}
this.emit('close', connection, closeReason, description);
};
module.exports = WebSocketServer;

View File

@ -0,0 +1,7 @@
module.exports = {
extend: function extend(dest, source) {
for (var prop in source) {
dest[prop] = source[prop];
}
}
};

View File

@ -0,0 +1,11 @@
module.exports = {
"server" : require('./WebSocketServer'),
"client" : require('./WebSocketClient'),
"router" : require('./WebSocketRouter'),
"frame" : require('./WebSocketFrame'),
"request" : require('./WebSocketRequest'),
"connection" : require('./WebSocketConnection'),
"constants" : require('./Constants'),
"deprecation": require('./Deprecation'),
"version" : "1.0.8"
};

View File

@ -0,0 +1,13 @@
module.exports = {
xor: function (payload, maskBytes, maskPos) {
var end = payload.length;
if (typeof(maskPos) !== 'number') {
maskPos = 0;
}
for (var i=0; i < end; i++) {
payload[i] = payload[i] ^ maskBytes[maskPos];
maskPos = (maskPos + 1) & 3;
}
return maskPos;
}
};

View File

@ -0,0 +1,18 @@
/*
* Buffer xor module
* Copyright (c) Agora S.A.
* Licensed under the MIT License.
* Version: 1.0
*/
try {
module.exports = require('../build/Release/xor');
} catch (e) { try {
module.exports = require('../build/default/xor');
} catch(e) { try {
module.exports = require('./xor.fallback');
console.warn("Warning: Native modules not compiled. XOR performance will be degraded.")
} catch (e) {
console.error("xor.node seems not to have been built. Run npm install.")
throw e;
}}}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,144 @@
/*!
* UTF-8 Validation Code originally from:
* ws: a node.js websocket client
* Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
* MIT Licensed
*/
#include <v8.h>
#include <node.h>
#include <node_buffer.h>
#include <node_object_wrap.h>
#include <stdlib.h>
#include <wchar.h>
#include <stdio.h>
using namespace v8;
using namespace node;
#define UNI_SUR_HIGH_START (uint32_t) 0xD800
#define UNI_SUR_LOW_END (uint32_t) 0xDFFF
#define UNI_REPLACEMENT_CHAR (uint32_t) 0x0000FFFD
#define UNI_MAX_LEGAL_UTF32 (uint32_t) 0x0010FFFF
static const uint8_t trailingBytesForUTF8[256] = {
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 3,3,3,3,3,3,3,3,4,4,4,4,5,5,5,5
};
static const uint32_t offsetsFromUTF8[6] = {
0x00000000, 0x00003080, 0x000E2080,
0x03C82080, 0xFA082080, 0x82082080
};
static int isLegalUTF8(const uint8_t *source, const int length)
{
uint8_t a;
const uint8_t *srcptr = source+length;
switch (length) {
default: return 0;
/* Everything else falls through when "true"... */
/* RFC3629 makes 5 & 6 bytes UTF-8 illegal
case 6: if ((a = (*--srcptr)) < 0x80 || a > 0xBF) return 0;
case 5: if ((a = (*--srcptr)) < 0x80 || a > 0xBF) return 0; */
case 4: if ((a = (*--srcptr)) < 0x80 || a > 0xBF) return 0;
case 3: if ((a = (*--srcptr)) < 0x80 || a > 0xBF) return 0;
case 2: if ((a = (*--srcptr)) > 0xBF) return 0;
switch (*source) {
/* no fall-through in this inner switch */
case 0xE0: if (a < 0xA0) return 0; break;
case 0xED: if (a > 0x9F) return 0; break;
case 0xF0: if (a < 0x90) return 0; break;
case 0xF4: if (a > 0x8F) return 0; break;
default: if (a < 0x80) return 0;
}
case 1: if (*source >= 0x80 && *source < 0xC2) return 0;
}
if (*source > 0xF4) return 0;
return 1;
}
int is_valid_utf8 (size_t len, char *value)
{
/* is the string valid UTF-8? */
for (size_t i = 0; i < len; i++) {
uint32_t ch = 0;
uint8_t extrabytes = trailingBytesForUTF8[(uint8_t) value[i]];
if (extrabytes + i >= len)
return 0;
if (isLegalUTF8 ((uint8_t *) (value + i), extrabytes + 1) == 0) return 0;
switch (extrabytes) {
case 5 : ch += (uint8_t) value[i++]; ch <<= 6;
case 4 : ch += (uint8_t) value[i++]; ch <<= 6;
case 3 : ch += (uint8_t) value[i++]; ch <<= 6;
case 2 : ch += (uint8_t) value[i++]; ch <<= 6;
case 1 : ch += (uint8_t) value[i++]; ch <<= 6;
case 0 : ch += (uint8_t) value[i];
}
ch -= offsetsFromUTF8[extrabytes];
if (ch <= UNI_MAX_LEGAL_UTF32) {
if (ch >= UNI_SUR_HIGH_START && ch <= UNI_SUR_LOW_END)
return 0;
} else {
return 0;
}
}
return 1;
}
class Validation : public ObjectWrap
{
public:
static void Initialize(v8::Handle<v8::Object> target)
{
HandleScope scope;
Local<FunctionTemplate> t = FunctionTemplate::New(New);
t->InstanceTemplate()->SetInternalFieldCount(1);
NODE_SET_METHOD(t->GetFunction(), "isValidUTF8", Validation::IsValidUTF8);
target->Set(String::NewSymbol("Validation"), t->GetFunction());
}
protected:
static Handle<Value> New(const Arguments& args)
{
HandleScope scope;
Validation* validation = new Validation();
validation->Wrap(args.This());
return args.This();
}
static Handle<Value> IsValidUTF8(const Arguments& args)
{
HandleScope scope;
if (!Buffer::HasInstance(args[0])) {
return ThrowException(Exception::Error(String::New("First argument needs to be a buffer")));
}
Local<Object> buffer_obj = args[0]->ToObject();
char *buffer_data = Buffer::Data(buffer_obj);
size_t buffer_length = Buffer::Length(buffer_obj);
return is_valid_utf8(buffer_length, buffer_data) == 1 ? scope.Close(True()) : scope.Close(False());
}
};
extern "C" void init (Handle<Object> target)
{
HandleScope scope;
Validation::Initialize(target);
}
NODE_MODULE(validation, init);

View File

@ -0,0 +1,86 @@
/*!
* Buffer xor module
* Copyright (c) Agora S.A.
* Licensed under the MIT License.
* Version: 1.0
*/
#include <v8.h>
#include <node_buffer.h>
#include <cstring>
using namespace node;
using namespace v8;
namespace {
#define XOR_BUFFER_THROW_EXCEPTION(name, msg) { \
static Persistent<String> name = Persistent<String>::New(String::New(msg)); \
return ThrowException(Exception::TypeError(name)); }
static Handle<Value> xorBuffer(const Arguments &args) {
if (args.Length() < 2) {
XOR_BUFFER_THROW_EXCEPTION(illegalArgumentCountException, "Expected 2 arguments")
}
if (!Buffer::HasInstance(args[0])) {
XOR_BUFFER_THROW_EXCEPTION(illegalFirstArgumentException, "First argument must be a Buffer.")
}
Handle<Object> payload = args[0]->ToObject();
if (!Buffer::HasInstance(args[1])) {
XOR_BUFFER_THROW_EXCEPTION(illegalArgumentException, "Second argument must be a Buffer.")
}
Handle<Object> mask = args[1]->ToObject();
size_t maskSize = Buffer::Length(mask);
if (maskSize != 4) {
XOR_BUFFER_THROW_EXCEPTION(illegalStringArgumentException, "Second argument must be a 4 byte Buffer.")
}
size_t maskOffset = 0;
if (args.Length() == 3) {
if (!args[2]->IsUint32()) {
XOR_BUFFER_THROW_EXCEPTION(illegalThirdArgumentException, "Third argument must be an unsigned number.")
}
maskOffset = args[2]->ToUint32()->Uint32Value();
}
if (maskOffset > 3) {
XOR_BUFFER_THROW_EXCEPTION(illegalStringArgumentException, "Third argument must be less than 4.")
}
size_t payloadLength = Buffer::Length(payload);
uint8_t* payloadData = (uint8_t*) Buffer::Data(payload);
uint8_t* maskData = (uint8_t*) Buffer::Data(mask);
uint8_t rotatedMask[4];
memcpy(rotatedMask, maskData + maskOffset, 4 - maskOffset);
if (maskOffset > 0) {
memcpy(rotatedMask + 4 - maskOffset, maskData, maskOffset);
}
uint32_t* pos32 = (uint32_t*) payloadData;
uint32_t* end32 = pos32 + (payloadLength >> 2);
uint32_t* mask32 = (uint32_t*) rotatedMask;
while (pos32 < end32) {
*(pos32++) ^= *mask32;
}
uint8_t* pos8 = (uint8_t*)pos32;
uint8_t* end8 = payloadData + payloadLength;
uint8_t* mask8 = rotatedMask;
while (pos8 < end8) {
*(pos8++) ^= *(mask8++);
}
return Integer::NewFromUnsigned((mask8 - rotatedMask + maskOffset) & 3);
}
void RegisterModule(Handle<Object> target) {
NODE_SET_METHOD(target, "xor", xorBuffer);
}
}
NODE_MODULE(xor, RegisterModule);

View File

@ -0,0 +1,192 @@
// This file was copied from https://github.com/substack/node-bufferlist
// and modified to be able to copy bytes from the bufferlist directly into
// a pre-existing fixed-size buffer without an additional memory allocation.
// bufferlist.js
// Treat a linked list of buffers as a single variable-size buffer.
var Buffer = require('buffer').Buffer;
var EventEmitter = require('events').EventEmitter;
module.exports = BufferList;
module.exports.BufferList = BufferList; // backwards compatibility
function BufferList(opts) {
if (!(this instanceof BufferList)) return new BufferList(opts);
EventEmitter.call(this);
var self = this;
if (typeof(opts) == 'undefined') opts = {};
// default encoding to use for take(). Leaving as 'undefined'
// makes take() return a Buffer instead.
self.encoding = opts.encoding;
// constructor to use for Buffer-esque operations
self.construct = opts.construct || Buffer;
var head = { next : null, buffer : null };
var last = { next : null, buffer : null };
// length can get negative when advanced past the end
// and this is the desired behavior
var length = 0;
self.__defineGetter__('length', function () {
return length;
});
// keep an offset of the head to decide when to head = head.next
var offset = 0;
// Write to the bufferlist. Emits 'write'. Always returns true.
self.write = function (buf) {
if (!head.buffer) {
head.buffer = buf;
last = head;
}
else {
last.next = { next : null, buffer : buf };
last = last.next;
}
length += buf.length;
self.emit('write', buf);
return true;
};
self.end = function (buf) {
if (Buffer.isBuffer(buf)) self.write(buf);
};
// Push buffers to the end of the linked list. (deprecated)
// Return this (self).
self.push = function () {
var args = [].concat.apply([], arguments);
args.forEach(self.write);
return self;
};
// For each buffer, perform some action.
// If fn's result is a true value, cut out early.
// Returns this (self).
self.forEach = function (fn) {
if (!head.buffer) return new self.construct(0);
if (head.buffer.length - offset <= 0) return self;
var firstBuf = head.buffer.slice(offset);
var b = { buffer : firstBuf, next : head.next };
while (b && b.buffer) {
var r = fn(b.buffer);
if (r) break;
b = b.next;
}
return self;
};
// Create a single Buffer out of all the chunks or some subset specified by
// start and one-past the end (like slice) in bytes.
self.join = function (start, end) {
if (!head.buffer) return new self.construct(0);
if (start == undefined) start = 0;
if (end == undefined) end = self.length;
var big = new self.construct(end - start);
var ix = 0;
self.forEach(function (buffer) {
if (start < (ix + buffer.length) && ix < end) {
// at least partially contained in the range
buffer.copy(
big,
Math.max(0, ix - start),
Math.max(0, start - ix),
Math.min(buffer.length, end - ix)
);
}
ix += buffer.length;
if (ix > end) return true; // stop processing past end
});
return big;
};
self.joinInto = function (targetBuffer, targetStart, sourceStart, sourceEnd) {
if (!head.buffer) return new self.construct(0);
if (sourceStart == undefined) sourceStart = 0;
if (sourceEnd == undefined) sourceEnd = self.length;
var big = targetBuffer;
if (big.length - targetStart < sourceEnd - sourceStart) {
throw new Error("Insufficient space available in target Buffer.");
}
var ix = 0;
self.forEach(function (buffer) {
if (sourceStart < (ix + buffer.length) && ix < sourceEnd) {
// at least partially contained in the range
buffer.copy(
big,
Math.max(targetStart, targetStart + ix - sourceStart),
Math.max(0, sourceStart - ix),
Math.min(buffer.length, sourceEnd - ix)
);
}
ix += buffer.length;
if (ix > sourceEnd) return true; // stop processing past end
});
return big;
};
// Advance the buffer stream by n bytes.
// If n the aggregate advance offset passes the end of the buffer list,
// operations such as .take() will return empty strings until enough data is
// pushed.
// Returns this (self).
self.advance = function (n) {
offset += n;
length -= n;
while (head.buffer && offset >= head.buffer.length) {
offset -= head.buffer.length;
head = head.next
? head.next
: { buffer : null, next : null }
;
}
self.emit('advance', n);
return self;
};
// Take n bytes from the start of the buffers.
// Returns a string.
// If there are less than n bytes in all the buffers or n is undefined,
// returns the entire concatenated buffer string.
self.take = function (n, encoding) {
if (n == undefined) n = self.length;
else if (typeof n !== 'number') {
encoding = n;
n = self.length;
}
var b = head;
if (!encoding) encoding = self.encoding;
if (encoding) {
var acc = '';
self.forEach(function (buffer) {
if (n <= 0) return true;
acc += buffer.toString(
encoding, 0, Math.min(n,buffer.length)
);
n -= buffer.length;
});
return acc;
} else {
// If no 'encoding' is specified, then return a Buffer.
return self.join(0, n);
}
};
// The entire concatenated buffer as a string.
self.toString = function () {
return self.take('binary');
};
}
require('util').inherits(BufferList, EventEmitter);

View File

@ -0,0 +1,18 @@
Copyright 2011, Robert Mustacchi. All rights reserved.
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.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,50 @@
var count = 0;
var clients = {};
var http = require('http');
var server = http.createServer(function(request, response) {});
server.listen(1234, function() {
console.log((new Date()) + ' Server is listening on port 1234');
});
var WebSocketServer = require('websocket').server;
wsServer = new WebSocketServer({
httpServer: server
});
wsServer.on('request', function(r){
var connection = r.accept('echo-protocol', r.origin);
// Specific id for this client & increment count
var id = count++;
// // Store the connection method so we can loop through & contact all clients
clients[id] = connection
console.log((new Date()) + ' Connection accepted [' + id + ']');
// Create event listener
connection.on('message', function(message) {
// The string message that was sent to us
var msgString = message.utf8Data;
// Loop through all clients
for(var i in clients){
// Send a message to the client with the message
clients[i].sendUTF(msgString);
}
});
// Create event listener for close
connection.on('close', function(reasonCode, description) {
delete clients[id];
console.log((new Date()) + ' Peer ' + connection.remoteAddress + ' disconnected.');
});
});

View File

@ -0,0 +1,31 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Web socket test</title>
<script type="text/javascript">
var ws = new WebSocket('ws://localhost:1234', 'echo-protocol');
function sendMessage(){
var message = document.getElementById('message').value;
ws.send(message);
}
ws.addEventListener("message", function(e) {
// The data is simply the message that we're sending back
var msg = e.data;
// Append the message
document.getElementById('chatlog').innerHTML += '<br>' + msg;
});
</script>
</head>
<body>
<div>Send data:</div>
<textarea id='message'>This is the sent text</textarea>
<input type='button' value='send message' onclick='sendMessage()'/>
<div>Received from server:</div>
<div id='chatlog'></div>
</body>
</html>