Initial commit

This commit is contained in:
Developer
2025-04-21 16:03:20 +02:00
commit 2832896157
22874 changed files with 3092801 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
dist/*
build/*
tests/html/lib/sockjs.js
tests/html/static/*

View File

@@ -0,0 +1,14 @@
tests/
dist/
!dist/sockjs.js
!dist/sockjs.js.map
!dist/sockjs.min.js
!dist/sockjs.min.js.map
build/
.travis.yml
.zuul.yml
.eslintrc
.jscsrc
bower.json
gulpfile.js
Makefile

View File

@@ -0,0 +1 @@
6.14.2

View File

@@ -0,0 +1,9 @@
Parts of the code are derived from various open source projects.
For code derived from Socket.IO by Guillermo Rauch see
https://github.com/LearnBoost/socket.io/tree/0.6.17#readme.
Snippets derived from jQuery-JSONP by Julian Aubourg, generic MIT
license.
All other code is released on MIT license, see LICENSE.

View File

@@ -0,0 +1,290 @@
1.1.4
==
* Upgrade `debug` and fix object key literal mangling, fixes regression in Opera 11.10 #359
* Trim descriptions in package.json and bower.json - #372
1.1.3
==
* Bad publish to NPM (removed)
1.1.2
==
* Ensure both sender and receiver are cleaned upon close - #342
* Remove event listeners before calling `close` - #344
* Update documentation links - #351, #339, #316
* Explicitly export `undefined` when `WebSocket` does not exist. Fixes Webpack. #321
* Include `dist` folder on npm - #265
* Simplify build setup
* Update to Node.js 6.9
* Add sourcemap for minified version
* Remove unused String.trim shim
1.1.1
==
* Do not pass `protocols` or `options` arguments to browser WebSocket constructor - #309
1.1.0
==
* Fix IE7/8 usage of `console.log` which does not have `apply` - #279
* Remove `dbg` global variable - #282
* Bump `faye-websocket` version to `0.11.0` - #267
* Optimize `arguments` usage - #263
* Add sourcemap file to dist folder - #237
* Add way to transparently pass transport-specific options - #272
1.0.3
==
* Use `https` module for xhr requests in node when url uses https - #254
1.0.2
==
* Fix iframe info receiver url
* Move iframe.contentWindow check inside setTimeout - #246
1.0.1
==
* Use proper base url for iframe-based info receiver - #249
* Don't register unload event in chrome packaged app - #223
* Allow custom session ids - #250
* Remove version property from bower.json - #247
* Update example CDN url - #244
1.0.0
===
* Simplify url handling by delegating to `url-parse` - #242
* Upgrade to `url-parse` 1.0.1 to fix colon issue if auth has no password
1.0.0-beta.13
===
* Transport timeout on connection should fallback - #238
1.0.0-beta.12
====
* Upgrade `url-parse` to 1.0.0 to fix #218 again
1.0.0-beta.10
====
* Upgrade `url-parse` to 0.2.3 to fix #222
1.0.0-beta.9
====
* Upgrade `url-parse` to 0.2.1 to fix 'too much recursion' errors
1.0.0-beta.8
====
* Upgrade `url-parse` to 0.2.0 to fix inheritance issues
1.0.0-beta.7
====
* Upgrade `url-parse` to 0.1.5 to fix #218
* Don't strip basic auth from url - #219
1.0.0-beta.6
====
* Upgrade `url-parse` to 0.1.3 to avoid CSP issues
1.0.0-beta.5
=====
* Upgrade `url-parse` to 0.1.1 to fix #214
1.0.0-beta.4
=====
* Upgrade `url-parse` to 0.1.0 and `sockjs` to 0.3.11
* Update .npmignore
1.0.0-beta.3
=====
* Move `debug` from devDependencies to dependencies
1.0.0-beta.2
=====
* Relax requirements when using same origin XHR - #80
* Upgrade to JSON3 from JSON2 - #123
* Package library with browserify supporting the UMD pattern - #184
* Move tests to JavaScript
* Add Gulp.js build script
* Fix getOrigin for file:/// urls and standard ports - #173
* Add onerror event handlers to Websockets - #169
* Increase RTO lower bound to prevent spurious timeouts on IE8/9 - #161
* Use window.crypto for random values when available - #128
* Fix handling of listeners added and removed mid-dispatch - #127
* Fix XHR Streaming for IE8 - #83
* Remove explicit AMD name - #107
* Check for an empty response from /info request - #143
* Add Content-Type to XHR requests to fix issue over HTTPS on Galaxy S4 - #164
* Fix iframe fallback when message is sent from a popup in IE7/8 - #166
* Add support for query strings on the url - #72
* Now works inside of Web Workers - #181
* Support EventSource / Server Sent Events outside of iframes - #201
* Rename protocols to transports - #65
* Allow transports which need the body to trigger on 'interactive' readyState - #175
* try/catch access to document.domain - #187
* Use `window.location` instead of `document.location` - #195
* Allow usage from node.js with same API
0.3.4
=====
* Mentioned njoyce's fork of sockjs-gevent.
* #90 - Don't catch onbeforeunload event - it breaks javascript://
links in IE.
* IE mangles 204 response code for 1223 on ajax, see:
http://bugs.jquery.com/ticket/1450
* Make `new` optional for SockJS constructor (via substack).
* It is impossible to cancel JSONP polling request - compensate for that.
* Refactored EventEmitter prototype (used only internally)
* #66 - Failure to post data to /xhr_send should kill the session
0.3.2
=====
* #77 - Getting /info on modern browsers when html is served from
file:// urls was broken.
0.3.1
=====
* #61 - Meteor guys found that we unintentionally catch "onopen" errors.
* #63 - Meteorjs guys found that xhr-streaming on Safari sometimes
left busy cursor running.
* Increased allowed time for websocket transport (from 1 rtt to 2),
this should make ws transport more reliable over SSL, at the cost
of slightly longer connection time for users with blocked ws.
* #57 - previous fix didn't really work, sockjs-client still left
a mess in browsers history when using iframe transports. This
is fixed now.
* #60 - Opera 12 (next) claims to do AJAX2 / CORS, but can't
do xhr-streaming.
* #58 - onunload test sometimes failed on Safari on windows
* Updated readme WRT websocket protocols
* Updated readme WRT deployments on heroku
* Add minimalistic license block to every source file.
0.3.0
=====
* Temporarily disabled iframe tests - they are failing unpredictably.
* #57 - pointing an iframe to "about:blank" during cleanup caused
Opera to messup history.
* #55 - Improved iframe abstraction (reduced a possible mem leak)
* Refactored AJAX abstractions, for better CORS handing - again.
* Add additional parent origin security check to an iframe.
* Urls with hashes or query strings can't be passed to SockJS.
* #18 - Mention workaround for Firefox ESC key issue
* #53 - AMD compliance
* sockjs/sockjs-protocol#28 - always use square brackets for
websocket frames
* #51 - initial support for IE10 - try XHR before XDR
* #28 - handle onunload / onbeforeunload in a more robust fashion
* #49 - support SockJS-client being used from files served from
file:// urls.
0.2.1
=====
* "smoke-latency.html" test was unnecesairly sending too much data.
* Bumped core dependencies (coffee-script and uglify-js)
* Minor updates to the README, few cosmetic changes in the code.
0.2.0
=====
* The API had changed - use `protocols_whitelist` option instead of
passing an array of protocols as a second argument to SockJS constructor.
* Dropped 'chunking-test' functionality and replace it with 'info'.
* Rewritten protocol-choosing alogirthm, see "utils.detectProtocols" method.
* Use dynamic protocol timeouts based on RTT, not hardcoded 5 seconds
* #34 - Don't ever reuse `session_id`, especially when trying
fallback protocols.
* The test server got moved from SockJS-client to SockJS-node.
* Don't test unicode surrogates - it can't work in some environments.
* XHR/XDR helpers were rewritten, ajax transports were simplified.
* Added a domain check in the iframe to improve security.
* SockJS will now trigger 1002 error if there is a problem during handshake
instead of 2000 error.
* Smoke-throughput test is renamed to smoke-latency.
0.1.2
=====
* #29 - Allow all unicode characters to be send over SockJS.
* #15 - SockJS should now work fine even if the connection is started
in HEAD, before BODY is loaded.
* #28 - In rare circumstances WebSocket connection can be left intact
after the page is unloaded in FireFox.
* Updated scripts to work with Node 0.6.
* Initial work to do better QUnit testing.
* Updated the minifying script (always escape unicode chars, remove
trailing comment).
* Use string instead of array of chars (utils.js:random_number_string).
0.1.1
=====
* #21 Get JsonP transport working on IE9 (Vladimir Dronnikov).
* #26 Emit heartbeat event.
* #27 Include license inline.
0.1.0
=====
* SockJS-client can only send UTF-8 encodable strings. Previously we
took advantage of rich data structures and automatically
json-encoded them, but this got removed. Now, all data passed to
`send` will be converted to string. This is also how native
* `status` property on `EventClose` is renamed to `code`
as per Websocket API
WebSockets behave.
* The test server was updated to new `sockjs-node` API
* Fixed problem with Jsonp-polling transport on IE9
* Repository was moved - updated links.
0.0.4
=====
* All transports were refactored, some transports were introduced:
htmlfile and separate xhr-streaming.
* Added logic to detect support for http chunking, and thus a
possibility to rule out streaming transports before running them.
* Added 'cookie' option, useful for cookie-based load balancing
(currently, it make a difference only for IE).
* Added hack to prevent EventSource from crashing Safari and Chrome.
* Loads and loads of other small and medium changes.
0.0.2
=====
* Initial support for JSESSIONID based load balancing. Currently
doesn't play nicely with IE XDomainRequest transport.
0.0.1
=====
* Initial release.

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2011-2012 VMware, Inc.
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.

View File

@@ -0,0 +1,339 @@
# SockJS-client
[![npm version](https://img.shields.io/npm/v/sockjs-client.svg?style=flat-square)](https://www.npmjs.com/package/sockjs-client)[![Build Status](https://img.shields.io/travis/sockjs/sockjs-client/master.svg?style=flat-square)](https://travis-ci.org/sockjs/sockjs-client)[![Dependencies](https://img.shields.io/david/sockjs/sockjs-client.svg?style=flat-square)](https://david-dm.org/sockjs/sockjs-client)[![Chat](https://img.shields.io/badge/Chat-gitter.im-blue.svg?style=flat-square)](https://gitter.im/sockjs/sockjs-client)
[![Sauce Test Status](https://saucelabs.com/buildstatus/brycekahle)](https://saucelabs.com/u/brycekahle)
SockJS is a browser JavaScript library that provides a WebSocket-like
object. SockJS gives you a coherent, cross-browser, Javascript API
which creates a low latency, full duplex, cross-domain communication
channel between the browser and the web server.
Under the hood SockJS tries to use native WebSockets first. If that
fails it can use a variety of browser-specific transport protocols and
presents them through WebSocket-like abstractions.
SockJS is intended to work for all modern browsers and in environments
which don't support the WebSocket protocol -- for example, behind restrictive
corporate proxies.
SockJS-client does require a server counterpart:
* [SockJS-node](https://github.com/sockjs/sockjs-node) is a SockJS
server for Node.js.
Philosophy:
* The API should follow
[HTML5 Websockets API](https://www.w3.org/TR/websockets/) as
closely as possible.
* All the transports must support cross domain connections out of the
box. It's possible and recommended to host a SockJS server on a
different server than your main web site.
* There is support for at least one streaming protocol for every
major browser.
* Streaming transports should work cross-domain and
should support cookies (for cookie-based sticky sessions).
* Polling transports are used as a fallback for old browsers and
hosts behind restrictive proxies.
* Connection establishment should be fast and lightweight.
* No Flash inside (no need to open port 843 - which doesn't work
through proxies, no need to host 'crossdomain.xml', no need
[to wait for 3 seconds](https://github.com/gimite/web-socket-js/issues/49)
in order to detect problems)
Subscribe to
[SockJS mailing list](https://groups.google.com/forum/#!forum/sockjs) for
discussions and support.
SockJS family:
* [SockJS-client](https://github.com/sockjs/sockjs-client) JavaScript client library
* [SockJS-node](https://github.com/sockjs/sockjs-node) Node.js server
* [SockJS-erlang](https://github.com/sockjs/sockjs-erlang) Erlang server
* [SockJS-cyclone](https://github.com/flaviogrossi/sockjs-cyclone) Python/Cyclone/Twisted server
* [SockJS-tornado](https://github.com/MrJoes/sockjs-tornado) Python/Tornado server
* [SockJS-twisted](https://github.com/DesertBus/sockjs-twisted/) Python/Twisted server
* [SockJS-aiohttp](https://github.com/aio-libs/sockjs/) Python/Aiohttp server
* [Spring Framework](https://projects.spring.io/spring-framework) Java [client](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/web.html#websocket-fallback-sockjs-client) & server
* [vert.x](https://github.com/vert-x/vert.x) Java/vert.x server
* [Xitrum](https://xitrum-framework.github.io/) Scala server
* [Atmosphere Framework](https://github.com/Atmosphere/atmosphere) JavaEE Server, Play Framework, Netty, Vert.x
* [Actix SockJS](https://github.com/fafhrd91/actix-sockjs) Rust Server, Actix Framework
Work in progress:
* [SockJS-ruby](https://github.com/nyarly/sockjs-ruby)
* [SockJS-netty](https://github.com/cgbystrom/sockjs-netty)
* [SockJS-gevent](https://github.com/ksava/sockjs-gevent) ([SockJS-gevent fork](https://github.com/njoyce/sockjs-gevent))
* [pyramid-SockJS](https://github.com/fafhrd91/pyramid_sockjs)
* [wildcloud-websockets](https://github.com/wildcloud/wildcloud-websockets)
* [wai-SockJS](https://github.com/Palmik/wai-sockjs)
* [SockJS-perl](https://github.com/vti/sockjs-perl)
* [SockJS-go](https://github.com/igm/sockjs-go/)
Getting Started
-------
SockJS mimics the [WebSockets API](https://www.w3.org/TR/websockets/),
but instead of `WebSocket` there is a `SockJS` Javascript object.
First, you need to load the SockJS JavaScript library. For example, you can
put that in your HTML head:
```html
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
```
After the script is loaded you can establish a connection with the
SockJS server. Here's a simple example:
```javascript
var sock = new SockJS('https://mydomain.com/my_prefix');
sock.onopen = function() {
console.log('open');
sock.send('test');
};
sock.onmessage = function(e) {
console.log('message', e.data);
sock.close();
};
sock.onclose = function() {
console.log('close');
};
```
SockJS-client API
-----------------
### SockJS class
Similar to the 'WebSocket' API, the 'SockJS' constructor takes one, or more arguments:
```javascript
var sockjs = new SockJS(url, _reserved, options);
```
`url` may contain a query string, if one is desired.
Where `options` is a hash which can contain:
* **server (string)**
String to append to url for actual data connection. Defaults to a random 4 digit number.
* **transports (string OR array of strings)**
Sometimes it is useful to disable some fallback transports. This
option allows you to supply a list transports that may be used by
SockJS. By default all available transports will be used.
* **sessionId (number OR function)**
Both client and server use session identifiers to distinguish connections.
If you specify this option as a number, SockJS will use its random string
generator function to generate session ids that are N-character long
(where N corresponds to the number specified by **sessionId**).
When you specify this option as a function, the function must return a
randomly generated string. Every time SockJS needs to generate a session
id it will call this function and use the returned string directly.
If you don't specify this option, the default is to use the default random
string generator to generate 8-character long session ids.
Although the 'SockJS' object tries to emulate the 'WebSocket'
behaviour, it's impossible to support all of its features. An
important SockJS limitation is the fact that you're not allowed to
open more than one SockJS connection to a single domain at a time.
This limitation is caused by an in-browser limit of outgoing
connections - usually [browsers don't allow opening more than two
outgoing connections to a single domain](https://stackoverflow.com/questions/985431/max-parallel-http-connections-in-a-browser). A single SockJS session
requires those two connections - one for downloading data, the other for
sending messages. Opening a second SockJS session at the same time
would most likely block, and can result in both sessions timing out.
Opening more than one SockJS connection at a time is generally a
bad practice. If you absolutely must do it, you can use
multiple subdomains, using a different subdomain for every
SockJS connection.
Supported transports, by browser (html served from http:// or https://)
-----------------------------------------------------------------------
_Browser_ | _Websockets_ | _Streaming_ | _Polling_
----------------|------------------|-------------|-------------------
IE 6, 7 | no | no | jsonp-polling
IE 8, 9 (cookies=no) | no | xdr-streaming &dagger; | xdr-polling &dagger;
IE 8, 9 (cookies=yes)| no | iframe-htmlfile | iframe-xhr-polling
IE 10 | rfc6455 | xhr-streaming | xhr-polling
Chrome 6-13 | hixie-76 | xhr-streaming | xhr-polling
Chrome 14+ | hybi-10 / rfc6455| xhr-streaming | xhr-polling
Firefox <10 | no &Dagger; | xhr-streaming | xhr-polling
Firefox 10+ | hybi-10 / rfc6455| xhr-streaming | xhr-polling
Safari 5.x | hixie-76 | xhr-streaming | xhr-polling
Safari 6+ | rfc6455 | xhr-streaming | xhr-polling
Opera 10.70+ | no &Dagger; | iframe-eventsource | iframe-xhr-polling
Opera 12.10+ | rfc6455 | xhr-streaming | xhr-polling
Konqueror | no | no | jsonp-polling
* **&dagger;**: IE 8+ supports [XDomainRequest][^9], which is
essentially a modified AJAX/XHR that can do requests across
domains. But unfortunately it doesn't send any cookies, which
makes it inappropriate for deployments when the load balancer uses
JSESSIONID cookie to do sticky sessions.
* **&Dagger;**: Firefox 4.0 and Opera 11.00 and shipped with disabled
Websockets "hixie-76". They can still be enabled by manually
changing a browser setting.
Supported transports, by browser (html served from file://)
-----------------------------------------------------------
Sometimes you may want to serve your html from "file://" address - for
development or if you're using PhoneGap or similar technologies. But
due to the Cross Origin Policy files served from "file://" have no
Origin, and that means some of SockJS transports won't work. For this
reason the SockJS transport table is different than usually, major
differences are:
_Browser_ | _Websockets_ | _Streaming_ | _Polling_
----------------|---------------|--------------------|-------------------
IE 8, 9 | same as above | iframe-htmlfile | iframe-xhr-polling
Other | same as above | iframe-eventsource | iframe-xhr-polling
Supported transports, by name
-----------------------------
_Transport_ | _References_
---------------------|---------------
websocket (rfc6455) | [rfc 6455][^10]
websocket (hixie-76) | [draft-hixie-thewebsocketprotocol-76][^1]
websocket (hybi-10) | [draft-ietf-hybi-thewebsocketprotocol-10][^2]
xhr-streaming | Transport using [Cross domain XHR][^5] [streaming][^7] capability (readyState=3).
xdr-streaming | Transport using [XDomainRequest][^9] [streaming][^7] capability (readyState=3).
eventsource | [EventSource/Server-sent events][^4].
iframe-eventsource | [EventSource/Server-sent events][^4] used from an [iframe via postMessage][^3].
htmlfile | [HtmlFile][^8].
iframe-htmlfile | [HtmlFile][^8] used from an [iframe via postMessage][^3].
xhr-polling | Long-polling using [cross domain XHR][^5].
xdr-polling | Long-polling using [XDomainRequest][^9].
iframe-xhr-polling | Long-polling using normal AJAX from an [iframe via postMessage][^3].
jsonp-polling | Slow and old fashioned [JSONP polling][^6]. This transport will show "busy indicator" (aka: "spinning wheel") when sending data.
[^1]: https://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
[^2]: https://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10
[^3]: https://developer.mozilla.org/en/DOM/window.postMessage
[^4]: https://html.spec.whatwg.org/multipage/comms.html#server-sent-events
[^5]: https://secure.wikimedia.org/wikipedia/en/wiki/XMLHttpRequest#Cross-domain_requests
[^6]: https://secure.wikimedia.org/wikipedia/en/wiki/JSONP
[^7]: http://www.debugtheweb.com/test/teststreaming.aspx
[^8]: http://cometdaily.com/2007/11/18/ie-activexhtmlfile-transport-part-ii/
[^9]: https://blogs.msdn.microsoft.com/ieinternals/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds/
[^10]: https://www.rfc-editor.org/rfc/rfc6455.txt
Connecting to SockJS without the client
---------------------------------------
Although the main point of SockJS is to enable browser-to-server
connectivity, it is possible to connect to SockJS from an external
application. Any SockJS server complying with 0.3 protocol does
support a raw WebSocket url. The raw WebSocket url for the test server
looks like:
* ws://localhost:8081/echo/websocket
You can connect any WebSocket RFC 6455 compliant WebSocket client to
this url. This can be a command line client, external application,
third party code or even a browser (though I don't know why you would
want to do so).
Deployment
----------
You should use a version of sockjs-client
that supports the protocol used by your server. For example:
```html
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
```
For server-side deployment tricks, especially about load balancing and
session stickiness, take a look at the
[SockJS-node readme](https://github.com/sockjs/sockjs-node#readme).
Development and testing
-----------------------
SockJS-client needs [node.js](https://nodejs.org/) for running a test
server and JavaScript minification. If you want to work on
SockJS-client source code, checkout the git repo and follow these
steps:
cd sockjs-client
npm install
To generate JavaScript, run:
gulp browserify
To generate minified JavaScript, run:
gulp browserify:min
Both commands output into the `build` directory.
### Testing
Once you've compiled the SockJS-client you may want to check if your changes
pass all the tests.
npm run test:browser_local
This will start [zuul](https://github.com/defunctzombie/zuul) and a test support server. Open the browser to [http://localhost:9090/_zuul](http://localhost:9090/_zuul) and watch the tests run.
Browser Quirks
--------------
There are various browser quirks which we don't intend to address:
* Pressing ESC in Firefox, before Firefox 20, closes the SockJS connection. For a workaround
and discussion see [#18](https://github.com/sockjs/sockjs-client/issues/18).
* `jsonp-polling` transport will show a "spinning wheel" (aka. "busy indicator")
when sending data.
* You can't open more than one SockJS connection to one domain at the
same time due to [the browser's limit of concurrent connections](https://stackoverflow.com/questions/985431/max-parallel-http-connections-in-a-browser)
(this limit is not counting native WebSocket connections).
* Although SockJS is trying to escape any strange Unicode characters
(even invalid ones - [like surrogates \xD800-\xDBFF](https://en.wikipedia.org/wiki/Mapping_of_Unicode_characters#Surrogates) or [\xFFFE and \xFFFF](https://en.wikipedia.org/wiki/Unicode#Character_General_Category))
it's advisable to use only valid characters. Using invalid
characters is a bit slower, and may not work with SockJS servers
that have proper Unicode support.
* Having a global function called `onmessage` or such is probably a
bad idea, as it could be called by the built-in `postMessage` API.
* From SockJS' point of view there is nothing special about
SSL/HTTPS. Connecting between unencrypted and encrypted sites
should work just fine.
* Although SockJS does its best to support both prefix and cookie based
sticky sessions, the latter may not work well cross-domain with
browsers that don't accept third-party cookies by default (Safari).
In order to get around this make sure you're connecting to SockJS
from the same parent domain as the main site. For example
'sockjs.a.com' is able to set cookies if you're connecting from
'www.a.com' or 'a.com'.
* Trying to connect from secure "https://" to insecure "http://" is
not a good idea. The other way around should be fine.
* Long polling is known to cause problems on Heroku, but a
[workaround for SockJS is available](https://github.com/sockjs/sockjs-node/issues/57#issuecomment-5242187).
* SockJS [websocket transport is more stable over SSL](https://github.com/sockjs/sockjs-client/issues/94). If
you're a serious SockJS user then consider using SSL
([more info](https://www.ietf.org/mail-archive/web/hybi/current/msg01605.html)).

View File

@@ -0,0 +1,18 @@
#!/bin/bash
set -e
if [ "x${BROWSER}" = "x" ]; then
npm run lint
npm test
elif [ "${TRAVIS_SECURE_ENV_VARS}" = "true" ]; then
npm run test:bundle
if [ "x${BROWSER_NAME}" = "x" ]; then
./node_modules/.bin/zuul tests/browser.js
elif [ "x${BROWSER_PLATFORM}" = "x" ]; then
./node_modules/.bin/zuul --browser-name $BROWSER_NAME --browser-version $BROWSER_VERSION tests/browser.js
else
./node_modules/.bin/zuul --browser-name $BROWSER_NAME --browser-version $BROWSER_VERSION --browser-platform "$BROWSER_PLATFORM" tests/browser.js
fi
else
exit 1
fi

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,10 @@
'use strict';
var transportList = require('./transport-list');
module.exports = require('./main')(transportList);
// TODO can't get rid of this until all servers do
if ('_sockjs_onload' in global) {
setTimeout(global._sockjs_onload, 1);
}

View File

@@ -0,0 +1,17 @@
'use strict';
var inherits = require('inherits')
, Event = require('./event')
;
function CloseEvent() {
Event.call(this);
this.initEvent('close', false, false);
this.wasClean = false;
this.code = 0;
this.reason = '';
}
inherits(CloseEvent, Event);
module.exports = CloseEvent;

View File

@@ -0,0 +1,57 @@
'use strict';
var inherits = require('inherits')
, EventTarget = require('./eventtarget')
;
function EventEmitter() {
EventTarget.call(this);
}
inherits(EventEmitter, EventTarget);
EventEmitter.prototype.removeAllListeners = function(type) {
if (type) {
delete this._listeners[type];
} else {
this._listeners = {};
}
};
EventEmitter.prototype.once = function(type, listener) {
var self = this
, fired = false;
function g() {
self.removeListener(type, g);
if (!fired) {
fired = true;
listener.apply(this, arguments);
}
}
this.on(type, g);
};
EventEmitter.prototype.emit = function() {
var type = arguments[0];
var listeners = this._listeners[type];
if (!listeners) {
return;
}
// equivalent of Array.prototype.slice.call(arguments, 1);
var l = arguments.length;
var args = new Array(l - 1);
for (var ai = 1; ai < l; ai++) {
args[ai - 1] = arguments[ai];
}
for (var i = 0; i < listeners.length; i++) {
listeners[i].apply(this, args);
}
};
EventEmitter.prototype.on = EventEmitter.prototype.addListener = EventTarget.prototype.addEventListener;
EventEmitter.prototype.removeListener = EventTarget.prototype.removeEventListener;
module.exports.EventEmitter = EventEmitter;

View File

@@ -0,0 +1,22 @@
'use strict';
function Event(eventType) {
this.type = eventType;
}
Event.prototype.initEvent = function(eventType, canBubble, cancelable) {
this.type = eventType;
this.bubbles = canBubble;
this.cancelable = cancelable;
this.timeStamp = +new Date();
return this;
};
Event.prototype.stopPropagation = function() {};
Event.prototype.preventDefault = function() {};
Event.CAPTURING_PHASE = 1;
Event.AT_TARGET = 2;
Event.BUBBLING_PHASE = 3;
module.exports = Event;

View File

@@ -0,0 +1,62 @@
'use strict';
/* Simplified implementation of DOM2 EventTarget.
* http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget
*/
function EventTarget() {
this._listeners = {};
}
EventTarget.prototype.addEventListener = function(eventType, listener) {
if (!(eventType in this._listeners)) {
this._listeners[eventType] = [];
}
var arr = this._listeners[eventType];
// #4
if (arr.indexOf(listener) === -1) {
// Make a copy so as not to interfere with a current dispatchEvent.
arr = arr.concat([listener]);
}
this._listeners[eventType] = arr;
};
EventTarget.prototype.removeEventListener = function(eventType, listener) {
var arr = this._listeners[eventType];
if (!arr) {
return;
}
var idx = arr.indexOf(listener);
if (idx !== -1) {
if (arr.length > 1) {
// Make a copy so as not to interfere with a current dispatchEvent.
this._listeners[eventType] = arr.slice(0, idx).concat(arr.slice(idx + 1));
} else {
delete this._listeners[eventType];
}
return;
}
};
EventTarget.prototype.dispatchEvent = function() {
var event = arguments[0];
var t = event.type;
// equivalent of Array.prototype.slice.call(arguments, 0);
var args = arguments.length === 1 ? [event] : Array.apply(null, arguments);
// TODO: This doesn't match the real behavior; per spec, onfoo get
// their place in line from the /first/ time they're set from
// non-null. Although WebKit bumps it to the end every time it's
// set.
if (this['on' + t]) {
this['on' + t].apply(this, args);
}
if (t in this._listeners) {
// Grab a reference to the listeners list. removeEventListener may alter the list.
var listeners = this._listeners[t];
for (var i = 0; i < listeners.length; i++) {
listeners[i].apply(this, args);
}
}
};
module.exports = EventTarget;

View File

@@ -0,0 +1,15 @@
'use strict';
var inherits = require('inherits')
, Event = require('./event')
;
function TransportMessageEvent(data) {
Event.call(this);
this.initEvent('message', false, false);
this.data = data;
}
inherits(TransportMessageEvent, Event);
module.exports = TransportMessageEvent;

View File

@@ -0,0 +1,27 @@
'use strict';
var JSON3 = require('json3')
, iframeUtils = require('./utils/iframe')
;
function FacadeJS(transport) {
this._transport = transport;
transport.on('message', this._transportMessage.bind(this));
transport.on('close', this._transportClose.bind(this));
}
FacadeJS.prototype._transportClose = function(code, reason) {
iframeUtils.postMessage('c', JSON3.stringify([code, reason]));
};
FacadeJS.prototype._transportMessage = function(frame) {
iframeUtils.postMessage('t', frame);
};
FacadeJS.prototype._send = function(data) {
this._transport.send(data);
};
FacadeJS.prototype._close = function() {
this._transport.close();
this._transport.removeAllListeners();
};
module.exports = FacadeJS;

View File

@@ -0,0 +1,102 @@
'use strict';
var urlUtils = require('./utils/url')
, eventUtils = require('./utils/event')
, JSON3 = require('json3')
, FacadeJS = require('./facade')
, InfoIframeReceiver = require('./info-iframe-receiver')
, iframeUtils = require('./utils/iframe')
, loc = require('./location')
;
var debug = function() {};
if (process.env.NODE_ENV !== 'production') {
debug = require('debug')('sockjs-client:iframe-bootstrap');
}
module.exports = function(SockJS, availableTransports) {
var transportMap = {};
availableTransports.forEach(function(at) {
if (at.facadeTransport) {
transportMap[at.facadeTransport.transportName] = at.facadeTransport;
}
});
// hard-coded for the info iframe
// TODO see if we can make this more dynamic
transportMap[InfoIframeReceiver.transportName] = InfoIframeReceiver;
var parentOrigin;
/* eslint-disable camelcase */
SockJS.bootstrap_iframe = function() {
/* eslint-enable camelcase */
var facade;
iframeUtils.currentWindowId = loc.hash.slice(1);
var onMessage = function(e) {
if (e.source !== parent) {
return;
}
if (typeof parentOrigin === 'undefined') {
parentOrigin = e.origin;
}
if (e.origin !== parentOrigin) {
return;
}
var iframeMessage;
try {
iframeMessage = JSON3.parse(e.data);
} catch (ignored) {
debug('bad json', e.data);
return;
}
if (iframeMessage.windowId !== iframeUtils.currentWindowId) {
return;
}
switch (iframeMessage.type) {
case 's':
var p;
try {
p = JSON3.parse(iframeMessage.data);
} catch (ignored) {
debug('bad json', iframeMessage.data);
break;
}
var version = p[0];
var transport = p[1];
var transUrl = p[2];
var baseUrl = p[3];
debug(version, transport, transUrl, baseUrl);
// change this to semver logic
if (version !== SockJS.version) {
throw new Error('Incompatible SockJS! Main site uses:' +
' "' + version + '", the iframe:' +
' "' + SockJS.version + '".');
}
if (!urlUtils.isOriginEqual(transUrl, loc.href) ||
!urlUtils.isOriginEqual(baseUrl, loc.href)) {
throw new Error('Can\'t connect to different domain from within an ' +
'iframe. (' + loc.href + ', ' + transUrl + ', ' + baseUrl + ')');
}
facade = new FacadeJS(new transportMap[transport](transUrl, baseUrl));
break;
case 'm':
facade._send(iframeMessage.data);
break;
case 'c':
if (facade) {
facade._close();
}
facade = null;
break;
}
};
eventUtils.attachEvent('message', onMessage);
// Start
iframeUtils.postMessage('s');
};
};

View File

@@ -0,0 +1,49 @@
'use strict';
var EventEmitter = require('events').EventEmitter
, inherits = require('inherits')
, JSON3 = require('json3')
, objectUtils = require('./utils/object')
;
var debug = function() {};
if (process.env.NODE_ENV !== 'production') {
debug = require('debug')('sockjs-client:info-ajax');
}
function InfoAjax(url, AjaxObject) {
EventEmitter.call(this);
var self = this;
var t0 = +new Date();
this.xo = new AjaxObject('GET', url);
this.xo.once('finish', function(status, text) {
var info, rtt;
if (status === 200) {
rtt = (+new Date()) - t0;
if (text) {
try {
info = JSON3.parse(text);
} catch (e) {
debug('bad json', text);
}
}
if (!objectUtils.isObject(info)) {
info = {};
}
}
self.emit('finish', info, rtt);
self.removeAllListeners();
});
}
inherits(InfoAjax, EventEmitter);
InfoAjax.prototype.close = function() {
this.removeAllListeners();
this.xo.close();
};
module.exports = InfoAjax;

View File

@@ -0,0 +1,33 @@
'use strict';
var inherits = require('inherits')
, EventEmitter = require('events').EventEmitter
, JSON3 = require('json3')
, XHRLocalObject = require('./transport/sender/xhr-local')
, InfoAjax = require('./info-ajax')
;
function InfoReceiverIframe(transUrl) {
var self = this;
EventEmitter.call(this);
this.ir = new InfoAjax(transUrl, XHRLocalObject);
this.ir.once('finish', function(info, rtt) {
self.ir = null;
self.emit('message', JSON3.stringify([info, rtt]));
});
}
inherits(InfoReceiverIframe, EventEmitter);
InfoReceiverIframe.transportName = 'iframe-info-receiver';
InfoReceiverIframe.prototype.close = function() {
if (this.ir) {
this.ir.close();
this.ir = null;
}
this.removeAllListeners();
};
module.exports = InfoReceiverIframe;

View File

@@ -0,0 +1,69 @@
'use strict';
var EventEmitter = require('events').EventEmitter
, inherits = require('inherits')
, JSON3 = require('json3')
, utils = require('./utils/event')
, IframeTransport = require('./transport/iframe')
, InfoReceiverIframe = require('./info-iframe-receiver')
;
var debug = function() {};
if (process.env.NODE_ENV !== 'production') {
debug = require('debug')('sockjs-client:info-iframe');
}
function InfoIframe(baseUrl, url) {
var self = this;
EventEmitter.call(this);
var go = function() {
var ifr = self.ifr = new IframeTransport(InfoReceiverIframe.transportName, url, baseUrl);
ifr.once('message', function(msg) {
if (msg) {
var d;
try {
d = JSON3.parse(msg);
} catch (e) {
debug('bad json', msg);
self.emit('finish');
self.close();
return;
}
var info = d[0], rtt = d[1];
self.emit('finish', info, rtt);
}
self.close();
});
ifr.once('close', function() {
self.emit('finish');
self.close();
});
};
// TODO this seems the same as the 'needBody' from transports
if (!global.document.body) {
utils.attachEvent('load', go);
} else {
go();
}
}
inherits(InfoIframe, EventEmitter);
InfoIframe.enabled = function() {
return IframeTransport.enabled();
};
InfoIframe.prototype.close = function() {
if (this.ifr) {
this.ifr.close();
}
this.removeAllListeners();
this.ifr = null;
};
module.exports = InfoIframe;

View File

@@ -0,0 +1,89 @@
'use strict';
var EventEmitter = require('events').EventEmitter
, inherits = require('inherits')
, urlUtils = require('./utils/url')
, XDR = require('./transport/sender/xdr')
, XHRCors = require('./transport/sender/xhr-cors')
, XHRLocal = require('./transport/sender/xhr-local')
, XHRFake = require('./transport/sender/xhr-fake')
, InfoIframe = require('./info-iframe')
, InfoAjax = require('./info-ajax')
;
var debug = function() {};
if (process.env.NODE_ENV !== 'production') {
debug = require('debug')('sockjs-client:info-receiver');
}
function InfoReceiver(baseUrl, urlInfo) {
debug(baseUrl);
var self = this;
EventEmitter.call(this);
setTimeout(function() {
self.doXhr(baseUrl, urlInfo);
}, 0);
}
inherits(InfoReceiver, EventEmitter);
// TODO this is currently ignoring the list of available transports and the whitelist
InfoReceiver._getReceiver = function(baseUrl, url, urlInfo) {
// determine method of CORS support (if needed)
if (urlInfo.sameOrigin) {
return new InfoAjax(url, XHRLocal);
}
if (XHRCors.enabled) {
return new InfoAjax(url, XHRCors);
}
if (XDR.enabled && urlInfo.sameScheme) {
return new InfoAjax(url, XDR);
}
if (InfoIframe.enabled()) {
return new InfoIframe(baseUrl, url);
}
return new InfoAjax(url, XHRFake);
};
InfoReceiver.prototype.doXhr = function(baseUrl, urlInfo) {
var self = this
, url = urlUtils.addPath(baseUrl, '/info')
;
debug('doXhr', url);
this.xo = InfoReceiver._getReceiver(baseUrl, url, urlInfo);
this.timeoutRef = setTimeout(function() {
debug('timeout');
self._cleanup(false);
self.emit('finish');
}, InfoReceiver.timeout);
this.xo.once('finish', function(info, rtt) {
debug('finish', info, rtt);
self._cleanup(true);
self.emit('finish', info, rtt);
});
};
InfoReceiver.prototype._cleanup = function(wasClean) {
debug('_cleanup');
clearTimeout(this.timeoutRef);
this.timeoutRef = null;
if (!wasClean && this.xo) {
this.xo.close();
}
this.xo = null;
};
InfoReceiver.prototype.close = function() {
debug('close');
this.removeAllListeners();
this._cleanup(false);
};
InfoReceiver.timeout = 8000;
module.exports = InfoReceiver;

View File

@@ -0,0 +1,10 @@
'use strict';
module.exports = global.location || {
origin: 'http://localhost:80'
, protocol: 'http:'
, host: 'localhost'
, port: 80
, href: 'http://localhost/'
, hash: ''
};

View File

@@ -0,0 +1,385 @@
'use strict';
require('./shims');
var URL = require('url-parse')
, inherits = require('inherits')
, JSON3 = require('json3')
, random = require('./utils/random')
, escape = require('./utils/escape')
, urlUtils = require('./utils/url')
, eventUtils = require('./utils/event')
, transport = require('./utils/transport')
, objectUtils = require('./utils/object')
, browser = require('./utils/browser')
, log = require('./utils/log')
, Event = require('./event/event')
, EventTarget = require('./event/eventtarget')
, loc = require('./location')
, CloseEvent = require('./event/close')
, TransportMessageEvent = require('./event/trans-message')
, InfoReceiver = require('./info-receiver')
;
var debug = function() {};
if (process.env.NODE_ENV !== 'production') {
debug = require('debug')('sockjs-client:main');
}
var transports;
// follow constructor steps defined at http://dev.w3.org/html5/websockets/#the-websocket-interface
function SockJS(url, protocols, options) {
if (!(this instanceof SockJS)) {
return new SockJS(url, protocols, options);
}
if (arguments.length < 1) {
throw new TypeError("Failed to construct 'SockJS: 1 argument required, but only 0 present");
}
EventTarget.call(this);
this.readyState = SockJS.CONNECTING;
this.extensions = '';
this.protocol = '';
// non-standard extension
options = options || {};
if (options.protocols_whitelist) {
log.warn("'protocols_whitelist' is DEPRECATED. Use 'transports' instead.");
}
this._transportsWhitelist = options.transports;
this._transportOptions = options.transportOptions || {};
var sessionId = options.sessionId || 8;
if (typeof sessionId === 'function') {
this._generateSessionId = sessionId;
} else if (typeof sessionId === 'number') {
this._generateSessionId = function() {
return random.string(sessionId);
};
} else {
throw new TypeError('If sessionId is used in the options, it needs to be a number or a function.');
}
this._server = options.server || random.numberString(1000);
// Step 1 of WS spec - parse and validate the url. Issue #8
var parsedUrl = new URL(url);
if (!parsedUrl.host || !parsedUrl.protocol) {
throw new SyntaxError("The URL '" + url + "' is invalid");
} else if (parsedUrl.hash) {
throw new SyntaxError('The URL must not contain a fragment');
} else if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
throw new SyntaxError("The URL's scheme must be either 'http:' or 'https:'. '" + parsedUrl.protocol + "' is not allowed.");
}
var secure = parsedUrl.protocol === 'https:';
// Step 2 - don't allow secure origin with an insecure protocol
if (loc.protocol === 'https:' && !secure) {
throw new Error('SecurityError: An insecure SockJS connection may not be initiated from a page loaded over HTTPS');
}
// Step 3 - check port access - no need here
// Step 4 - parse protocols argument
if (!protocols) {
protocols = [];
} else if (!Array.isArray(protocols)) {
protocols = [protocols];
}
// Step 5 - check protocols argument
var sortedProtocols = protocols.sort();
sortedProtocols.forEach(function(proto, i) {
if (!proto) {
throw new SyntaxError("The protocols entry '" + proto + "' is invalid.");
}
if (i < (sortedProtocols.length - 1) && proto === sortedProtocols[i + 1]) {
throw new SyntaxError("The protocols entry '" + proto + "' is duplicated.");
}
});
// Step 6 - convert origin
var o = urlUtils.getOrigin(loc.href);
this._origin = o ? o.toLowerCase() : null;
// remove the trailing slash
parsedUrl.set('pathname', parsedUrl.pathname.replace(/\/+$/, ''));
// store the sanitized url
this.url = parsedUrl.href;
debug('using url', this.url);
// Step 7 - start connection in background
// obtain server info
// http://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html#section-26
this._urlInfo = {
nullOrigin: !browser.hasDomain()
, sameOrigin: urlUtils.isOriginEqual(this.url, loc.href)
, sameScheme: urlUtils.isSchemeEqual(this.url, loc.href)
};
this._ir = new InfoReceiver(this.url, this._urlInfo);
this._ir.once('finish', this._receiveInfo.bind(this));
}
inherits(SockJS, EventTarget);
function userSetCode(code) {
return code === 1000 || (code >= 3000 && code <= 4999);
}
SockJS.prototype.close = function(code, reason) {
// Step 1
if (code && !userSetCode(code)) {
throw new Error('InvalidAccessError: Invalid code');
}
// Step 2.4 states the max is 123 bytes, but we are just checking length
if (reason && reason.length > 123) {
throw new SyntaxError('reason argument has an invalid length');
}
// Step 3.1
if (this.readyState === SockJS.CLOSING || this.readyState === SockJS.CLOSED) {
return;
}
// TODO look at docs to determine how to set this
var wasClean = true;
this._close(code || 1000, reason || 'Normal closure', wasClean);
};
SockJS.prototype.send = function(data) {
// #13 - convert anything non-string to string
// TODO this currently turns objects into [object Object]
if (typeof data !== 'string') {
data = '' + data;
}
if (this.readyState === SockJS.CONNECTING) {
throw new Error('InvalidStateError: The connection has not been established yet');
}
if (this.readyState !== SockJS.OPEN) {
return;
}
this._transport.send(escape.quote(data));
};
SockJS.version = require('./version');
SockJS.CONNECTING = 0;
SockJS.OPEN = 1;
SockJS.CLOSING = 2;
SockJS.CLOSED = 3;
SockJS.prototype._receiveInfo = function(info, rtt) {
debug('_receiveInfo', rtt);
this._ir = null;
if (!info) {
this._close(1002, 'Cannot connect to server');
return;
}
// establish a round-trip timeout (RTO) based on the
// round-trip time (RTT)
this._rto = this.countRTO(rtt);
// allow server to override url used for the actual transport
this._transUrl = info.base_url ? info.base_url : this.url;
info = objectUtils.extend(info, this._urlInfo);
debug('info', info);
// determine list of desired and supported transports
var enabledTransports = transports.filterToEnabled(this._transportsWhitelist, info);
this._transports = enabledTransports.main;
debug(this._transports.length + ' enabled transports');
this._connect();
};
SockJS.prototype._connect = function() {
for (var Transport = this._transports.shift(); Transport; Transport = this._transports.shift()) {
debug('attempt', Transport.transportName);
if (Transport.needBody) {
if (!global.document.body ||
(typeof global.document.readyState !== 'undefined' &&
global.document.readyState !== 'complete' &&
global.document.readyState !== 'interactive')) {
debug('waiting for body');
this._transports.unshift(Transport);
eventUtils.attachEvent('load', this._connect.bind(this));
return;
}
}
// calculate timeout based on RTO and round trips. Default to 5s
var timeoutMs = (this._rto * Transport.roundTrips) || 5000;
this._transportTimeoutId = setTimeout(this._transportTimeout.bind(this), timeoutMs);
debug('using timeout', timeoutMs);
var transportUrl = urlUtils.addPath(this._transUrl, '/' + this._server + '/' + this._generateSessionId());
var options = this._transportOptions[Transport.transportName];
debug('transport url', transportUrl);
var transportObj = new Transport(transportUrl, this._transUrl, options);
transportObj.on('message', this._transportMessage.bind(this));
transportObj.once('close', this._transportClose.bind(this));
transportObj.transportName = Transport.transportName;
this._transport = transportObj;
return;
}
this._close(2000, 'All transports failed', false);
};
SockJS.prototype._transportTimeout = function() {
debug('_transportTimeout');
if (this.readyState === SockJS.CONNECTING) {
if (this._transport) {
this._transport.close();
}
this._transportClose(2007, 'Transport timed out');
}
};
SockJS.prototype._transportMessage = function(msg) {
debug('_transportMessage', msg);
var self = this
, type = msg.slice(0, 1)
, content = msg.slice(1)
, payload
;
// first check for messages that don't need a payload
switch (type) {
case 'o':
this._open();
return;
case 'h':
this.dispatchEvent(new Event('heartbeat'));
debug('heartbeat', this.transport);
return;
}
if (content) {
try {
payload = JSON3.parse(content);
} catch (e) {
debug('bad json', content);
}
}
if (typeof payload === 'undefined') {
debug('empty payload', content);
return;
}
switch (type) {
case 'a':
if (Array.isArray(payload)) {
payload.forEach(function(p) {
debug('message', self.transport, p);
self.dispatchEvent(new TransportMessageEvent(p));
});
}
break;
case 'm':
debug('message', this.transport, payload);
this.dispatchEvent(new TransportMessageEvent(payload));
break;
case 'c':
if (Array.isArray(payload) && payload.length === 2) {
this._close(payload[0], payload[1], true);
}
break;
}
};
SockJS.prototype._transportClose = function(code, reason) {
debug('_transportClose', this.transport, code, reason);
if (this._transport) {
this._transport.removeAllListeners();
this._transport = null;
this.transport = null;
}
if (!userSetCode(code) && code !== 2000 && this.readyState === SockJS.CONNECTING) {
this._connect();
return;
}
this._close(code, reason);
};
SockJS.prototype._open = function() {
debug('_open', this._transport.transportName, this.readyState);
if (this.readyState === SockJS.CONNECTING) {
if (this._transportTimeoutId) {
clearTimeout(this._transportTimeoutId);
this._transportTimeoutId = null;
}
this.readyState = SockJS.OPEN;
this.transport = this._transport.transportName;
this.dispatchEvent(new Event('open'));
debug('connected', this.transport);
} else {
// The server might have been restarted, and lost track of our
// connection.
this._close(1006, 'Server lost session');
}
};
SockJS.prototype._close = function(code, reason, wasClean) {
debug('_close', this.transport, code, reason, wasClean, this.readyState);
var forceFail = false;
if (this._ir) {
forceFail = true;
this._ir.close();
this._ir = null;
}
if (this._transport) {
this._transport.close();
this._transport = null;
this.transport = null;
}
if (this.readyState === SockJS.CLOSED) {
throw new Error('InvalidStateError: SockJS has already been closed');
}
this.readyState = SockJS.CLOSING;
setTimeout(function() {
this.readyState = SockJS.CLOSED;
if (forceFail) {
this.dispatchEvent(new Event('error'));
}
var e = new CloseEvent('close');
e.wasClean = wasClean || false;
e.code = code || 1000;
e.reason = reason;
this.dispatchEvent(e);
this.onmessage = this.onclose = this.onerror = null;
debug('disconnected');
}.bind(this), 0);
};
// See: http://www.erg.abdn.ac.uk/~gerrit/dccp/notes/ccid2/rto_estimator/
// and RFC 2988.
SockJS.prototype.countRTO = function(rtt) {
// In a local environment, when using IE8/9 and the `jsonp-polling`
// transport the time needed to establish a connection (the time that pass
// from the opening of the transport to the call of `_dispatchOpen`) is
// around 200msec (the lower bound used in the article above) and this
// causes spurious timeouts. For this reason we calculate a value slightly
// larger than that used in the article.
if (rtt > 100) {
return 4 * rtt; // rto > 400msec
}
return 300 + rtt; // 300msec < rto <= 400msec
};
module.exports = function(availableTransports) {
transports = transport(availableTransports);
require('./iframe-bootstrap')(SockJS, availableTransports);
return SockJS;
};

View File

@@ -0,0 +1,452 @@
/* eslint-disable */
/* jscs: disable */
'use strict';
// pulled specific shims from https://github.com/es-shims/es5-shim
var ArrayPrototype = Array.prototype;
var ObjectPrototype = Object.prototype;
var FunctionPrototype = Function.prototype;
var StringPrototype = String.prototype;
var array_slice = ArrayPrototype.slice;
var _toString = ObjectPrototype.toString;
var isFunction = function (val) {
return ObjectPrototype.toString.call(val) === '[object Function]';
};
var isArray = function isArray(obj) {
return _toString.call(obj) === '[object Array]';
};
var isString = function isString(obj) {
return _toString.call(obj) === '[object String]';
};
var supportsDescriptors = Object.defineProperty && (function () {
try {
Object.defineProperty({}, 'x', {});
return true;
} catch (e) { /* this is ES3 */
return false;
}
}());
// Define configurable, writable and non-enumerable props
// if they don't exist.
var defineProperty;
if (supportsDescriptors) {
defineProperty = function (object, name, method, forceAssign) {
if (!forceAssign && (name in object)) { return; }
Object.defineProperty(object, name, {
configurable: true,
enumerable: false,
writable: true,
value: method
});
};
} else {
defineProperty = function (object, name, method, forceAssign) {
if (!forceAssign && (name in object)) { return; }
object[name] = method;
};
}
var defineProperties = function (object, map, forceAssign) {
for (var name in map) {
if (ObjectPrototype.hasOwnProperty.call(map, name)) {
defineProperty(object, name, map[name], forceAssign);
}
}
};
var toObject = function (o) {
if (o == null) { // this matches both null and undefined
throw new TypeError("can't convert " + o + ' to object');
}
return Object(o);
};
//
// Util
// ======
//
// ES5 9.4
// http://es5.github.com/#x9.4
// http://jsperf.com/to-integer
function toInteger(num) {
var n = +num;
if (n !== n) { // isNaN
n = 0;
} else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) {
n = (n > 0 || -1) * Math.floor(Math.abs(n));
}
return n;
}
function ToUint32(x) {
return x >>> 0;
}
//
// Function
// ========
//
// ES-5 15.3.4.5
// http://es5.github.com/#x15.3.4.5
function Empty() {}
defineProperties(FunctionPrototype, {
bind: function bind(that) { // .length is 1
// 1. Let Target be the this value.
var target = this;
// 2. If IsCallable(Target) is false, throw a TypeError exception.
if (!isFunction(target)) {
throw new TypeError('Function.prototype.bind called on incompatible ' + target);
}
// 3. Let A be a new (possibly empty) internal list of all of the
// argument values provided after thisArg (arg1, arg2 etc), in order.
// XXX slicedArgs will stand in for "A" if used
var args = array_slice.call(arguments, 1); // for normal call
// 4. Let F be a new native ECMAScript object.
// 11. Set the [[Prototype]] internal property of F to the standard
// built-in Function prototype object as specified in 15.3.3.1.
// 12. Set the [[Call]] internal property of F as described in
// 15.3.4.5.1.
// 13. Set the [[Construct]] internal property of F as described in
// 15.3.4.5.2.
// 14. Set the [[HasInstance]] internal property of F as described in
// 15.3.4.5.3.
var binder = function () {
if (this instanceof bound) {
// 15.3.4.5.2 [[Construct]]
// When the [[Construct]] internal method of a function object,
// F that was created using the bind function is called with a
// list of arguments ExtraArgs, the following steps are taken:
// 1. Let target be the value of F's [[TargetFunction]]
// internal property.
// 2. If target has no [[Construct]] internal method, a
// TypeError exception is thrown.
// 3. Let boundArgs be the value of F's [[BoundArgs]] internal
// property.
// 4. Let args be a new list containing the same values as the
// list boundArgs in the same order followed by the same
// values as the list ExtraArgs in the same order.
// 5. Return the result of calling the [[Construct]] internal
// method of target providing args as the arguments.
var result = target.apply(
this,
args.concat(array_slice.call(arguments))
);
if (Object(result) === result) {
return result;
}
return this;
} else {
// 15.3.4.5.1 [[Call]]
// When the [[Call]] internal method of a function object, F,
// which was created using the bind function is called with a
// this value and a list of arguments ExtraArgs, the following
// steps are taken:
// 1. Let boundArgs be the value of F's [[BoundArgs]] internal
// property.
// 2. Let boundThis be the value of F's [[BoundThis]] internal
// property.
// 3. Let target be the value of F's [[TargetFunction]] internal
// property.
// 4. Let args be a new list containing the same values as the
// list boundArgs in the same order followed by the same
// values as the list ExtraArgs in the same order.
// 5. Return the result of calling the [[Call]] internal method
// of target providing boundThis as the this value and
// providing args as the arguments.
// equiv: target.call(this, ...boundArgs, ...args)
return target.apply(
that,
args.concat(array_slice.call(arguments))
);
}
};
// 15. If the [[Class]] internal property of Target is "Function", then
// a. Let L be the length property of Target minus the length of A.
// b. Set the length own property of F to either 0 or L, whichever is
// larger.
// 16. Else set the length own property of F to 0.
var boundLength = Math.max(0, target.length - args.length);
// 17. Set the attributes of the length own property of F to the values
// specified in 15.3.5.1.
var boundArgs = [];
for (var i = 0; i < boundLength; i++) {
boundArgs.push('$' + i);
}
// XXX Build a dynamic function with desired amount of arguments is the only
// way to set the length property of a function.
// In environments where Content Security Policies enabled (Chrome extensions,
// for ex.) all use of eval or Function costructor throws an exception.
// However in all of these environments Function.prototype.bind exists
// and so this code will never be executed.
var bound = Function('binder', 'return function (' + boundArgs.join(',') + '){ return binder.apply(this, arguments); }')(binder);
if (target.prototype) {
Empty.prototype = target.prototype;
bound.prototype = new Empty();
// Clean up dangling references.
Empty.prototype = null;
}
// TODO
// 18. Set the [[Extensible]] internal property of F to true.
// TODO
// 19. Let thrower be the [[ThrowTypeError]] function Object (13.2.3).
// 20. Call the [[DefineOwnProperty]] internal method of F with
// arguments "caller", PropertyDescriptor {[[Get]]: thrower, [[Set]]:
// thrower, [[Enumerable]]: false, [[Configurable]]: false}, and
// false.
// 21. Call the [[DefineOwnProperty]] internal method of F with
// arguments "arguments", PropertyDescriptor {[[Get]]: thrower,
// [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: false},
// and false.
// TODO
// NOTE Function objects created using Function.prototype.bind do not
// have a prototype property or the [[Code]], [[FormalParameters]], and
// [[Scope]] internal properties.
// XXX can't delete prototype in pure-js.
// 22. Return F.
return bound;
}
});
//
// Array
// =====
//
// ES5 15.4.3.2
// http://es5.github.com/#x15.4.3.2
// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/isArray
defineProperties(Array, { isArray: isArray });
var boxedString = Object('a');
var splitString = boxedString[0] !== 'a' || !(0 in boxedString);
var properlyBoxesContext = function properlyBoxed(method) {
// Check node 0.6.21 bug where third parameter is not boxed
var properlyBoxesNonStrict = true;
var properlyBoxesStrict = true;
if (method) {
method.call('foo', function (_, __, context) {
if (typeof context !== 'object') { properlyBoxesNonStrict = false; }
});
method.call([1], function () {
'use strict';
properlyBoxesStrict = typeof this === 'string';
}, 'x');
}
return !!method && properlyBoxesNonStrict && properlyBoxesStrict;
};
defineProperties(ArrayPrototype, {
forEach: function forEach(fun /*, thisp*/) {
var object = toObject(this),
self = splitString && isString(this) ? this.split('') : object,
thisp = arguments[1],
i = -1,
length = self.length >>> 0;
// If no callback function or if callback is not a callable function
if (!isFunction(fun)) {
throw new TypeError(); // TODO message
}
while (++i < length) {
if (i in self) {
// Invoke the callback function with call, passing arguments:
// context, property value, property key, thisArg object
// context
fun.call(thisp, self[i], i, object);
}
}
}
}, !properlyBoxesContext(ArrayPrototype.forEach));
// ES5 15.4.4.14
// http://es5.github.com/#x15.4.4.14
// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/indexOf
var hasFirefox2IndexOfBug = Array.prototype.indexOf && [0, 1].indexOf(1, 2) !== -1;
defineProperties(ArrayPrototype, {
indexOf: function indexOf(sought /*, fromIndex */ ) {
var self = splitString && isString(this) ? this.split('') : toObject(this),
length = self.length >>> 0;
if (!length) {
return -1;
}
var i = 0;
if (arguments.length > 1) {
i = toInteger(arguments[1]);
}
// handle negative indices
i = i >= 0 ? i : Math.max(0, length + i);
for (; i < length; i++) {
if (i in self && self[i] === sought) {
return i;
}
}
return -1;
}
}, hasFirefox2IndexOfBug);
//
// String
// ======
//
// ES5 15.5.4.14
// http://es5.github.com/#x15.5.4.14
// [bugfix, IE lt 9, firefox 4, Konqueror, Opera, obscure browsers]
// Many browsers do not split properly with regular expressions or they
// do not perform the split correctly under obscure conditions.
// See http://blog.stevenlevithan.com/archives/cross-browser-split
// I've tested in many browsers and this seems to cover the deviant ones:
// 'ab'.split(/(?:ab)*/) should be ["", ""], not [""]
// '.'.split(/(.?)(.?)/) should be ["", ".", "", ""], not ["", ""]
// 'tesst'.split(/(s)*/) should be ["t", undefined, "e", "s", "t"], not
// [undefined, "t", undefined, "e", ...]
// ''.split(/.?/) should be [], not [""]
// '.'.split(/()()/) should be ["."], not ["", "", "."]
var string_split = StringPrototype.split;
if (
'ab'.split(/(?:ab)*/).length !== 2 ||
'.'.split(/(.?)(.?)/).length !== 4 ||
'tesst'.split(/(s)*/)[1] === 't' ||
'test'.split(/(?:)/, -1).length !== 4 ||
''.split(/.?/).length ||
'.'.split(/()()/).length > 1
) {
(function () {
var compliantExecNpcg = /()??/.exec('')[1] === void 0; // NPCG: nonparticipating capturing group
StringPrototype.split = function (separator, limit) {
var string = this;
if (separator === void 0 && limit === 0) {
return [];
}
// If `separator` is not a regex, use native split
if (_toString.call(separator) !== '[object RegExp]') {
return string_split.call(this, separator, limit);
}
var output = [],
flags = (separator.ignoreCase ? 'i' : '') +
(separator.multiline ? 'm' : '') +
(separator.extended ? 'x' : '') + // Proposed for ES6
(separator.sticky ? 'y' : ''), // Firefox 3+
lastLastIndex = 0,
// Make `global` and avoid `lastIndex` issues by working with a copy
separator2, match, lastIndex, lastLength;
separator = new RegExp(separator.source, flags + 'g');
string += ''; // Type-convert
if (!compliantExecNpcg) {
// Doesn't need flags gy, but they don't hurt
separator2 = new RegExp('^' + separator.source + '$(?!\\s)', flags);
}
/* Values for `limit`, per the spec:
* If undefined: 4294967295 // Math.pow(2, 32) - 1
* If 0, Infinity, or NaN: 0
* If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296;
* If negative number: 4294967296 - Math.floor(Math.abs(limit))
* If other: Type-convert, then use the above rules
*/
limit = limit === void 0 ?
-1 >>> 0 : // Math.pow(2, 32) - 1
ToUint32(limit);
while (match = separator.exec(string)) {
// `separator.lastIndex` is not reliable cross-browser
lastIndex = match.index + match[0].length;
if (lastIndex > lastLastIndex) {
output.push(string.slice(lastLastIndex, match.index));
// Fix browsers whose `exec` methods don't consistently return `undefined` for
// nonparticipating capturing groups
if (!compliantExecNpcg && match.length > 1) {
match[0].replace(separator2, function () {
for (var i = 1; i < arguments.length - 2; i++) {
if (arguments[i] === void 0) {
match[i] = void 0;
}
}
});
}
if (match.length > 1 && match.index < string.length) {
ArrayPrototype.push.apply(output, match.slice(1));
}
lastLength = match[0].length;
lastLastIndex = lastIndex;
if (output.length >= limit) {
break;
}
}
if (separator.lastIndex === match.index) {
separator.lastIndex++; // Avoid an infinite loop
}
}
if (lastLastIndex === string.length) {
if (lastLength || !separator.test('')) {
output.push('');
}
} else {
output.push(string.slice(lastLastIndex));
}
return output.length > limit ? output.slice(0, limit) : output;
};
}());
// [bugfix, chrome]
// If separator is undefined, then the result array contains just one String,
// which is the this value (converted to a String). If limit is not undefined,
// then the output array is truncated so that it contains no more than limit
// elements.
// "0".split(undefined, 0) -> []
} else if ('0'.split(void 0, 0).length) {
StringPrototype.split = function split(separator, limit) {
if (separator === void 0 && limit === 0) { return []; }
return string_split.call(this, separator, limit);
};
}
// ECMA-262, 3rd B.2.3
// Not an ECMAScript standard, although ECMAScript 3rd Edition has a
// non-normative section suggesting uniform semantics and it should be
// normalized across all browsers
// [bugfix, IE lt 9] IE < 9 substr() with negative value not working in IE
var string_substr = StringPrototype.substr;
var hasNegativeSubstrBug = ''.substr && '0b'.substr(-1) !== 'b';
defineProperties(StringPrototype, {
substr: function substr(start, length) {
return string_substr.call(
this,
start < 0 ? ((start = this.length + start) < 0 ? 0 : start) : start,
length
);
}
}, hasNegativeSubstrBug);

View File

@@ -0,0 +1,18 @@
'use strict';
module.exports = [
// streaming transports
require('./transport/websocket')
, require('./transport/xhr-streaming')
, require('./transport/xdr-streaming')
, require('./transport/eventsource')
, require('./transport/lib/iframe-wrap')(require('./transport/eventsource'))
// polling transports
, require('./transport/htmlfile')
, require('./transport/lib/iframe-wrap')(require('./transport/htmlfile'))
, require('./transport/xhr-polling')
, require('./transport/xdr-polling')
, require('./transport/lib/iframe-wrap')(require('./transport/xhr-polling'))
, require('./transport/jsonp-polling')
];

View File

@@ -0,0 +1,193 @@
'use strict';
var EventEmitter = require('events').EventEmitter
, inherits = require('inherits')
, utils = require('../../utils/event')
, urlUtils = require('../../utils/url')
, XHR = global.XMLHttpRequest
;
var debug = function() {};
if (process.env.NODE_ENV !== 'production') {
debug = require('debug')('sockjs-client:browser:xhr');
}
function AbstractXHRObject(method, url, payload, opts) {
debug(method, url);
var self = this;
EventEmitter.call(this);
setTimeout(function () {
self._start(method, url, payload, opts);
}, 0);
}
inherits(AbstractXHRObject, EventEmitter);
AbstractXHRObject.prototype._start = function(method, url, payload, opts) {
var self = this;
try {
this.xhr = new XHR();
} catch (x) {
// intentionally empty
}
if (!this.xhr) {
debug('no xhr');
this.emit('finish', 0, 'no xhr support');
this._cleanup();
return;
}
// several browsers cache POSTs
url = urlUtils.addQuery(url, 't=' + (+new Date()));
// Explorer tends to keep connection open, even after the
// tab gets closed: http://bugs.jquery.com/ticket/5280
this.unloadRef = utils.unloadAdd(function() {
debug('unload cleanup');
self._cleanup(true);
});
try {
this.xhr.open(method, url, true);
if (this.timeout && 'timeout' in this.xhr) {
this.xhr.timeout = this.timeout;
this.xhr.ontimeout = function() {
debug('xhr timeout');
self.emit('finish', 0, '');
self._cleanup(false);
};
}
} catch (e) {
debug('exception', e);
// IE raises an exception on wrong port.
this.emit('finish', 0, '');
this._cleanup(false);
return;
}
if ((!opts || !opts.noCredentials) && AbstractXHRObject.supportsCORS) {
debug('withCredentials');
// Mozilla docs says https://developer.mozilla.org/en/XMLHttpRequest :
// "This never affects same-site requests."
this.xhr.withCredentials = true;
}
if (opts && opts.headers) {
for (var key in opts.headers) {
this.xhr.setRequestHeader(key, opts.headers[key]);
}
}
this.xhr.onreadystatechange = function() {
if (self.xhr) {
var x = self.xhr;
var text, status;
debug('readyState', x.readyState);
switch (x.readyState) {
case 3:
// IE doesn't like peeking into responseText or status
// on Microsoft.XMLHTTP and readystate=3
try {
status = x.status;
text = x.responseText;
} catch (e) {
// intentionally empty
}
debug('status', status);
// IE returns 1223 for 204: http://bugs.jquery.com/ticket/1450
if (status === 1223) {
status = 204;
}
// IE does return readystate == 3 for 404 answers.
if (status === 200 && text && text.length > 0) {
debug('chunk');
self.emit('chunk', status, text);
}
break;
case 4:
status = x.status;
debug('status', status);
// IE returns 1223 for 204: http://bugs.jquery.com/ticket/1450
if (status === 1223) {
status = 204;
}
// IE returns this for a bad port
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa383770(v=vs.85).aspx
if (status === 12005 || status === 12029) {
status = 0;
}
debug('finish', status, x.responseText);
self.emit('finish', status, x.responseText);
self._cleanup(false);
break;
}
}
};
try {
self.xhr.send(payload);
} catch (e) {
self.emit('finish', 0, '');
self._cleanup(false);
}
};
AbstractXHRObject.prototype._cleanup = function(abort) {
debug('cleanup');
if (!this.xhr) {
return;
}
this.removeAllListeners();
utils.unloadDel(this.unloadRef);
// IE needs this field to be a function
this.xhr.onreadystatechange = function() {};
if (this.xhr.ontimeout) {
this.xhr.ontimeout = null;
}
if (abort) {
try {
this.xhr.abort();
} catch (x) {
// intentionally empty
}
}
this.unloadRef = this.xhr = null;
};
AbstractXHRObject.prototype.close = function() {
debug('close');
this._cleanup(true);
};
AbstractXHRObject.enabled = !!XHR;
// override XMLHttpRequest for IE6/7
// obfuscate to avoid firewalls
var axo = ['Active'].concat('Object').join('X');
if (!AbstractXHRObject.enabled && (axo in global)) {
debug('overriding xmlhttprequest');
XHR = function() {
try {
return new global[axo]('Microsoft.XMLHTTP');
} catch (e) {
return null;
}
};
AbstractXHRObject.enabled = !!new XHR();
}
var cors = false;
try {
cors = 'withCredentials' in new XHR();
} catch (ignored) {
// intentionally empty
}
AbstractXHRObject.supportsCORS = cors;
module.exports = AbstractXHRObject;

View File

@@ -0,0 +1 @@
module.exports = global.EventSource;

View File

@@ -0,0 +1,10 @@
'use strict';
var Driver = global.WebSocket || global.MozWebSocket;
if (Driver) {
module.exports = function WebSocketBrowserDriver(url) {
return new Driver(url);
};
} else {
module.exports = undefined;
}

View File

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

View File

@@ -0,0 +1,72 @@
'use strict';
var EventEmitter = require('events').EventEmitter
, inherits = require('inherits')
, http = require('http')
, https = require('https')
, URL = require('url-parse')
;
var debug = function() {};
if (process.env.NODE_ENV !== 'production') {
debug = require('debug')('sockjs-client:driver:xhr');
}
function XhrDriver(method, url, payload, opts) {
debug(method, url, payload);
var self = this;
EventEmitter.call(this);
var parsedUrl = new URL(url);
var options = {
method: method
, hostname: parsedUrl.hostname.replace(/\[|\]/g, '')
, port: parsedUrl.port
, path: parsedUrl.pathname + (parsedUrl.query || '')
, headers: opts && opts.headers
, agent: false
};
var protocol = parsedUrl.protocol === 'https:' ? https : http;
this.req = protocol.request(options, function(res) {
res.setEncoding('utf8');
var responseText = '';
res.on('data', function(chunk) {
debug('data', chunk);
responseText += chunk;
self.emit('chunk', 200, responseText);
});
res.once('end', function() {
debug('end');
self.emit('finish', res.statusCode, responseText);
self.req = null;
});
});
this.req.on('error', function(e) {
debug('error', e);
self.emit('finish', 0, e.message);
});
if (payload) {
this.req.write(payload);
}
this.req.end();
}
inherits(XhrDriver, EventEmitter);
XhrDriver.prototype.close = function() {
debug('close');
this.removeAllListeners();
if (this.req) {
this.req.abort();
this.req = null;
}
};
XhrDriver.enabled = true;
XhrDriver.supportsCORS = true;
module.exports = XhrDriver;

View File

@@ -0,0 +1,27 @@
'use strict';
var inherits = require('inherits')
, AjaxBasedTransport = require('./lib/ajax-based')
, EventSourceReceiver = require('./receiver/eventsource')
, XHRCorsObject = require('./sender/xhr-cors')
, EventSourceDriver = require('eventsource')
;
function EventSourceTransport(transUrl) {
if (!EventSourceTransport.enabled()) {
throw new Error('Transport created when disabled');
}
AjaxBasedTransport.call(this, transUrl, '/eventsource', EventSourceReceiver, XHRCorsObject);
}
inherits(EventSourceTransport, AjaxBasedTransport);
EventSourceTransport.enabled = function() {
return !!EventSourceDriver;
};
EventSourceTransport.transportName = 'eventsource';
EventSourceTransport.roundTrips = 2;
module.exports = EventSourceTransport;

View File

@@ -0,0 +1,25 @@
'use strict';
var inherits = require('inherits')
, HtmlfileReceiver = require('./receiver/htmlfile')
, XHRLocalObject = require('./sender/xhr-local')
, AjaxBasedTransport = require('./lib/ajax-based')
;
function HtmlFileTransport(transUrl) {
if (!HtmlfileReceiver.enabled) {
throw new Error('Transport created when disabled');
}
AjaxBasedTransport.call(this, transUrl, '/htmlfile', HtmlfileReceiver, XHRLocalObject);
}
inherits(HtmlFileTransport, AjaxBasedTransport);
HtmlFileTransport.enabled = function(info) {
return HtmlfileReceiver.enabled && info.sameOrigin;
};
HtmlFileTransport.transportName = 'htmlfile';
HtmlFileTransport.roundTrips = 2;
module.exports = HtmlFileTransport;

View File

@@ -0,0 +1,141 @@
'use strict';
// Few cool transports do work only for same-origin. In order to make
// them work cross-domain we shall use iframe, served from the
// remote domain. New browsers have capabilities to communicate with
// cross domain iframe using postMessage(). In IE it was implemented
// from IE 8+, but of course, IE got some details wrong:
// http://msdn.microsoft.com/en-us/library/cc197015(v=VS.85).aspx
// http://stevesouders.com/misc/test-postmessage.php
var inherits = require('inherits')
, JSON3 = require('json3')
, EventEmitter = require('events').EventEmitter
, version = require('../version')
, urlUtils = require('../utils/url')
, iframeUtils = require('../utils/iframe')
, eventUtils = require('../utils/event')
, random = require('../utils/random')
;
var debug = function() {};
if (process.env.NODE_ENV !== 'production') {
debug = require('debug')('sockjs-client:transport:iframe');
}
function IframeTransport(transport, transUrl, baseUrl) {
if (!IframeTransport.enabled()) {
throw new Error('Transport created when disabled');
}
EventEmitter.call(this);
var self = this;
this.origin = urlUtils.getOrigin(baseUrl);
this.baseUrl = baseUrl;
this.transUrl = transUrl;
this.transport = transport;
this.windowId = random.string(8);
var iframeUrl = urlUtils.addPath(baseUrl, '/iframe.html') + '#' + this.windowId;
debug(transport, transUrl, iframeUrl);
this.iframeObj = iframeUtils.createIframe(iframeUrl, function(r) {
debug('err callback');
self.emit('close', 1006, 'Unable to load an iframe (' + r + ')');
self.close();
});
this.onmessageCallback = this._message.bind(this);
eventUtils.attachEvent('message', this.onmessageCallback);
}
inherits(IframeTransport, EventEmitter);
IframeTransport.prototype.close = function() {
debug('close');
this.removeAllListeners();
if (this.iframeObj) {
eventUtils.detachEvent('message', this.onmessageCallback);
try {
// When the iframe is not loaded, IE raises an exception
// on 'contentWindow'.
this.postMessage('c');
} catch (x) {
// intentionally empty
}
this.iframeObj.cleanup();
this.iframeObj = null;
this.onmessageCallback = this.iframeObj = null;
}
};
IframeTransport.prototype._message = function(e) {
debug('message', e.data);
if (!urlUtils.isOriginEqual(e.origin, this.origin)) {
debug('not same origin', e.origin, this.origin);
return;
}
var iframeMessage;
try {
iframeMessage = JSON3.parse(e.data);
} catch (ignored) {
debug('bad json', e.data);
return;
}
if (iframeMessage.windowId !== this.windowId) {
debug('mismatched window id', iframeMessage.windowId, this.windowId);
return;
}
switch (iframeMessage.type) {
case 's':
this.iframeObj.loaded();
// window global dependency
this.postMessage('s', JSON3.stringify([
version
, this.transport
, this.transUrl
, this.baseUrl
]));
break;
case 't':
this.emit('message', iframeMessage.data);
break;
case 'c':
var cdata;
try {
cdata = JSON3.parse(iframeMessage.data);
} catch (ignored) {
debug('bad json', iframeMessage.data);
return;
}
this.emit('close', cdata[0], cdata[1]);
this.close();
break;
}
};
IframeTransport.prototype.postMessage = function(type, data) {
debug('postMessage', type, data);
this.iframeObj.post(JSON3.stringify({
windowId: this.windowId
, type: type
, data: data || ''
}), this.origin);
};
IframeTransport.prototype.send = function(message) {
debug('send', message);
this.postMessage('m', message);
};
IframeTransport.enabled = function() {
return iframeUtils.iframeEnabled;
};
IframeTransport.transportName = 'iframe';
IframeTransport.roundTrips = 2;
module.exports = IframeTransport;

View File

@@ -0,0 +1,34 @@
'use strict';
// The simplest and most robust transport, using the well-know cross
// domain hack - JSONP. This transport is quite inefficient - one
// message could use up to one http request. But at least it works almost
// everywhere.
// Known limitations:
// o you will get a spinning cursor
// o for Konqueror a dumb timer is needed to detect errors
var inherits = require('inherits')
, SenderReceiver = require('./lib/sender-receiver')
, JsonpReceiver = require('./receiver/jsonp')
, jsonpSender = require('./sender/jsonp')
;
function JsonPTransport(transUrl) {
if (!JsonPTransport.enabled()) {
throw new Error('Transport created when disabled');
}
SenderReceiver.call(this, transUrl, '/jsonp', jsonpSender, JsonpReceiver);
}
inherits(JsonPTransport, SenderReceiver);
JsonPTransport.enabled = function() {
return !!global.document;
};
JsonPTransport.transportName = 'jsonp-polling';
JsonPTransport.roundTrips = 1;
JsonPTransport.needBody = true;
module.exports = JsonPTransport;

View File

@@ -0,0 +1,49 @@
'use strict';
var inherits = require('inherits')
, urlUtils = require('../../utils/url')
, SenderReceiver = require('./sender-receiver')
;
var debug = function() {};
if (process.env.NODE_ENV !== 'production') {
debug = require('debug')('sockjs-client:ajax-based');
}
function createAjaxSender(AjaxObject) {
return function(url, payload, callback) {
debug('create ajax sender', url, payload);
var opt = {};
if (typeof payload === 'string') {
opt.headers = {'Content-type': 'text/plain'};
}
var ajaxUrl = urlUtils.addPath(url, '/xhr_send');
var xo = new AjaxObject('POST', ajaxUrl, payload, opt);
xo.once('finish', function(status) {
debug('finish', status);
xo = null;
if (status !== 200 && status !== 204) {
return callback(new Error('http status ' + status));
}
callback();
});
return function() {
debug('abort');
xo.close();
xo = null;
var err = new Error('Aborted');
err.code = 1000;
callback(err);
};
};
}
function AjaxBasedTransport(transUrl, urlSuffix, Receiver, AjaxObject) {
SenderReceiver.call(this, transUrl, urlSuffix, createAjaxSender(AjaxObject), Receiver, AjaxObject);
}
inherits(AjaxBasedTransport, SenderReceiver);
module.exports = AjaxBasedTransport;

View File

@@ -0,0 +1,87 @@
'use strict';
var inherits = require('inherits')
, EventEmitter = require('events').EventEmitter
;
var debug = function() {};
if (process.env.NODE_ENV !== 'production') {
debug = require('debug')('sockjs-client:buffered-sender');
}
function BufferedSender(url, sender) {
debug(url);
EventEmitter.call(this);
this.sendBuffer = [];
this.sender = sender;
this.url = url;
}
inherits(BufferedSender, EventEmitter);
BufferedSender.prototype.send = function(message) {
debug('send', message);
this.sendBuffer.push(message);
if (!this.sendStop) {
this.sendSchedule();
}
};
// For polling transports in a situation when in the message callback,
// new message is being send. If the sending connection was started
// before receiving one, it is possible to saturate the network and
// timeout due to the lack of receiving socket. To avoid that we delay
// sending messages by some small time, in order to let receiving
// connection be started beforehand. This is only a halfmeasure and
// does not fix the big problem, but it does make the tests go more
// stable on slow networks.
BufferedSender.prototype.sendScheduleWait = function() {
debug('sendScheduleWait');
var self = this;
var tref;
this.sendStop = function() {
debug('sendStop');
self.sendStop = null;
clearTimeout(tref);
};
tref = setTimeout(function() {
debug('timeout');
self.sendStop = null;
self.sendSchedule();
}, 25);
};
BufferedSender.prototype.sendSchedule = function() {
debug('sendSchedule', this.sendBuffer.length);
var self = this;
if (this.sendBuffer.length > 0) {
var payload = '[' + this.sendBuffer.join(',') + ']';
this.sendStop = this.sender(this.url, payload, function(err) {
self.sendStop = null;
if (err) {
debug('error', err);
self.emit('close', err.code || 1006, 'Sending error: ' + err);
self.close();
} else {
self.sendScheduleWait();
}
});
this.sendBuffer = [];
}
};
BufferedSender.prototype._cleanup = function() {
debug('_cleanup');
this.removeAllListeners();
};
BufferedSender.prototype.close = function() {
debug('close');
this._cleanup();
if (this.sendStop) {
this.sendStop();
this.sendStop = null;
}
};
module.exports = BufferedSender;

View File

@@ -0,0 +1,33 @@
'use strict';
var inherits = require('inherits')
, IframeTransport = require('../iframe')
, objectUtils = require('../../utils/object')
;
module.exports = function(transport) {
function IframeWrapTransport(transUrl, baseUrl) {
IframeTransport.call(this, transport.transportName, transUrl, baseUrl);
}
inherits(IframeWrapTransport, IframeTransport);
IframeWrapTransport.enabled = function(url, info) {
if (!global.document) {
return false;
}
var iframeInfo = objectUtils.extend({}, info);
iframeInfo.sameOrigin = true;
return transport.enabled(iframeInfo) && IframeTransport.enabled();
};
IframeWrapTransport.transportName = 'iframe-' + transport.transportName;
IframeWrapTransport.needBody = true;
IframeWrapTransport.roundTrips = IframeTransport.roundTrips + transport.roundTrips - 1; // html, javascript (2) + transport - no CORS (1)
IframeWrapTransport.facadeTransport = transport;
return IframeWrapTransport;
};

View File

@@ -0,0 +1,57 @@
'use strict';
var inherits = require('inherits')
, EventEmitter = require('events').EventEmitter
;
var debug = function() {};
if (process.env.NODE_ENV !== 'production') {
debug = require('debug')('sockjs-client:polling');
}
function Polling(Receiver, receiveUrl, AjaxObject) {
debug(receiveUrl);
EventEmitter.call(this);
this.Receiver = Receiver;
this.receiveUrl = receiveUrl;
this.AjaxObject = AjaxObject;
this._scheduleReceiver();
}
inherits(Polling, EventEmitter);
Polling.prototype._scheduleReceiver = function() {
debug('_scheduleReceiver');
var self = this;
var poll = this.poll = new this.Receiver(this.receiveUrl, this.AjaxObject);
poll.on('message', function(msg) {
debug('message', msg);
self.emit('message', msg);
});
poll.once('close', function(code, reason) {
debug('close', code, reason, self.pollIsClosing);
self.poll = poll = null;
if (!self.pollIsClosing) {
if (reason === 'network') {
self._scheduleReceiver();
} else {
self.emit('close', code || 1006, reason);
self.removeAllListeners();
}
}
});
};
Polling.prototype.abort = function() {
debug('abort');
this.removeAllListeners();
this.pollIsClosing = true;
if (this.poll) {
this.poll.abort();
}
};
module.exports = Polling;

View File

@@ -0,0 +1,45 @@
'use strict';
var inherits = require('inherits')
, urlUtils = require('../../utils/url')
, BufferedSender = require('./buffered-sender')
, Polling = require('./polling')
;
var debug = function() {};
if (process.env.NODE_ENV !== 'production') {
debug = require('debug')('sockjs-client:sender-receiver');
}
function SenderReceiver(transUrl, urlSuffix, senderFunc, Receiver, AjaxObject) {
var pollUrl = urlUtils.addPath(transUrl, urlSuffix);
debug(pollUrl);
var self = this;
BufferedSender.call(this, transUrl, senderFunc);
this.poll = new Polling(Receiver, pollUrl, AjaxObject);
this.poll.on('message', function(msg) {
debug('poll message', msg);
self.emit('message', msg);
});
this.poll.once('close', function(code, reason) {
debug('poll close', code, reason);
self.poll = null;
self.emit('close', code, reason);
self.close();
});
}
inherits(SenderReceiver, BufferedSender);
SenderReceiver.prototype.close = function() {
BufferedSender.prototype.close.call(this);
debug('close');
this.removeAllListeners();
if (this.poll) {
this.poll.abort();
this.poll = null;
}
};
module.exports = SenderReceiver;

View File

@@ -0,0 +1,63 @@
'use strict';
var inherits = require('inherits')
, EventEmitter = require('events').EventEmitter
, EventSourceDriver = require('eventsource')
;
var debug = function() {};
if (process.env.NODE_ENV !== 'production') {
debug = require('debug')('sockjs-client:receiver:eventsource');
}
function EventSourceReceiver(url) {
debug(url);
EventEmitter.call(this);
var self = this;
var es = this.es = new EventSourceDriver(url);
es.onmessage = function(e) {
debug('message', e.data);
self.emit('message', decodeURI(e.data));
};
es.onerror = function(e) {
debug('error', es.readyState, e);
// ES on reconnection has readyState = 0 or 1.
// on network error it's CLOSED = 2
var reason = (es.readyState !== 2 ? 'network' : 'permanent');
self._cleanup();
self._close(reason);
};
}
inherits(EventSourceReceiver, EventEmitter);
EventSourceReceiver.prototype.abort = function() {
debug('abort');
this._cleanup();
this._close('user');
};
EventSourceReceiver.prototype._cleanup = function() {
debug('cleanup');
var es = this.es;
if (es) {
es.onmessage = es.onerror = null;
es.close();
this.es = null;
}
};
EventSourceReceiver.prototype._close = function(reason) {
debug('close', reason);
var self = this;
// Safari and chrome < 15 crash if we close window before
// waiting for ES cleanup. See:
// https://code.google.com/p/chromium/issues/detail?id=89155
setTimeout(function() {
self.emit('close', null, reason);
self.removeAllListeners();
}, 200);
};
module.exports = EventSourceReceiver;

View File

@@ -0,0 +1,87 @@
'use strict';
var inherits = require('inherits')
, iframeUtils = require('../../utils/iframe')
, urlUtils = require('../../utils/url')
, EventEmitter = require('events').EventEmitter
, random = require('../../utils/random')
;
var debug = function() {};
if (process.env.NODE_ENV !== 'production') {
debug = require('debug')('sockjs-client:receiver:htmlfile');
}
function HtmlfileReceiver(url) {
debug(url);
EventEmitter.call(this);
var self = this;
iframeUtils.polluteGlobalNamespace();
this.id = 'a' + random.string(6);
url = urlUtils.addQuery(url, 'c=' + decodeURIComponent(iframeUtils.WPrefix + '.' + this.id));
debug('using htmlfile', HtmlfileReceiver.htmlfileEnabled);
var constructFunc = HtmlfileReceiver.htmlfileEnabled ?
iframeUtils.createHtmlfile : iframeUtils.createIframe;
global[iframeUtils.WPrefix][this.id] = {
start: function() {
debug('start');
self.iframeObj.loaded();
}
, message: function(data) {
debug('message', data);
self.emit('message', data);
}
, stop: function() {
debug('stop');
self._cleanup();
self._close('network');
}
};
this.iframeObj = constructFunc(url, function() {
debug('callback');
self._cleanup();
self._close('permanent');
});
}
inherits(HtmlfileReceiver, EventEmitter);
HtmlfileReceiver.prototype.abort = function() {
debug('abort');
this._cleanup();
this._close('user');
};
HtmlfileReceiver.prototype._cleanup = function() {
debug('_cleanup');
if (this.iframeObj) {
this.iframeObj.cleanup();
this.iframeObj = null;
}
delete global[iframeUtils.WPrefix][this.id];
};
HtmlfileReceiver.prototype._close = function(reason) {
debug('_close', reason);
this.emit('close', null, reason);
this.removeAllListeners();
};
HtmlfileReceiver.htmlfileEnabled = false;
// obfuscate to avoid firewalls
var axo = ['Active'].concat('Object').join('X');
if (axo in global) {
try {
HtmlfileReceiver.htmlfileEnabled = !!new global[axo]('htmlfile');
} catch (x) {
// intentionally empty
}
}
HtmlfileReceiver.enabled = HtmlfileReceiver.htmlfileEnabled || iframeUtils.iframeEnabled;
module.exports = HtmlfileReceiver;

View File

@@ -0,0 +1,183 @@
'use strict';
var utils = require('../../utils/iframe')
, random = require('../../utils/random')
, browser = require('../../utils/browser')
, urlUtils = require('../../utils/url')
, inherits = require('inherits')
, EventEmitter = require('events').EventEmitter
;
var debug = function() {};
if (process.env.NODE_ENV !== 'production') {
debug = require('debug')('sockjs-client:receiver:jsonp');
}
function JsonpReceiver(url) {
debug(url);
var self = this;
EventEmitter.call(this);
utils.polluteGlobalNamespace();
this.id = 'a' + random.string(6);
var urlWithId = urlUtils.addQuery(url, 'c=' + encodeURIComponent(utils.WPrefix + '.' + this.id));
global[utils.WPrefix][this.id] = this._callback.bind(this);
this._createScript(urlWithId);
// Fallback mostly for Konqueror - stupid timer, 35 seconds shall be plenty.
this.timeoutId = setTimeout(function() {
debug('timeout');
self._abort(new Error('JSONP script loaded abnormally (timeout)'));
}, JsonpReceiver.timeout);
}
inherits(JsonpReceiver, EventEmitter);
JsonpReceiver.prototype.abort = function() {
debug('abort');
if (global[utils.WPrefix][this.id]) {
var err = new Error('JSONP user aborted read');
err.code = 1000;
this._abort(err);
}
};
JsonpReceiver.timeout = 35000;
JsonpReceiver.scriptErrorTimeout = 1000;
JsonpReceiver.prototype._callback = function(data) {
debug('_callback', data);
this._cleanup();
if (this.aborting) {
return;
}
if (data) {
debug('message', data);
this.emit('message', data);
}
this.emit('close', null, 'network');
this.removeAllListeners();
};
JsonpReceiver.prototype._abort = function(err) {
debug('_abort', err);
this._cleanup();
this.aborting = true;
this.emit('close', err.code, err.message);
this.removeAllListeners();
};
JsonpReceiver.prototype._cleanup = function() {
debug('_cleanup');
clearTimeout(this.timeoutId);
if (this.script2) {
this.script2.parentNode.removeChild(this.script2);
this.script2 = null;
}
if (this.script) {
var script = this.script;
// Unfortunately, you can't really abort script loading of
// the script.
script.parentNode.removeChild(script);
script.onreadystatechange = script.onerror =
script.onload = script.onclick = null;
this.script = null;
}
delete global[utils.WPrefix][this.id];
};
JsonpReceiver.prototype._scriptError = function() {
debug('_scriptError');
var self = this;
if (this.errorTimer) {
return;
}
this.errorTimer = setTimeout(function() {
if (!self.loadedOkay) {
self._abort(new Error('JSONP script loaded abnormally (onerror)'));
}
}, JsonpReceiver.scriptErrorTimeout);
};
JsonpReceiver.prototype._createScript = function(url) {
debug('_createScript', url);
var self = this;
var script = this.script = global.document.createElement('script');
var script2; // Opera synchronous load trick.
script.id = 'a' + random.string(8);
script.src = url;
script.type = 'text/javascript';
script.charset = 'UTF-8';
script.onerror = this._scriptError.bind(this);
script.onload = function() {
debug('onload');
self._abort(new Error('JSONP script loaded abnormally (onload)'));
};
// IE9 fires 'error' event after onreadystatechange or before, in random order.
// Use loadedOkay to determine if actually errored
script.onreadystatechange = function() {
debug('onreadystatechange', script.readyState);
if (/loaded|closed/.test(script.readyState)) {
if (script && script.htmlFor && script.onclick) {
self.loadedOkay = true;
try {
// In IE, actually execute the script.
script.onclick();
} catch (x) {
// intentionally empty
}
}
if (script) {
self._abort(new Error('JSONP script loaded abnormally (onreadystatechange)'));
}
}
};
// IE: event/htmlFor/onclick trick.
// One can't rely on proper order for onreadystatechange. In order to
// make sure, set a 'htmlFor' and 'event' properties, so that
// script code will be installed as 'onclick' handler for the
// script object. Later, onreadystatechange, manually execute this
// code. FF and Chrome doesn't work with 'event' and 'htmlFor'
// set. For reference see:
// http://jaubourg.net/2010/07/loading-script-as-onclick-handler-of.html
// Also, read on that about script ordering:
// http://wiki.whatwg.org/wiki/Dynamic_Script_Execution_Order
if (typeof script.async === 'undefined' && global.document.attachEvent) {
// According to mozilla docs, in recent browsers script.async defaults
// to 'true', so we may use it to detect a good browser:
// https://developer.mozilla.org/en/HTML/Element/script
if (!browser.isOpera()) {
// Naively assume we're in IE
try {
script.htmlFor = script.id;
script.event = 'onclick';
} catch (x) {
// intentionally empty
}
script.async = true;
} else {
// Opera, second sync script hack
script2 = this.script2 = global.document.createElement('script');
script2.text = "try{var a = document.getElementById('" + script.id + "'); if(a)a.onerror();}catch(x){};";
script.async = script2.async = false;
}
}
if (typeof script.async !== 'undefined') {
script.async = true;
}
var head = global.document.getElementsByTagName('head')[0];
head.insertBefore(script, head.firstChild);
if (script2) {
head.insertBefore(script2, head.firstChild);
}
};
module.exports = JsonpReceiver;

View File

@@ -0,0 +1,70 @@
'use strict';
var inherits = require('inherits')
, EventEmitter = require('events').EventEmitter
;
var debug = function() {};
if (process.env.NODE_ENV !== 'production') {
debug = require('debug')('sockjs-client:receiver:xhr');
}
function XhrReceiver(url, AjaxObject) {
debug(url);
EventEmitter.call(this);
var self = this;
this.bufferPosition = 0;
this.xo = new AjaxObject('POST', url, null);
this.xo.on('chunk', this._chunkHandler.bind(this));
this.xo.once('finish', function(status, text) {
debug('finish', status, text);
self._chunkHandler(status, text);
self.xo = null;
var reason = status === 200 ? 'network' : 'permanent';
debug('close', reason);
self.emit('close', null, reason);
self._cleanup();
});
}
inherits(XhrReceiver, EventEmitter);
XhrReceiver.prototype._chunkHandler = function(status, text) {
debug('_chunkHandler', status);
if (status !== 200 || !text) {
return;
}
for (var idx = -1; ; this.bufferPosition += idx + 1) {
var buf = text.slice(this.bufferPosition);
idx = buf.indexOf('\n');
if (idx === -1) {
break;
}
var msg = buf.slice(0, idx);
if (msg) {
debug('message', msg);
this.emit('message', msg);
}
}
};
XhrReceiver.prototype._cleanup = function() {
debug('_cleanup');
this.removeAllListeners();
};
XhrReceiver.prototype.abort = function() {
debug('abort');
if (this.xo) {
this.xo.close();
debug('close');
this.emit('close', null, 'user');
this.xo = null;
}
this._cleanup();
};
module.exports = XhrReceiver;

View File

@@ -0,0 +1,99 @@
'use strict';
var random = require('../../utils/random')
, urlUtils = require('../../utils/url')
;
var debug = function() {};
if (process.env.NODE_ENV !== 'production') {
debug = require('debug')('sockjs-client:sender:jsonp');
}
var form, area;
function createIframe(id) {
debug('createIframe', id);
try {
// ie6 dynamic iframes with target="" support (thanks Chris Lambacher)
return global.document.createElement('<iframe name="' + id + '">');
} catch (x) {
var iframe = global.document.createElement('iframe');
iframe.name = id;
return iframe;
}
}
function createForm() {
debug('createForm');
form = global.document.createElement('form');
form.style.display = 'none';
form.style.position = 'absolute';
form.method = 'POST';
form.enctype = 'application/x-www-form-urlencoded';
form.acceptCharset = 'UTF-8';
area = global.document.createElement('textarea');
area.name = 'd';
form.appendChild(area);
global.document.body.appendChild(form);
}
module.exports = function(url, payload, callback) {
debug(url, payload);
if (!form) {
createForm();
}
var id = 'a' + random.string(8);
form.target = id;
form.action = urlUtils.addQuery(urlUtils.addPath(url, '/jsonp_send'), 'i=' + id);
var iframe = createIframe(id);
iframe.id = id;
iframe.style.display = 'none';
form.appendChild(iframe);
try {
area.value = payload;
} catch (e) {
// seriously broken browsers get here
}
form.submit();
var completed = function(err) {
debug('completed', id, err);
if (!iframe.onerror) {
return;
}
iframe.onreadystatechange = iframe.onerror = iframe.onload = null;
// Opera mini doesn't like if we GC iframe
// immediately, thus this timeout.
setTimeout(function() {
debug('cleaning up', id);
iframe.parentNode.removeChild(iframe);
iframe = null;
}, 500);
area.value = '';
// It is not possible to detect if the iframe succeeded or
// failed to submit our form.
callback(err);
};
iframe.onerror = function() {
debug('onerror', id);
completed();
};
iframe.onload = function() {
debug('onload', id);
completed();
};
iframe.onreadystatechange = function(e) {
debug('onreadystatechange', id, iframe.readyState, e);
if (iframe.readyState === 'complete') {
completed();
}
};
return function() {
debug('aborted', id);
completed(new Error('Aborted'));
};
};

View File

@@ -0,0 +1,103 @@
'use strict';
var EventEmitter = require('events').EventEmitter
, inherits = require('inherits')
, eventUtils = require('../../utils/event')
, browser = require('../../utils/browser')
, urlUtils = require('../../utils/url')
;
var debug = function() {};
if (process.env.NODE_ENV !== 'production') {
debug = require('debug')('sockjs-client:sender:xdr');
}
// References:
// http://ajaxian.com/archives/100-line-ajax-wrapper
// http://msdn.microsoft.com/en-us/library/cc288060(v=VS.85).aspx
function XDRObject(method, url, payload) {
debug(method, url);
var self = this;
EventEmitter.call(this);
setTimeout(function() {
self._start(method, url, payload);
}, 0);
}
inherits(XDRObject, EventEmitter);
XDRObject.prototype._start = function(method, url, payload) {
debug('_start');
var self = this;
var xdr = new global.XDomainRequest();
// IE caches even POSTs
url = urlUtils.addQuery(url, 't=' + (+new Date()));
xdr.onerror = function() {
debug('onerror');
self._error();
};
xdr.ontimeout = function() {
debug('ontimeout');
self._error();
};
xdr.onprogress = function() {
debug('progress', xdr.responseText);
self.emit('chunk', 200, xdr.responseText);
};
xdr.onload = function() {
debug('load');
self.emit('finish', 200, xdr.responseText);
self._cleanup(false);
};
this.xdr = xdr;
this.unloadRef = eventUtils.unloadAdd(function() {
self._cleanup(true);
});
try {
// Fails with AccessDenied if port number is bogus
this.xdr.open(method, url);
if (this.timeout) {
this.xdr.timeout = this.timeout;
}
this.xdr.send(payload);
} catch (x) {
this._error();
}
};
XDRObject.prototype._error = function() {
this.emit('finish', 0, '');
this._cleanup(false);
};
XDRObject.prototype._cleanup = function(abort) {
debug('cleanup', abort);
if (!this.xdr) {
return;
}
this.removeAllListeners();
eventUtils.unloadDel(this.unloadRef);
this.xdr.ontimeout = this.xdr.onerror = this.xdr.onprogress = this.xdr.onload = null;
if (abort) {
try {
this.xdr.abort();
} catch (x) {
// intentionally empty
}
}
this.unloadRef = this.xdr = null;
};
XDRObject.prototype.close = function() {
debug('close');
this._cleanup(true);
};
// IE 8/9 if the request target uses the same scheme - #79
XDRObject.enabled = !!(global.XDomainRequest && browser.hasDomain());
module.exports = XDRObject;

View File

@@ -0,0 +1,15 @@
'use strict';
var inherits = require('inherits')
, XhrDriver = require('../driver/xhr')
;
function XHRCorsObject(method, url, payload, opts) {
XhrDriver.call(this, method, url, payload, opts);
}
inherits(XHRCorsObject, XhrDriver);
XHRCorsObject.enabled = XhrDriver.enabled && XhrDriver.supportsCORS;
module.exports = XHRCorsObject;

View File

@@ -0,0 +1,24 @@
'use strict';
var EventEmitter = require('events').EventEmitter
, inherits = require('inherits')
;
function XHRFake(/* method, url, payload, opts */) {
var self = this;
EventEmitter.call(this);
this.to = setTimeout(function() {
self.emit('finish', 200, '{}');
}, XHRFake.timeout);
}
inherits(XHRFake, EventEmitter);
XHRFake.prototype.close = function() {
clearTimeout(this.to);
};
XHRFake.timeout = 2000;
module.exports = XHRFake;

View File

@@ -0,0 +1,17 @@
'use strict';
var inherits = require('inherits')
, XhrDriver = require('../driver/xhr')
;
function XHRLocalObject(method, url, payload /*, opts */) {
XhrDriver.call(this, method, url, payload, {
noCredentials: true
});
}
inherits(XHRLocalObject, XhrDriver);
XHRLocalObject.enabled = XhrDriver.enabled;
module.exports = XHRLocalObject;

View File

@@ -0,0 +1,99 @@
'use strict';
var utils = require('../utils/event')
, urlUtils = require('../utils/url')
, inherits = require('inherits')
, EventEmitter = require('events').EventEmitter
, WebsocketDriver = require('./driver/websocket')
;
var debug = function() {};
if (process.env.NODE_ENV !== 'production') {
debug = require('debug')('sockjs-client:websocket');
}
function WebSocketTransport(transUrl, ignore, options) {
if (!WebSocketTransport.enabled()) {
throw new Error('Transport created when disabled');
}
EventEmitter.call(this);
debug('constructor', transUrl);
var self = this;
var url = urlUtils.addPath(transUrl, '/websocket');
if (url.slice(0, 5) === 'https') {
url = 'wss' + url.slice(5);
} else {
url = 'ws' + url.slice(4);
}
this.url = url;
this.ws = new WebsocketDriver(this.url, [], options);
this.ws.onmessage = function(e) {
debug('message event', e.data);
self.emit('message', e.data);
};
// Firefox has an interesting bug. If a websocket connection is
// created after onunload, it stays alive even when user
// navigates away from the page. In such situation let's lie -
// let's not open the ws connection at all. See:
// https://github.com/sockjs/sockjs-client/issues/28
// https://bugzilla.mozilla.org/show_bug.cgi?id=696085
this.unloadRef = utils.unloadAdd(function() {
debug('unload');
self.ws.close();
});
this.ws.onclose = function(e) {
debug('close event', e.code, e.reason);
self.emit('close', e.code, e.reason);
self._cleanup();
};
this.ws.onerror = function(e) {
debug('error event', e);
self.emit('close', 1006, 'WebSocket connection broken');
self._cleanup();
};
}
inherits(WebSocketTransport, EventEmitter);
WebSocketTransport.prototype.send = function(data) {
var msg = '[' + data + ']';
debug('send', msg);
this.ws.send(msg);
};
WebSocketTransport.prototype.close = function() {
debug('close');
var ws = this.ws;
this._cleanup();
if (ws) {
ws.close();
}
};
WebSocketTransport.prototype._cleanup = function() {
debug('_cleanup');
var ws = this.ws;
if (ws) {
ws.onmessage = ws.onclose = ws.onerror = null;
}
utils.unloadDel(this.unloadRef);
this.unloadRef = this.ws = null;
this.removeAllListeners();
};
WebSocketTransport.enabled = function() {
debug('enabled');
return !!WebsocketDriver;
};
WebSocketTransport.transportName = 'websocket';
// In theory, ws should require 1 round trip. But in chrome, this is
// not very stable over SSL. Most likely a ws connection requires a
// separate SSL connection, in which case 2 round trips are an
// absolute minumum.
WebSocketTransport.roundTrips = 2;
module.exports = WebSocketTransport;

View File

@@ -0,0 +1,23 @@
'use strict';
var inherits = require('inherits')
, AjaxBasedTransport = require('./lib/ajax-based')
, XdrStreamingTransport = require('./xdr-streaming')
, XhrReceiver = require('./receiver/xhr')
, XDRObject = require('./sender/xdr')
;
function XdrPollingTransport(transUrl) {
if (!XDRObject.enabled) {
throw new Error('Transport created when disabled');
}
AjaxBasedTransport.call(this, transUrl, '/xhr', XhrReceiver, XDRObject);
}
inherits(XdrPollingTransport, AjaxBasedTransport);
XdrPollingTransport.enabled = XdrStreamingTransport.enabled;
XdrPollingTransport.transportName = 'xdr-polling';
XdrPollingTransport.roundTrips = 2; // preflight, ajax
module.exports = XdrPollingTransport;

View File

@@ -0,0 +1,32 @@
'use strict';
var inherits = require('inherits')
, AjaxBasedTransport = require('./lib/ajax-based')
, XhrReceiver = require('./receiver/xhr')
, XDRObject = require('./sender/xdr')
;
// According to:
// http://stackoverflow.com/questions/1641507/detect-browser-support-for-cross-domain-xmlhttprequests
// http://hacks.mozilla.org/2009/07/cross-site-xmlhttprequest-with-cors/
function XdrStreamingTransport(transUrl) {
if (!XDRObject.enabled) {
throw new Error('Transport created when disabled');
}
AjaxBasedTransport.call(this, transUrl, '/xhr_streaming', XhrReceiver, XDRObject);
}
inherits(XdrStreamingTransport, AjaxBasedTransport);
XdrStreamingTransport.enabled = function(info) {
if (info.cookie_needed || info.nullOrigin) {
return false;
}
return XDRObject.enabled && info.sameScheme;
};
XdrStreamingTransport.transportName = 'xdr-streaming';
XdrStreamingTransport.roundTrips = 2; // preflight, ajax
module.exports = XdrStreamingTransport;

View File

@@ -0,0 +1,33 @@
'use strict';
var inherits = require('inherits')
, AjaxBasedTransport = require('./lib/ajax-based')
, XhrReceiver = require('./receiver/xhr')
, XHRCorsObject = require('./sender/xhr-cors')
, XHRLocalObject = require('./sender/xhr-local')
;
function XhrPollingTransport(transUrl) {
if (!XHRLocalObject.enabled && !XHRCorsObject.enabled) {
throw new Error('Transport created when disabled');
}
AjaxBasedTransport.call(this, transUrl, '/xhr', XhrReceiver, XHRCorsObject);
}
inherits(XhrPollingTransport, AjaxBasedTransport);
XhrPollingTransport.enabled = function(info) {
if (info.nullOrigin) {
return false;
}
if (XHRLocalObject.enabled && info.sameOrigin) {
return true;
}
return XHRCorsObject.enabled;
};
XhrPollingTransport.transportName = 'xhr-polling';
XhrPollingTransport.roundTrips = 2; // preflight, ajax
module.exports = XhrPollingTransport;

View File

@@ -0,0 +1,41 @@
'use strict';
var inherits = require('inherits')
, AjaxBasedTransport = require('./lib/ajax-based')
, XhrReceiver = require('./receiver/xhr')
, XHRCorsObject = require('./sender/xhr-cors')
, XHRLocalObject = require('./sender/xhr-local')
, browser = require('../utils/browser')
;
function XhrStreamingTransport(transUrl) {
if (!XHRLocalObject.enabled && !XHRCorsObject.enabled) {
throw new Error('Transport created when disabled');
}
AjaxBasedTransport.call(this, transUrl, '/xhr_streaming', XhrReceiver, XHRCorsObject);
}
inherits(XhrStreamingTransport, AjaxBasedTransport);
XhrStreamingTransport.enabled = function(info) {
if (info.nullOrigin) {
return false;
}
// Opera doesn't support xhr-streaming #60
// But it might be able to #92
if (browser.isOpera()) {
return false;
}
return XHRCorsObject.enabled;
};
XhrStreamingTransport.transportName = 'xhr-streaming';
XhrStreamingTransport.roundTrips = 2; // preflight, ajax
// Safari gets confused when a streaming ajax request is started
// before onload. This causes the load indicator to spin indefinetely.
// Only require body when used in a browser
XhrStreamingTransport.needBody = !!global.document;
module.exports = XhrStreamingTransport;

View File

@@ -0,0 +1,17 @@
'use strict';
if (global.crypto && global.crypto.getRandomValues) {
module.exports.randomBytes = function(length) {
var bytes = new Uint8Array(length);
global.crypto.getRandomValues(bytes);
return bytes;
};
} else {
module.exports.randomBytes = function(length) {
var bytes = new Array(length);
for (var i = 0; i < length; i++) {
bytes[i] = Math.floor(Math.random() * 256);
}
return bytes;
};
}

View File

@@ -0,0 +1,27 @@
'use strict';
module.exports = {
isOpera: function() {
return global.navigator &&
/opera/i.test(global.navigator.userAgent);
}
, isKonqueror: function() {
return global.navigator &&
/konqueror/i.test(global.navigator.userAgent);
}
// #187 wrap document.domain in try/catch because of WP8 from file:///
, hasDomain: function () {
// non-browser client always has a domain
if (!global.document) {
return true;
}
try {
return !!global.document.domain;
} catch (e) {
return false;
}
}
};

View File

@@ -0,0 +1,50 @@
'use strict';
var JSON3 = require('json3');
// Some extra characters that Chrome gets wrong, and substitutes with
// something else on the wire.
// eslint-disable-next-line no-control-regex
var extraEscapable = /[\x00-\x1f\ud800-\udfff\ufffe\uffff\u0300-\u0333\u033d-\u0346\u034a-\u034c\u0350-\u0352\u0357-\u0358\u035c-\u0362\u0374\u037e\u0387\u0591-\u05af\u05c4\u0610-\u0617\u0653-\u0654\u0657-\u065b\u065d-\u065e\u06df-\u06e2\u06eb-\u06ec\u0730\u0732-\u0733\u0735-\u0736\u073a\u073d\u073f-\u0741\u0743\u0745\u0747\u07eb-\u07f1\u0951\u0958-\u095f\u09dc-\u09dd\u09df\u0a33\u0a36\u0a59-\u0a5b\u0a5e\u0b5c-\u0b5d\u0e38-\u0e39\u0f43\u0f4d\u0f52\u0f57\u0f5c\u0f69\u0f72-\u0f76\u0f78\u0f80-\u0f83\u0f93\u0f9d\u0fa2\u0fa7\u0fac\u0fb9\u1939-\u193a\u1a17\u1b6b\u1cda-\u1cdb\u1dc0-\u1dcf\u1dfc\u1dfe\u1f71\u1f73\u1f75\u1f77\u1f79\u1f7b\u1f7d\u1fbb\u1fbe\u1fc9\u1fcb\u1fd3\u1fdb\u1fe3\u1feb\u1fee-\u1fef\u1ff9\u1ffb\u1ffd\u2000-\u2001\u20d0-\u20d1\u20d4-\u20d7\u20e7-\u20e9\u2126\u212a-\u212b\u2329-\u232a\u2adc\u302b-\u302c\uaab2-\uaab3\uf900-\ufa0d\ufa10\ufa12\ufa15-\ufa1e\ufa20\ufa22\ufa25-\ufa26\ufa2a-\ufa2d\ufa30-\ufa6d\ufa70-\ufad9\ufb1d\ufb1f\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4e\ufff0-\uffff]/g
, extraLookup;
// This may be quite slow, so let's delay until user actually uses bad
// characters.
var unrollLookup = function(escapable) {
var i;
var unrolled = {};
var c = [];
for (i = 0; i < 65536; i++) {
c.push( String.fromCharCode(i) );
}
escapable.lastIndex = 0;
c.join('').replace(escapable, function(a) {
unrolled[ a ] = '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
return '';
});
escapable.lastIndex = 0;
return unrolled;
};
// Quote string, also taking care of unicode characters that browsers
// often break. Especially, take care of unicode surrogates:
// http://en.wikipedia.org/wiki/Mapping_of_Unicode_characters#Surrogates
module.exports = {
quote: function(string) {
var quoted = JSON3.stringify(string);
// In most cases this should be very fast and good enough.
extraEscapable.lastIndex = 0;
if (!extraEscapable.test(quoted)) {
return quoted;
}
if (!extraLookup) {
extraLookup = unrollLookup(extraEscapable);
}
return quoted.replace(extraEscapable, function(a) {
return extraLookup[a];
});
}
};

View File

@@ -0,0 +1,73 @@
'use strict';
var random = require('./random');
var onUnload = {}
, afterUnload = false
// detect google chrome packaged apps because they don't allow the 'unload' event
, isChromePackagedApp = global.chrome && global.chrome.app && global.chrome.app.runtime
;
module.exports = {
attachEvent: function(event, listener) {
if (typeof global.addEventListener !== 'undefined') {
global.addEventListener(event, listener, false);
} else if (global.document && global.attachEvent) {
// IE quirks.
// According to: http://stevesouders.com/misc/test-postmessage.php
// the message gets delivered only to 'document', not 'window'.
global.document.attachEvent('on' + event, listener);
// I get 'window' for ie8.
global.attachEvent('on' + event, listener);
}
}
, detachEvent: function(event, listener) {
if (typeof global.addEventListener !== 'undefined') {
global.removeEventListener(event, listener, false);
} else if (global.document && global.detachEvent) {
global.document.detachEvent('on' + event, listener);
global.detachEvent('on' + event, listener);
}
}
, unloadAdd: function(listener) {
if (isChromePackagedApp) {
return null;
}
var ref = random.string(8);
onUnload[ref] = listener;
if (afterUnload) {
setTimeout(this.triggerUnloadCallbacks, 0);
}
return ref;
}
, unloadDel: function(ref) {
if (ref in onUnload) {
delete onUnload[ref];
}
}
, triggerUnloadCallbacks: function() {
for (var ref in onUnload) {
onUnload[ref]();
delete onUnload[ref];
}
}
};
var unloadTriggered = function() {
if (afterUnload) {
return;
}
afterUnload = true;
module.exports.triggerUnloadCallbacks();
};
// 'unload' alone is not reliable in opera within an iframe, but we
// can't use `beforeunload` as IE fires it on javascript: links.
if (!isChromePackagedApp) {
module.exports.attachEvent('unload', unloadTriggered);
}

View File

@@ -0,0 +1,186 @@
'use strict';
var eventUtils = require('./event')
, JSON3 = require('json3')
, browser = require('./browser')
;
var debug = function() {};
if (process.env.NODE_ENV !== 'production') {
debug = require('debug')('sockjs-client:utils:iframe');
}
module.exports = {
WPrefix: '_jp'
, currentWindowId: null
, polluteGlobalNamespace: function() {
if (!(module.exports.WPrefix in global)) {
global[module.exports.WPrefix] = {};
}
}
, postMessage: function(type, data) {
if (global.parent !== global) {
global.parent.postMessage(JSON3.stringify({
windowId: module.exports.currentWindowId
, type: type
, data: data || ''
}), '*');
} else {
debug('Cannot postMessage, no parent window.', type, data);
}
}
, createIframe: function(iframeUrl, errorCallback) {
var iframe = global.document.createElement('iframe');
var tref, unloadRef;
var unattach = function() {
debug('unattach');
clearTimeout(tref);
// Explorer had problems with that.
try {
iframe.onload = null;
} catch (x) {
// intentionally empty
}
iframe.onerror = null;
};
var cleanup = function() {
debug('cleanup');
if (iframe) {
unattach();
// This timeout makes chrome fire onbeforeunload event
// within iframe. Without the timeout it goes straight to
// onunload.
setTimeout(function() {
if (iframe) {
iframe.parentNode.removeChild(iframe);
}
iframe = null;
}, 0);
eventUtils.unloadDel(unloadRef);
}
};
var onerror = function(err) {
debug('onerror', err);
if (iframe) {
cleanup();
errorCallback(err);
}
};
var post = function(msg, origin) {
debug('post', msg, origin);
setTimeout(function() {
try {
// When the iframe is not loaded, IE raises an exception
// on 'contentWindow'.
if (iframe && iframe.contentWindow) {
iframe.contentWindow.postMessage(msg, origin);
}
} catch (x) {
// intentionally empty
}
}, 0);
};
iframe.src = iframeUrl;
iframe.style.display = 'none';
iframe.style.position = 'absolute';
iframe.onerror = function() {
onerror('onerror');
};
iframe.onload = function() {
debug('onload');
// `onload` is triggered before scripts on the iframe are
// executed. Give it few seconds to actually load stuff.
clearTimeout(tref);
tref = setTimeout(function() {
onerror('onload timeout');
}, 2000);
};
global.document.body.appendChild(iframe);
tref = setTimeout(function() {
onerror('timeout');
}, 15000);
unloadRef = eventUtils.unloadAdd(cleanup);
return {
post: post
, cleanup: cleanup
, loaded: unattach
};
}
/* eslint no-undef: "off", new-cap: "off" */
, createHtmlfile: function(iframeUrl, errorCallback) {
var axo = ['Active'].concat('Object').join('X');
var doc = new global[axo]('htmlfile');
var tref, unloadRef;
var iframe;
var unattach = function() {
clearTimeout(tref);
iframe.onerror = null;
};
var cleanup = function() {
if (doc) {
unattach();
eventUtils.unloadDel(unloadRef);
iframe.parentNode.removeChild(iframe);
iframe = doc = null;
CollectGarbage();
}
};
var onerror = function(r) {
debug('onerror', r);
if (doc) {
cleanup();
errorCallback(r);
}
};
var post = function(msg, origin) {
try {
// When the iframe is not loaded, IE raises an exception
// on 'contentWindow'.
setTimeout(function() {
if (iframe && iframe.contentWindow) {
iframe.contentWindow.postMessage(msg, origin);
}
}, 0);
} catch (x) {
// intentionally empty
}
};
doc.open();
doc.write('<html><s' + 'cript>' +
'document.domain="' + global.document.domain + '";' +
'</s' + 'cript></html>');
doc.close();
doc.parentWindow[module.exports.WPrefix] = global[module.exports.WPrefix];
var c = doc.createElement('div');
doc.body.appendChild(c);
iframe = doc.createElement('iframe');
c.appendChild(iframe);
iframe.src = iframeUrl;
iframe.onerror = function() {
onerror('onerror');
};
tref = setTimeout(function() {
onerror('timeout');
}, 15000);
unloadRef = eventUtils.unloadAdd(cleanup);
return {
post: post
, cleanup: cleanup
, loaded: unattach
};
}
};
module.exports.iframeEnabled = false;
if (global.document) {
// postMessage misbehaves in konqueror 4.6.5 - the messages are delivered with
// huge delay, or not at all.
module.exports.iframeEnabled = (typeof global.postMessage === 'function' ||
typeof global.postMessage === 'object') && (!browser.isKonqueror());
}

View File

@@ -0,0 +1,18 @@
'use strict';
var logObject = {};
['log', 'debug', 'warn'].forEach(function (level) {
var levelExists;
try {
levelExists = global.console && global.console[level] && global.console[level].apply;
} catch(e) {
// do nothing
}
logObject[level] = levelExists ? function () {
return global.console[level].apply(global.console, arguments);
} : (level === 'log' ? function () {} : logObject.log);
});
module.exports = logObject;

View File

@@ -0,0 +1,24 @@
'use strict';
module.exports = {
isObject: function(obj) {
var type = typeof obj;
return type === 'function' || type === 'object' && !!obj;
}
, extend: function(obj) {
if (!this.isObject(obj)) {
return obj;
}
var source, prop;
for (var i = 1, length = arguments.length; i < length; i++) {
source = arguments[i];
for (prop in source) {
if (Object.prototype.hasOwnProperty.call(source, prop)) {
obj[prop] = source[prop];
}
}
}
return obj;
}
};

View File

@@ -0,0 +1,29 @@
'use strict';
/* global crypto:true */
var crypto = require('crypto');
// This string has length 32, a power of 2, so the modulus doesn't introduce a
// bias.
var _randomStringChars = 'abcdefghijklmnopqrstuvwxyz012345';
module.exports = {
string: function(length) {
var max = _randomStringChars.length;
var bytes = crypto.randomBytes(length);
var ret = [];
for (var i = 0; i < length; i++) {
ret.push(_randomStringChars.substr(bytes[i] % max, 1));
}
return ret.join('');
}
, number: function(max) {
return Math.floor(Math.random() * max);
}
, numberString: function(max) {
var t = ('' + (max - 1)).length;
var p = new Array(t + 1).join('0');
return (p + this.number(max)).slice(-t);
}
};

View File

@@ -0,0 +1,50 @@
'use strict';
var debug = function() {};
if (process.env.NODE_ENV !== 'production') {
debug = require('debug')('sockjs-client:utils:transport');
}
module.exports = function(availableTransports) {
return {
filterToEnabled: function(transportsWhitelist, info) {
var transports = {
main: []
, facade: []
};
if (!transportsWhitelist) {
transportsWhitelist = [];
} else if (typeof transportsWhitelist === 'string') {
transportsWhitelist = [transportsWhitelist];
}
availableTransports.forEach(function(trans) {
if (!trans) {
return;
}
if (trans.transportName === 'websocket' && info.websocket === false) {
debug('disabled from server', 'websocket');
return;
}
if (transportsWhitelist.length &&
transportsWhitelist.indexOf(trans.transportName) === -1) {
debug('not in whitelist', trans.transportName);
return;
}
if (trans.enabled(info)) {
debug('enabled', trans.transportName);
transports.main.push(trans);
if (trans.facadeTransport) {
transports.facade.push(trans.facadeTransport);
}
} else {
debug('disabled', trans.transportName);
}
});
return transports;
}
};
};

View File

@@ -0,0 +1,47 @@
'use strict';
var URL = require('url-parse');
var debug = function() {};
if (process.env.NODE_ENV !== 'production') {
debug = require('debug')('sockjs-client:utils:url');
}
module.exports = {
getOrigin: function(url) {
if (!url) {
return null;
}
var p = new URL(url);
if (p.protocol === 'file:') {
return null;
}
var port = p.port;
if (!port) {
port = (p.protocol === 'https:') ? '443' : '80';
}
return p.protocol + '//' + p.hostname + ':' + port;
}
, isOriginEqual: function(a, b) {
var res = this.getOrigin(a) === this.getOrigin(b);
debug('same', a, b, res);
return res;
}
, isSchemeEqual: function(a, b) {
return (a.split(':')[0] === b.split(':')[0]);
}
, addPath: function (url, path) {
var qs = url.split('?');
return qs[0] + path + (qs[1] ? '?' + qs[1] : '');
}
, addQuery: function (url, q) {
return url + (url.indexOf('?') === -1 ? ('?' + q) : ('&' + q));
}
};

View File

@@ -0,0 +1 @@
module.exports = '1.1.5';

View File

@@ -0,0 +1,134 @@
### 0.11.3 / 2019-06-10
- Fix a race condition that caused a timeout not to be cancelled immediately
when the WebSocket is closed
### 0.11.2 / 2019-06-10
(This version was pulled due to an error when publishing)
### 0.11.1 / 2017-01-22
- Forcibly close the I/O stream after a timeout if the peer does not respond
after calling `close()`
### 0.11.0 / 2016-02-24
- Introduce a `net` option to the `Client` class for setting things like, say,
`servername`
### 0.10.0 / 2015-07-08
- Add the standard `code` and `reason` parameters to the `close` method
### 0.9.4 / 2015-03-08
- Don't send input to the driver before `start()` is called
### 0.9.3 / 2015-02-19
- Make sure the TCP socket is not left open when closing the connection
### 0.9.2 / 2014-12-21
- Only emit `error` once, and don't emit it after `close`
### 0.9.1 / 2014-12-18
- Check that all options to the WebSocket constructor are recognized
### 0.9.0 / 2014-12-13
- Allow protocol extensions to be passed into websocket-extensions
### 0.8.1 / 2014-11-12
- Send the correct hostname when upgrading a connection to TLS
### 0.8.0 / 2014-11-08
- Support connections via HTTP proxies
- Close the connection cleanly if we're still waiting for a handshake response
### 0.7.3 / 2014-10-04
- Allow sockets to be closed when they are in any state other than `CLOSED`
### 0.7.2 / 2013-12-29
- Make sure the `close` event is emitted by clients on Node v0.10
### 0.7.1 / 2013-12-03
- Support the `maxLength` websocket-driver option
- Make the client emit `error` events on network errors
### 0.7.0 / 2013-09-09
- Allow the server to send custom headers with EventSource responses
### 0.6.1 / 2013-07-05
- Add `ca` option to the client for specifying certificate authorities
- Start the server driver asynchronously so that `onopen` handlers can be added
### 0.6.0 / 2013-05-12
- Add support for custom headers
### 0.5.0 / 2013-05-05
- Extract the protocol handlers into the `websocket-driver` library
- Support the Node streaming API
### 0.4.4 / 2013-02-14
- Emit the `close` event if TCP is closed before CLOSE frame is acked
### 0.4.3 / 2012-07-09
- Add `Connection: close` to EventSource response
- Handle situations where `request.socket` is undefined
### 0.4.2 / 2012-04-06
- Add WebSocket error code `1011`.
- Handle URLs with no path correctly by sending `GET /`
### 0.4.1 / 2012-02-26
- Treat anything other than a `Buffer` as a string when calling `send()`
### 0.4.0 / 2012-02-13
- Add `ping()` method to server-side `WebSocket` and `EventSource`
- Buffer `send()` calls until the draft-76 handshake is complete
- Fix HTTPS problems on Node 0.7
### 0.3.1 / 2012-01-16
- Call `setNoDelay(true)` on `net.Socket` objects to reduce latency
### 0.3.0 / 2012-01-13
- Add support for `EventSource` connections
### 0.2.0 / 2011-12-21
- Add support for `Sec-WebSocket-Protocol` negotiation
- Support `hixie-76` close frames and 75/76 ignored segments
- Improve performance of HyBi parsing/framing functions
- Decouple parsers from TCP and reduce write volume
### 0.1.2 / 2011-12-05
- Detect closed sockets on the server side when TCP connection breaks
- Make `hixie-76` sockets work through HAProxy
### 0.1.1 / 2011-11-30
- Fix `addEventListener()` interface methods
### 0.1.0 / 2011-11-27
- Initial release, based on WebSocket components from Faye

View File

@@ -0,0 +1,12 @@
Copyright 2010-2019 James Coglan
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.

View File

@@ -0,0 +1,311 @@
# faye-websocket [![Build status](https://secure.travis-ci.org/faye/faye-websocket-node.svg)](http://travis-ci.org/faye/faye-websocket-node)
This is a general-purpose WebSocket implementation extracted from the
[Faye](http://faye.jcoglan.com) project. It provides classes for easily building
WebSocket servers and clients in Node. It does not provide a server itself, but
rather makes it easy to handle WebSocket connections within an existing
[Node](https://nodejs.org/) application. It does not provide any abstraction
other than the standard [WebSocket
API](https://html.spec.whatwg.org/multipage/comms.html#network).
It also provides an abstraction for handling
[EventSource](https://html.spec.whatwg.org/multipage/comms.html#server-sent-events)
connections, which are one-way connections that allow the server to push data to
the client. They are based on streaming HTTP responses and can be easier to access
via proxies than WebSockets.
## Installation
```
$ npm install faye-websocket
```
## Handling WebSocket connections in Node
You can handle WebSockets on the server side by listening for HTTP Upgrade
requests, and creating a new socket for the request. This socket object exposes
the usual WebSocket methods for receiving and sending messages. For example this
is how you'd implement an echo server:
```js
var WebSocket = require('faye-websocket'),
http = require('http');
var server = http.createServer();
server.on('upgrade', function(request, socket, body) {
if (WebSocket.isWebSocket(request)) {
var ws = new WebSocket(request, socket, body);
ws.on('message', function(event) {
ws.send(event.data);
});
ws.on('close', function(event) {
console.log('close', event.code, event.reason);
ws = null;
});
}
});
server.listen(8000);
```
`WebSocket` objects are also duplex streams, so you could replace the
`ws.on('message', ...)` line with:
```js
ws.pipe(ws);
```
Note that under certain circumstances (notably a draft-76 client connecting
through an HTTP proxy), the WebSocket handshake will not be complete after you
call `new WebSocket()` because the server will not have received the entire
handshake from the client yet. In this case, calls to `ws.send()` will buffer
the message in memory until the handshake is complete, at which point any
buffered messages will be sent to the client.
If you need to detect when the WebSocket handshake is complete, you can use the
`onopen` event.
If the connection's protocol version supports it, you can call `ws.ping()` to
send a ping message and wait for the client's response. This method takes a
message string, and an optional callback that fires when a matching pong message
is received. It returns `true` if and only if a ping message was sent. If the
client does not support ping/pong, this method sends no data and returns
`false`.
```js
ws.ping('Mic check, one, two', function() {
// fires when pong is received
});
```
## Using the WebSocket client
The client supports both the plain-text `ws` protocol and the encrypted `wss`
protocol, and has exactly the same interface as a socket you would use in a web
browser. On the wire it identifies itself as `hybi-13`.
```js
var WebSocket = require('faye-websocket'),
ws = new WebSocket.Client('ws://www.example.com/');
ws.on('open', function(event) {
console.log('open');
ws.send('Hello, world!');
});
ws.on('message', function(event) {
console.log('message', event.data);
});
ws.on('close', function(event) {
console.log('close', event.code, event.reason);
ws = null;
});
```
The WebSocket client also lets you inspect the status and headers of the
handshake response via its `statusCode` and `headers` properties.
To connect via a proxy, set the `proxy` option to the HTTP origin of the proxy,
including any authorization information, custom headers and TLS config you
require. Only the `origin` setting is required.
```js
var ws = new WebSocket.Client('ws://www.example.com/', [], {
proxy: {
origin: 'http://username:password@proxy.example.com',
headers: {'User-Agent': 'node'},
tls: {cert: fs.readFileSync('client.crt')}
}
});
```
The `tls` value is an object that will be passed to
[`tls.connect()`](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback).
## Subprotocol negotiation
The WebSocket protocol allows peers to select and identify the application
protocol to use over the connection. On the client side, you can set which
protocols the client accepts by passing a list of protocol names when you
construct the socket:
```js
var ws = new WebSocket.Client('ws://www.example.com/', ['irc', 'amqp']);
```
On the server side, you can likewise pass in the list of protocols the server
supports after the other constructor arguments:
```js
var ws = new WebSocket(request, socket, body, ['irc', 'amqp']);
```
If the client and server agree on a protocol, both the client- and server-side
socket objects expose the selected protocol through the `ws.protocol` property.
## Protocol extensions
faye-websocket is based on the
[websocket-extensions](https://github.com/faye/websocket-extensions-node)
framework that allows extensions to be negotiated via the
`Sec-WebSocket-Extensions` header. To add extensions to a connection, pass an
array of extensions to the `:extensions` option. For example, to add
[permessage-deflate](https://github.com/faye/permessage-deflate-node):
```js
var deflate = require('permessage-deflate');
var ws = new WebSocket(request, socket, body, [], {extensions: [deflate]});
```
## Initialization options
Both the server- and client-side classes allow an options object to be passed in
at initialization time, for example:
```js
var ws = new WebSocket(request, socket, body, protocols, options);
var ws = new WebSocket.Client(url, protocols, options);
```
`protocols` is an array of subprotocols as described above, or `null`.
`options` is an optional object containing any of these fields:
- `extensions` - an array of
[websocket-extensions](https://github.com/faye/websocket-extensions-node)
compatible extensions, as described above
- `headers` - an object containing key-value pairs representing HTTP headers to
be sent during the handshake process
- `maxLength` - the maximum allowed size of incoming message frames, in bytes.
The default value is `2^26 - 1`, or 1 byte short of 64 MiB.
- `ping` - an integer that sets how often the WebSocket should send ping frames,
measured in seconds
The client accepts some additional options:
- `proxy` - settings for a proxy as described above
- `net` - an object containing settings for the origin server that will be
passed to
[`net.connect()`](https://nodejs.org/api/net.html#net_socket_connect_options_connectlistener)
- `tls` - an object containing TLS settings for the origin server, this will be
passed to
[`tls.connect()`](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback)
- `ca` - (legacy) a shorthand for passing `{tls: {ca: value}}`
## WebSocket API
Both server- and client-side `WebSocket` objects support the following API.
- **`on('open', function(event) {})`** fires when the socket connection is
established. Event has no attributes.
- **`on('message', function(event) {})`** fires when the socket receives a
message. Event has one attribute, **`data`**, which is either a `String` (for
text frames) or a `Buffer` (for binary frames).
- **`on('error', function(event) {})`** fires when there is a protocol error due
to bad data sent by the other peer. This event is purely informational, you do
not need to implement error recover.
- **`on('close', function(event) {})`** fires when either the client or the
server closes the connection. Event has two optional attributes, **`code`**
and **`reason`**, that expose the status code and message sent by the peer
that closed the connection.
- **`send(message)`** accepts either a `String` or a `Buffer` and sends a text
or binary message over the connection to the other peer.
- **`ping(message, function() {})`** sends a ping frame with an optional message
and fires the callback when a matching pong is received.
- **`close(code, reason)`** closes the connection, sending the given status code
and reason text, both of which are optional.
- **`version`** is a string containing the version of the `WebSocket` protocol
the connection is using.
- **`protocol`** is a string (which may be empty) identifying the subprotocol
the socket is using.
## Handling EventSource connections in Node
EventSource connections provide a very similar interface, although because they
only allow the server to send data to the client, there is no `onmessage` API.
EventSource allows the server to push text messages to the client, where each
message has an optional event-type and ID.
```js
var WebSocket = require('faye-websocket'),
EventSource = WebSocket.EventSource,
http = require('http');
var server = http.createServer();
server.on('request', function(request, response) {
if (EventSource.isEventSource(request)) {
var es = new EventSource(request, response);
console.log('open', es.url, es.lastEventId);
// Periodically send messages
var loop = setInterval(function() { es.send('Hello') }, 1000);
es.on('close', function() {
clearInterval(loop);
es = null;
});
} else {
// Normal HTTP request
response.writeHead(200, {'Content-Type': 'text/plain'});
response.end('Hello');
}
});
server.listen(8000);
```
The `send` method takes two optional parameters, `event` and `id`. The default
event-type is `'message'` with no ID. For example, to send a `notification`
event with ID `99`:
```js
es.send('Breaking News!', {event: 'notification', id: '99'});
```
The `EventSource` object exposes the following properties:
- **`url`** is a string containing the URL the client used to create the
EventSource.
- **`lastEventId`** is a string containing the last event ID received by the
client. You can use this when the client reconnects after a dropped connection
to determine which messages need resending.
When you initialize an EventSource with ` new EventSource()`, you can pass
configuration options after the `response` parameter. Available options are:
- **`headers`** is an object containing custom headers to be set on the
EventSource response.
- **`retry`** is a number that tells the client how long (in seconds) it should
wait after a dropped connection before attempting to reconnect.
- **`ping`** is a number that tells the server how often (in seconds) to send
'ping' packets to the client to keep the connection open, to defeat timeouts
set by proxies. The client will ignore these messages.
For example, this creates a connection that allows access from any origin, pings
every 15 seconds and is retryable every 10 seconds if the connection is broken:
```js
var es = new EventSource(request, response, {
headers: {'Access-Control-Allow-Origin': '*'},
ping: 15,
retry: 10
});
```
You can send a ping message at any time by calling `es.ping()`. Unlike
WebSocket, the client does not send a response to this; it is merely to send
some data over the wire to keep the connection alive.

View File

@@ -0,0 +1,133 @@
'use strict';
var Stream = require('stream').Stream,
util = require('util'),
driver = require('websocket-driver'),
Headers = require('websocket-driver/lib/websocket/driver/headers'),
API = require('./websocket/api'),
EventTarget = require('./websocket/api/event_target'),
Event = require('./websocket/api/event');
var EventSource = function(request, response, options) {
this.writable = true;
options = options || {};
this._stream = response.socket;
this._ping = options.ping || this.DEFAULT_PING;
this._retry = options.retry || this.DEFAULT_RETRY;
var scheme = driver.isSecureRequest(request) ? 'https:' : 'http:';
this.url = scheme + '//' + request.headers.host + request.url;
this.lastEventId = request.headers['last-event-id'] || '';
this.readyState = API.CONNECTING;
var headers = new Headers(),
self = this;
if (options.headers) {
for (var key in options.headers) headers.set(key, options.headers[key]);
}
if (!this._stream || !this._stream.writable) return;
process.nextTick(function() { self._open() });
this._stream.setTimeout(0);
this._stream.setNoDelay(true);
var handshake = 'HTTP/1.1 200 OK\r\n' +
'Content-Type: text/event-stream\r\n' +
'Cache-Control: no-cache, no-store\r\n' +
'Connection: close\r\n' +
headers.toString() +
'\r\n' +
'retry: ' + Math.floor(this._retry * 1000) + '\r\n\r\n';
this._write(handshake);
this._stream.on('drain', function() { self.emit('drain') });
if (this._ping)
this._pingTimer = setInterval(function() { self.ping() }, this._ping * 1000);
['error', 'end'].forEach(function(event) {
self._stream.on(event, function() { self.close() });
});
};
util.inherits(EventSource, Stream);
EventSource.isEventSource = function(request) {
if (request.method !== 'GET') return false;
var accept = (request.headers.accept || '').split(/\s*,\s*/);
return accept.indexOf('text/event-stream') >= 0;
};
var instance = {
DEFAULT_PING: 10,
DEFAULT_RETRY: 5,
_write: function(chunk) {
if (!this.writable) return false;
try {
return this._stream.write(chunk, 'utf8');
} catch (e) {
return false;
}
},
_open: function() {
if (this.readyState !== API.CONNECTING) return;
this.readyState = API.OPEN;
var event = new Event('open');
event.initEvent('open', false, false);
this.dispatchEvent(event);
},
write: function(message) {
return this.send(message);
},
end: function(message) {
if (message !== undefined) this.write(message);
this.close();
},
send: function(message, options) {
if (this.readyState > API.OPEN) return false;
message = String(message).replace(/(\r\n|\r|\n)/g, '$1data: ');
options = options || {};
var frame = '';
if (options.event) frame += 'event: ' + options.event + '\r\n';
if (options.id) frame += 'id: ' + options.id + '\r\n';
frame += 'data: ' + message + '\r\n\r\n';
return this._write(frame);
},
ping: function() {
return this._write(':\r\n\r\n');
},
close: function() {
if (this.readyState > API.OPEN) return false;
this.readyState = API.CLOSED;
this.writable = false;
if (this._pingTimer) clearInterval(this._pingTimer);
if (this._stream) this._stream.end();
var event = new Event('close');
event.initEvent('close', false, false);
this.dispatchEvent(event);
return true;
}
};
for (var method in instance) EventSource.prototype[method] = instance[method];
for (var key in EventTarget) EventSource.prototype[key] = EventTarget[key];
module.exports = EventSource;

View File

@@ -0,0 +1,47 @@
// API references:
//
// * https://html.spec.whatwg.org/multipage/comms.html#network
// * https://dom.spec.whatwg.org/#interface-eventtarget
// * https://dom.spec.whatwg.org/#interface-event
'use strict';
var util = require('util'),
driver = require('websocket-driver'),
API = require('./websocket/api');
var WebSocket = function(request, socket, body, protocols, options) {
options = options || {};
this._stream = socket;
this._driver = driver.http(request, {maxLength: options.maxLength, protocols: protocols});
var self = this;
if (!this._stream || !this._stream.writable) return;
if (!this._stream.readable) return this._stream.end();
var catchup = function() { self._stream.removeListener('data', catchup) };
this._stream.on('data', catchup);
API.call(this, options);
process.nextTick(function() {
self._driver.start();
self._driver.io.write(body);
});
};
util.inherits(WebSocket, API);
WebSocket.isWebSocket = function(request) {
return driver.isWebSocket(request);
};
WebSocket.validateOptions = function(options, validKeys) {
driver.validateOptions(options, validKeys);
};
WebSocket.WebSocket = WebSocket;
WebSocket.Client = require('./websocket/client');
WebSocket.EventSource = require('./eventsource');
module.exports = WebSocket;

View File

@@ -0,0 +1,197 @@
'use strict';
var Stream = require('stream').Stream,
util = require('util'),
driver = require('websocket-driver'),
EventTarget = require('./api/event_target'),
Event = require('./api/event');
var API = function(options) {
options = options || {};
driver.validateOptions(options, ['headers', 'extensions', 'maxLength', 'ping', 'proxy', 'tls', 'ca']);
this.readable = this.writable = true;
var headers = options.headers;
if (headers) {
for (var name in headers) this._driver.setHeader(name, headers[name]);
}
var extensions = options.extensions;
if (extensions) {
[].concat(extensions).forEach(this._driver.addExtension, this._driver);
}
this._ping = options.ping;
this._pingId = 0;
this.readyState = API.CONNECTING;
this.bufferedAmount = 0;
this.protocol = '';
this.url = this._driver.url;
this.version = this._driver.version;
var self = this;
this._driver.on('open', function(e) { self._open() });
this._driver.on('message', function(e) { self._receiveMessage(e.data) });
this._driver.on('close', function(e) { self._beginClose(e.reason, e.code) });
this._driver.on('error', function(error) {
self._emitError(error.message);
});
this.on('error', function() {});
this._driver.messages.on('drain', function() {
self.emit('drain');
});
if (this._ping)
this._pingTimer = setInterval(function() {
self._pingId += 1;
self.ping(self._pingId.toString());
}, this._ping * 1000);
this._configureStream();
if (!this._proxy) {
this._stream.pipe(this._driver.io);
this._driver.io.pipe(this._stream);
}
};
util.inherits(API, Stream);
API.CONNECTING = 0;
API.OPEN = 1;
API.CLOSING = 2;
API.CLOSED = 3;
API.CLOSE_TIMEOUT = 30000;
var instance = {
write: function(data) {
return this.send(data);
},
end: function(data) {
if (data !== undefined) this.send(data);
this.close();
},
pause: function() {
return this._driver.messages.pause();
},
resume: function() {
return this._driver.messages.resume();
},
send: function(data) {
if (this.readyState > API.OPEN) return false;
if (!(data instanceof Buffer)) data = String(data);
return this._driver.messages.write(data);
},
ping: function(message, callback) {
if (this.readyState > API.OPEN) return false;
return this._driver.ping(message, callback);
},
close: function(code, reason) {
if (code === undefined) code = 1000;
if (reason === undefined) reason = '';
if (code !== 1000 && (code < 3000 || code > 4999))
throw new Error("Failed to execute 'close' on WebSocket: " +
"The code must be either 1000, or between 3000 and 4999. " +
code + " is neither.");
if (this.readyState !== API.CLOSED) this.readyState = API.CLOSING;
var self = this;
this._closeTimer = setTimeout(function() {
self._beginClose('', 1006);
}, API.CLOSE_TIMEOUT);
this._driver.close(reason, code);
},
_configureStream: function() {
var self = this;
this._stream.setTimeout(0);
this._stream.setNoDelay(true);
['close', 'end'].forEach(function(event) {
this._stream.on(event, function() { self._finalizeClose() });
}, this);
this._stream.on('error', function(error) {
self._emitError('Network error: ' + self.url + ': ' + error.message);
self._finalizeClose();
});
},
_open: function() {
if (this.readyState !== API.CONNECTING) return;
this.readyState = API.OPEN;
this.protocol = this._driver.protocol || '';
var event = new Event('open');
event.initEvent('open', false, false);
this.dispatchEvent(event);
},
_receiveMessage: function(data) {
if (this.readyState > API.OPEN) return false;
if (this.readable) this.emit('data', data);
var event = new Event('message', {data: data});
event.initEvent('message', false, false);
this.dispatchEvent(event);
},
_emitError: function(message) {
if (this.readyState >= API.CLOSING) return;
var event = new Event('error', {message: message});
event.initEvent('error', false, false);
this.dispatchEvent(event);
},
_beginClose: function(reason, code) {
if (this.readyState === API.CLOSED) return;
this.readyState = API.CLOSING;
this._closeParams = [reason, code];
if (this._stream) {
this._stream.destroy();
if (!this._stream.readable) this._finalizeClose();
}
},
_finalizeClose: function() {
if (this.readyState === API.CLOSED) return;
this.readyState = API.CLOSED;
if (this._closeTimer) clearTimeout(this._closeTimer);
if (this._pingTimer) clearInterval(this._pingTimer);
if (this._stream) this._stream.end();
if (this.readable) this.emit('end');
this.readable = this.writable = false;
var reason = this._closeParams ? this._closeParams[0] : '',
code = this._closeParams ? this._closeParams[1] : 1006;
var event = new Event('close', {code: code, reason: reason});
event.initEvent('close', false, false);
this.dispatchEvent(event);
}
};
for (var method in instance) API.prototype[method] = instance[method];
for (var key in EventTarget) API.prototype[key] = EventTarget[key];
module.exports = API;

View File

@@ -0,0 +1,22 @@
'use strict';
var Event = function(eventType, options) {
this.type = eventType;
for (var key in options)
this[key] = options[key];
};
Event.prototype.initEvent = function(eventType, canBubble, cancelable) {
this.type = eventType;
this.bubbles = canBubble;
this.cancelable = cancelable;
};
Event.prototype.stopPropagation = function() {};
Event.prototype.preventDefault = function() {};
Event.CAPTURING_PHASE = 1;
Event.AT_TARGET = 2;
Event.BUBBLING_PHASE = 3;
module.exports = Event;

View File

@@ -0,0 +1,30 @@
'use strict';
var Event = require('./event');
var EventTarget = {
onopen: null,
onmessage: null,
onerror: null,
onclose: null,
addEventListener: function(eventType, listener, useCapture) {
this.on(eventType, listener);
},
removeEventListener: function(eventType, listener, useCapture) {
this.removeListener(eventType, listener);
},
dispatchEvent: function(event) {
event.target = event.currentTarget = this;
event.eventPhase = Event.AT_TARGET;
if (this['on' + event.type])
this['on' + event.type](event);
this.emit(event.type, event);
}
};
module.exports = EventTarget;

View File

@@ -0,0 +1,90 @@
'use strict';
var util = require('util'),
net = require('net'),
tls = require('tls'),
url = require('url'),
driver = require('websocket-driver'),
API = require('./api'),
Event = require('./api/event');
var DEFAULT_PORTS = {'http:': 80, 'https:': 443, 'ws:':80, 'wss:': 443},
SECURE_PROTOCOLS = ['https:', 'wss:'];
var Client = function(_url, protocols, options) {
options = options || {};
this.url = _url;
this._driver = driver.client(this.url, {maxLength: options.maxLength, protocols: protocols});
['open', 'error'].forEach(function(event) {
this._driver.on(event, function() {
self.headers = self._driver.headers;
self.statusCode = self._driver.statusCode;
});
}, this);
var proxy = options.proxy || {},
endpoint = url.parse(proxy.origin || this.url),
port = endpoint.port || DEFAULT_PORTS[endpoint.protocol],
secure = SECURE_PROTOCOLS.indexOf(endpoint.protocol) >= 0,
onConnect = function() { self._onConnect() },
netOptions = options.net || {},
originTLS = options.tls || {},
socketTLS = proxy.origin ? (proxy.tls || {}) : originTLS,
self = this;
netOptions.host = socketTLS.host = endpoint.hostname;
netOptions.port = socketTLS.port = port;
originTLS.ca = originTLS.ca || options.ca;
socketTLS.servername = socketTLS.servername || endpoint.hostname;
this._stream = secure
? tls.connect(socketTLS, onConnect)
: net.connect(netOptions, onConnect);
if (proxy.origin) this._configureProxy(proxy, originTLS);
API.call(this, options);
};
util.inherits(Client, API);
Client.prototype._onConnect = function() {
var worker = this._proxy || this._driver;
worker.start();
};
Client.prototype._configureProxy = function(proxy, originTLS) {
var uri = url.parse(this.url),
secure = SECURE_PROTOCOLS.indexOf(uri.protocol) >= 0,
self = this,
name;
this._proxy = this._driver.proxy(proxy.origin);
if (proxy.headers) {
for (name in proxy.headers) this._proxy.setHeader(name, proxy.headers[name]);
}
this._proxy.pipe(this._stream, {end: false});
this._stream.pipe(this._proxy);
this._proxy.on('connect', function() {
if (secure) {
var options = {socket: self._stream, servername: uri.hostname};
for (name in originTLS) options[name] = originTLS[name];
self._stream = tls.connect(options);
self._configureStream();
}
self._driver.io.pipe(self._stream);
self._stream.pipe(self._driver.io);
self._driver.start();
});
this._proxy.on('error', function(error) {
self._driver.emit('error', error);
});
};
module.exports = Client;

View File

@@ -0,0 +1,66 @@
{
"_from": "faye-websocket@~0.11.0",
"_id": "faye-websocket@0.11.3",
"_inBundle": false,
"_integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==",
"_location": "/sockjs-client/faye-websocket",
"_phantomChildren": {},
"_requested": {
"type": "range",
"registry": true,
"raw": "faye-websocket@~0.11.0",
"name": "faye-websocket",
"escapedName": "faye-websocket",
"rawSpec": "~0.11.0",
"saveSpec": null,
"fetchSpec": "~0.11.0"
},
"_requiredBy": [
"/sockjs-client"
],
"_resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz",
"_shasum": "5c0e9a8968e8912c286639fde977a8b209f2508e",
"_spec": "faye-websocket@~0.11.0",
"_where": "D:\\developments\\teaser-inertia\\nova-components\\NovaLeader\\node_modules\\sockjs-client",
"author": {
"name": "James Coglan",
"email": "jcoglan@gmail.com",
"url": "http://jcoglan.com/"
},
"bugs": {
"url": "https://github.com/faye/faye-websocket-node/issues"
},
"bundleDependencies": false,
"dependencies": {
"websocket-driver": ">=0.5.1"
},
"deprecated": false,
"description": "Standards-compliant WebSocket server and client",
"devDependencies": {
"jstest": "*",
"pace": "*",
"permessage-deflate": "*"
},
"engines": {
"node": ">=0.8.0"
},
"files": [
"lib"
],
"homepage": "https://github.com/faye/faye-websocket-node",
"keywords": [
"websocket",
"eventsource"
],
"license": "Apache-2.0",
"main": "./lib/faye/websocket",
"name": "faye-websocket",
"repository": {
"type": "git",
"url": "git://github.com/faye/faye-websocket-node.git"
},
"scripts": {
"test": "jstest spec/runner.js"
},
"version": "0.11.3"
}

View File

@@ -0,0 +1,107 @@
{
"_from": "sockjs-client@1.1.5",
"_id": "sockjs-client@1.1.5",
"_inBundle": false,
"_integrity": "sha1-G7fA9yIsQPQq3xT0RCy9Eml3GoM=",
"_location": "/sockjs-client",
"_phantomChildren": {
"websocket-driver": "0.7.4"
},
"_requested": {
"type": "version",
"registry": true,
"raw": "sockjs-client@1.1.5",
"name": "sockjs-client",
"escapedName": "sockjs-client",
"rawSpec": "1.1.5",
"saveSpec": null,
"fetchSpec": "1.1.5"
},
"_requiredBy": [
"/webpack-dev-server"
],
"_resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.1.5.tgz",
"_shasum": "1bb7c0f7222c40f42adf14f4442cbd1269771a83",
"_spec": "sockjs-client@1.1.5",
"_where": "D:\\developments\\teaser-inertia\\nova-components\\NovaLeader\\node_modules\\webpack-dev-server",
"author": {
"name": "Bryce Kahle"
},
"browser": {
"./lib/transport/driver/websocket.js": "./lib/transport/browser/websocket.js",
"eventsource": "./lib/transport/browser/eventsource.js",
"./lib/transport/driver/xhr.js": "./lib/transport/browser/abstract-xhr.js",
"crypto": "./lib/utils/browser-crypto.js",
"events": "./lib/event/emitter.js"
},
"bugs": {
"url": "https://github.com/sockjs/sockjs-client/issues"
},
"bundleDependencies": false,
"contributors": [
{
"name": "Bryce Kahle",
"email": "bkahle@gmail.com"
},
{
"name": "Marek Majkowski",
"email": "deadbeef@popcount.org"
}
],
"dependencies": {
"debug": "^2.6.6",
"eventsource": "0.1.6",
"faye-websocket": "~0.11.0",
"inherits": "^2.0.1",
"json3": "^3.3.2",
"url-parse": "^1.1.8"
},
"deprecated": false,
"description": "SockJS-client is a browser JavaScript library that provides a WebSocket-like object.",
"devDependencies": {
"browserify": "^13.3.0",
"envify": "^4.0.0",
"eslint": "^3.19.0",
"expect.js": "~0.3.1",
"gulp": "^3.9.1",
"gulp-header": "^1.8.8",
"gulp-rename": "~1.2.0",
"gulp-replace": "^0.5.4",
"gulp-sourcemaps": "^2.6.0",
"gulp-uglify": "^2.1.2",
"mocha": "^3.3.0",
"node-static": "^0.7.6",
"proxyquire": "^1.7.11",
"pump": "^1.0.2",
"sockjs": "^0.3.17",
"vinyl-buffer": "~1.0.0",
"vinyl-source-stream": "^1.0.0",
"zuul": "github:brycekahle/zuul#ngrok",
"zuul-ngrok": "github:brycekahle/zuul-ngrok#master"
},
"homepage": "http://sockjs.org",
"jsdelivr": "dist/sockjs.min.js",
"keywords": [
"websockets",
"websocket"
],
"license": "MIT",
"main": "./lib/entry.js",
"name": "sockjs-client",
"repository": {
"type": "git",
"url": "git+https://github.com/sockjs/sockjs-client.git"
},
"scripts": {
"gulp": "gulp",
"lint": "eslint .",
"postpublish": "git push origin --all && git push origin --tags",
"postversion": "npm publish",
"test": "mocha tests/node.js",
"test:browser_local": "npm run test:bundle && zuul --disable-tunnel --local 9090 -- tests/browser.js",
"test:browser_remote": "npm run test:bundle && zuul -- tests/browser.js",
"test:bundle": "gulp testbundle",
"version": "gulp release && git add -A dist lib/version.js"
},
"version": "1.1.5"
}