139 Commits

Author SHA1 Message Date
Solly Ross 69c04c819c Update to 0.7.0
See CHANGES.txt for details.
2015-07-23 21:02:37 -04:00
Solly 558a6439f1 Merge pull request #176 from crodjer/venv-with-run
Make `run` respect virtualenv configuration
2015-06-02 15:39:03 -04:00
Rohan Jain 2b9e0a5794 Make run respect virtualenv configuration
Signed-off-by: Rohan Jain <crodjer@gmail.com>
2015-05-15 16:54:38 +05:30
Solly 3371090909 Merge pull request #175 from kanaka/bug/catch-interrupted-syscall
Catch interrupted system call
2015-05-13 16:52:31 -04:00
Bennett Kanuka 1f960d9f3c Catch interrupted system call
Closes #166
2015-05-13 16:15:25 -04:00
Solly Ross 1221960baa Introduce strict mode
This commit introduces strict mode, which is on by default.  Currently
strict mode only enforces client-to-server frame masking.  However,
in the future, it might enforce other parts of the RFC as well.

Closes #164
2015-05-13 16:03:57 -04:00
Solly Ross 2e57ee3159 Enable Travis
This commit enables running the unit tests on Travis for Python 2.6,
2.7, 3.3, and 3.4.  Note that Travis does not support Python 2.4, so
we cannot test there.
2015-05-13 16:03:57 -04:00
Solly Ross 04fd789a67 Update Tests and Test Plugins
This commit updates the unit tests to work with the current code
and adds in tests for the auth and token plugin functionality.
2015-05-13 16:03:57 -04:00
Solly Ross d5216c3166 Process plugin parameters in main
Previously, we just passed the values of '--*-plugin' and
'--*-source' directly to `LibProxyServer` and `WebSocketProxy`,
which handled turning that into an instance of the plugin class.
Now, that's done in main, and the classes receive an instance
directly.
2015-05-13 16:03:56 -04:00
Solly Ross 11f9a6cf10 Fix bug in ReadOnlyTokenFile
ReadOnlyTokenFile didn't initialize `self._targets` to `None`,
causing issues.
2015-05-13 16:03:37 -04:00
Solly Ross df10501615 Introduce Auth Plugins
Auth plugins provide a generic interface for authenticating requests.

The plugin name is specified using the '--auth-plugin' option, and
may either be the name of a class from `websockify.auth_plugins`,
or a fully qualified python path to the auth plugin class (see below).

An optional plugin parameter can be specified using the '--auth-source'
option (a value of `None` will be used if no '--auth-source' option is
specified).

Auth plugins should inherit from `websockify.auth_plugins.BasePlugin`,
and should implement the `authenticate(headers, target_host, target_port)`
method.  The value of the '--auth-source' option is available as
`self.source`.

One plugin is currently included: `ExpectOrigin`.  The `ExpectOrigin`
plugin checks that the 'Origin' header is an expected value.  The list
of acceptable origins is passed using the plugin source, as a
space-separated list.
2015-05-13 16:03:37 -04:00
Solly Ross d94b944f93 Restore functionaltiy to file_only.
There was a bug in WebSocketServer that prevented the `file_only`
parameter from actually being set as an instance property, causing
directory listings to appear even with `file_only=True`.  This
commit fixes that.

See-Also: https://bugs.launchpad.net/nova/+bug/1447675
2015-05-13 16:03:37 -04:00
Aric Stewart 52adf957b3 Add ping heartbeat option
With this option we will ping the client with a heartbeat.
Helpful for connection through firewalls that may disable inactive
connections.
2015-04-14 16:09:26 -05:00
Aric Stewart 731fd20796 Add option to turn on auto-pong
This allows the websockify server to respond to heart beat ping
messages sent by clients
2015-04-13 14:57:24 -05:00
Solly Ross 52c2e62535 Fix bug in token_plugin/target_cfg support
Previously, if no `target_cfg` flag was used, the `target_cfg` option
would not get removed from the opts dict, causing an error when it got
passed through to `WebSocketProxy`.  Now we always remove it.

Fixes #168.
2015-04-09 11:43:20 -04:00
Solly ac9d357c87 Merge pull request #161 from rafaelfolco/bug/150-websocket-bigendian
BUGFIX:  Websocket frame corruption on big-endian #150
2015-04-07 12:51:52 -04:00
Solly ce07749223 Merge pull request #162 from kanaka/feature/token-plugins
Introduce Token Plugins
2015-04-07 12:51:02 -04:00
Rafael Folco af10f458a1 BUGFIX: Websocket frame corruption on big-endian #150
This patch fixes noVNC endianess problem on handshake.
It affects noVNC sessions on Big Endian platforms.

Fixes #150
2015-04-07 09:36:21 -03:00
Solly Ross 69a8b928aa Introduce Token Plugins
Token plugins provide a generic interface for transforming a token
into a `(host, port)` tuple.

The plugin name is specified using the '--token-plugin' option,
and may either be the name of a class from `websockify.token_plugins`,
or a fully qualified python path to the token plugin class (see below).

An optional plugin parameter can be specified using the '--token-source'
option (a value of `None` will be used if no '--token-source' option is
passed).

Token plugins should inherit from `websockify.token_plugins.BasePlugin`,
and should implement the `lookup(token)` method.  The value of the
'--token-source' option is available as `self.source`.

Several plugins are included by default.  The `ReadOnlyTokenFile`
and `TokenFile` plugins implement functionality from '--target-config'
(with the former only reading the file(s) once, and the latter reading
them every time).  The 'BaseTokenAPI' plugin fetches the value from
an API, returning the result of `process_result(response_object)`.
By default, `process_result` simply returns the text of the response,
but may be overriden.  The `JSONTokenAPI` does just this, returning
the 'host' and 'port' values from the response JSON object.

The old '--target-config' option is now deprecated, and maps to the
`TokenFile` plugin under the hood.

Also-Authored-By: James Portman (@james-portman)

Closes #157
2015-03-26 16:21:25 -04:00
Solly 19b558e4cd Merge pull request #159 from vaibhavsagar/python3-trove
Add Python 3 trove classifiers.
2015-03-17 10:32:40 -04:00
Vaibhav Sagar dcdf0a0b6c Add Python 3 trove classifiers.
This makes <http://py3readiness.org> display the status of this package correctly :).
2015-03-17 07:20:26 +11:00
Solly 23045cb212 Merge pull request #155 from kanaka/bug/154-no-implicit-relative-imports
Don't use implicit relative imports
2015-02-06 11:29:57 -05:00
Solly Ross acd276e1a2 Don't use implicit relative imports
Implicit relative imports don't work in Python 3.  This converts
implicit relative imports into absolute imports
(e.g. `import websocket` becomes `from websockify import websocket`).

Fixes #154
2015-02-03 16:52:29 -05:00
Solly 999a133bc5 Merge pull request #128 from ags131/patch-1
Added 'websockify.js' to package.json files list
2015-01-05 12:15:56 -05:00
Joel Martin f36877c684 Merge pull request #140 from rpodgorny/python3
python3 compatibility fixes
2014-10-02 10:02:11 -05:00
Radek Podgorny 303a71310c python3 compatibility fixes 2014-10-02 11:42:17 +02:00
samhed 63209f84ca sync with commit from PR #347 in noVNC 2014-08-19 14:13:00 +02:00
Joel Martin d37e729587 Merge pull request #129 from mmb/master
Fix typo in reaping zombies message.
2014-05-20 18:01:00 -05:00
Matthew M. Boedicker 12191d266b Fix typo in reaping zombies message. 2014-05-20 14:43:35 -07:00
ags131 67a12a2394 Fixed Package.json
Added websockify.js to the files section, this fixes #90 and allows NPM to properly install
2014-05-16 11:56:24 -05:00
samhed 6c526fd645 remove some trailing whitespaces 2014-04-14 14:20:00 +02:00
samhed 6c1a2e9032 Avoid using the %-operator, URL-escaped strings can contain extra %'s which can cause bugs. 2014-03-18 15:21:13 +01:00
Solly Ross 739af6ecb4 Update to v0.6.0
*** NOTE ***

This version of websockify will break existing code which sub-classes
`WebsocketProxy` -- see pull requests #110 and #111
2014-02-18 17:03:09 -05:00
Joel Martin 56b740ef5c README: update with general contact info. 2014-02-03 09:26:03 -06:00
Solly Ross 18e70c52e1 Fix default log level in echo and load tests
Since we switched to using the `logging` module to log
in pull request #100, none of the messages on the 'INFO'
level were being shown from `tests/echo.py` and
`tests/load.py`, since the default log level is 'WARNING'.

Now, the log level is set to INFO in `tests/echo.py` and
`tests/load.py`, to match the log level in the main websockify
executable.

Fixes #109
2014-01-30 17:11:20 -05:00
Joel Martin cc3ccf0ddd README: fix wrap mode examples. 2014-01-13 13:25:13 -06:00
astrand 38db12c2b0 Merge pull request #111 from astrand/master
Refactor to use standard SocketServer RequestHandler design.
2013-12-19 01:31:14 -08:00
Peter Åstrand (astrand) a749611370 Merge remote branch 'upstream/master' 2013-12-19 10:28:37 +01:00
Peter Åstrand (astrand) b662d185ca Prepare for fixing https://github.com/kanaka/websockify/pull/111
Move around functions and methods, so that connection-related and
server-related stuff are close together.

This patch just moves things around - it does not change anything at
all. This can be verified with:

git diff | grep ^- | cut -c 2- | sort > removed
git diff | grep ^+ | cut -c 2- | sort > added
diff -u removed added
2013-12-19 10:24:36 +01:00
Peter Åstrand (astrand) 6de6933819 Minor whitespace and layout tweaks, to reduce diff against
upstream/master.
2013-12-17 14:20:14 +01:00
Peter Åstrand (astrand) f46db48399 Merge remote branch 'upstream/master'
Conflicts:
	websockify/websocket.py
2013-12-17 14:04:30 +01:00
Peter Åstrand (astrand) 8049ddfe26 Cherry-picked 4e3388964a from
astrand/websockify:

    Prepare for fixing https://github.com/kanaka/websockify/issues/71:
    Move around functions and methods, so that connection-related and
    server-related stuff are close together.

    This patch just moves things around - it does not change anything at
    all. This can be verified with:

    git diff websocket.py | grep ^- | cut -c 2- | sort > removed
    git diff websocket.py | grep ^+ | cut -c 2- | sort > added
    diff -u removed added
2013-12-17 13:56:20 +01:00
Peter Åstrand (astrand) f64e3dea51 Merge remote branch 'upstream/master', after #110 was merged. 2013-12-16 16:18:41 +01:00
astrand 17455afe4d Merge pull request #110 from astrand/rename-self-client
Rename self.client and new_client - prepare for #72 / #111
2013-12-16 05:45:03 -08:00
Peter Åstrand (astrand) 047ce47742 Rename new_client to new_websocket_client, in order to have a better
name in the SocketServer/HTTPServer request handler hierarchy. Prepare
for merge pull request #72. This work has been picked out of
7b3dd8a6f5 .
2013-11-28 13:33:28 +01:00
Peter Åstrand (astrand) 558402848e Rename self.client to self.request, in preparation for merging pull
request #72. The standard Python SocketServer/BaseRequestHandler
requires this name.
2013-11-28 13:23:50 +01:00
Peter Åstrand (astrand) db93395061 Follow up on 131f9ea645: Proper logging
in request handler class.
2013-11-28 12:37:57 +01:00
Peter Åstrand (astrand) e246e98b20 Merge commit '34a1b68d79a13c03aa63b5c4194796341c9383fe'
* commit '34a1b68d79a13c03aa63b5c4194796341c9383fe':
  Clarify ssl module build for old python versions.
2013-11-28 09:34:43 +01:00
Peter Åstrand (astrand) 7ecfa4f384 Merge commit 'a04edfe80f54b44df5a3579f71710560c6b7b4fc'
* commit 'a04edfe80f54b44df5a3579f71710560c6b7b4fc':
  Added temp dir for unit test data and cleanup
2013-11-28 09:34:33 +01:00
Peter Åstrand (astrand) bc216fb7d1 Merge commit 'a47be21f9fa69ddf8d888ff9e3c75cdfc9e31c00'
* commit 'a47be21f9fa69ddf8d888ff9e3c75cdfc9e31c00':
  Added unit tests for websocket and websocketproxy
2013-11-28 09:32:49 +01:00
Peter Åstrand (astrand) b92528aeba Merge commit 'c3acdc2e38f871e28ffda1847b4338c4b02296b8'
* commit 'c3acdc2e38f871e28ffda1847b4338c4b02296b8':
  Adds optional TCP_KEEPALIVE to WebSocketServer
2013-11-28 09:32:30 +01:00
Peter Åstrand (astrand) 81e2a53692 Merge commit '13c99bcf053f7f3af8ba84c0d963a9591e020f49'
* commit '13c99bcf053f7f3af8ba84c0d963a9591e020f49':
  Fix search path construction in tests.
2013-11-28 09:14:25 +01:00
Peter Åstrand (astrand) 602248425a Merge commit 'b4e0b534d5d04d57265045b4baf49dd81555064b'
* commit 'b4e0b534d5d04d57265045b4baf49dd81555064b':
  Fix crash when an import is missing
2013-11-28 09:09:03 +01:00
Peter Åstrand (astrand) 3b802c08b7 Merge commit '32b0567343aee7753b2b6be1bc1ee9a69657ba26'
* commit '32b0567343aee7753b2b6be1bc1ee9a69657ba26':
  Fix syntax errors in other/websockify.rb
2013-11-28 09:07:52 +01:00
Peter Åstrand (astrand) 972b30ddc2 Merge commit '0e5c3ecfda3b1506b41412052db75d84df2b4ae7'
* commit '0e5c3ecfda3b1506b41412052db75d84df2b4ae7':
  Handle SIGCHLD properly for multiprocessing
2013-11-28 09:07:41 +01:00
Peter Åstrand (astrand) f58b49fa08 Merge commit 'a61ae52610642ae58e914dda705df8bb9c8213ec'
* commit 'a61ae52610642ae58e914dda705df8bb9c8213ec':
  fixed 1.8 compatibility bug for OpenSSL::SSL::SSLSocket#read_nonblock vs #readpartial tested in 1.8 and 2.0
  adding SSL support and Ruby1.9 support
2013-11-28 09:07:30 +01:00
Peter Åstrand (astrand) 131f9ea645 Merge commit '477dce6cf86d61b20a394f3cbf3170e60d199658'
* commit '477dce6cf86d61b20a394f3cbf3170e60d199658':
  websocket: use python logging module
  websocket: fix exception statement introduced by comment 903e3f06ee557

Adapted to new standard SocketServer RequestHandler design. For
example, this means that self.i_am_client is not needed.
2013-11-28 09:05:24 +01:00
Peter Åstrand (astrand) cbf05f84fe Merge commit '4459824cc8196ad78fe9258b6c560ad46fe4cd52'
* commit '4459824cc8196ad78fe9258b6c560ad46fe4cd52':
  websocket: do not exit at the middle of process
  websocket: restore signals after processing
  websocket: support SIGTERM as exit signal
2013-11-27 14:49:54 +01:00
Peter Åstrand (astrand) 611da86353 Merge commit 'a7fa97f0e14926cc4433483fcb7581e0b3782140'
* commit 'a7fa97f0e14926cc4433483fcb7581e0b3782140':
  WebSocketProxy: support non path target_cfg
2013-11-27 13:41:00 +01:00
Peter Åstrand (astrand) fb4ac5ae51 Merge commit 'edde5cd0ff6059bddae10c9b9faf0b3e8f388a9e'
* commit 'edde5cd0ff6059bddae10c9b9faf0b3e8f388a9e':
  Fixed wiki reference
2013-11-27 13:38:03 +01:00
Peter Åstrand (astrand) 063c7de783 Merge commit 'bff3c373b32ebf707085e3c677bfad19b44fa054' 2013-11-27 13:34:17 +01:00
Peter Åstrand (astrand) 08d37e0deb Merge commit 'f30ad05c70ab2a43c9078e2f79da40f1dc0c60ec'
* commit 'f30ad05c70ab2a43c9078e2f79da40f1dc0c60ec':
  Fix #97: rebind.so not found when installed
2013-11-27 13:33:30 +01:00
Peter Åstrand (astrand) ff30e5461f Merge commit 'ab389d4e7114d7ddbfd085591d336ea5cc06c00d'
* commit 'ab389d4e7114d7ddbfd085591d336ea5cc06c00d':
  Collect zombie child processes within server loop
2013-11-27 13:30:30 +01:00
Peter Åstrand (astrand) 4071291fa9 Merge commit '264f8fdd7f12bd5b9f6813fb8de81c55b6328d9b'
* commit '264f8fdd7f12bd5b9f6813fb8de81c55b6328d9b':
  Update to version 0.5.1
2013-11-27 13:30:22 +01:00
Peter Åstrand (astrand) 611effd83b Merge commit '36cb8f4676c7b5ff34bd22ad729e00e77efb6f00'
* commit '36cb8f4676c7b5ff34bd22ad729e00e77efb6f00':
  Move javascript websockify files to other/js
2013-11-27 13:30:13 +01:00
Peter Åstrand (astrand) b63d197292 Merge commit '36fcd5784fa0825eedbf31d91bc42c970605ddb4'
* commit '36fcd5784fa0825eedbf31d91bc42c970605ddb4':
  Add package file for websockify.js
2013-11-27 13:30:05 +01:00
Peter Åstrand (astrand) cf901ddac5 Merge commit '46450577c2fb119fc5bf0ac09664f22651a080d0' 2013-11-27 13:29:54 +01:00
Peter Åstrand (astrand) 122985bd67 Merge commit 'd3ba23fa64d79eeb602ff1015ec31014fd8e9b35'
* commit 'd3ba23fa64d79eeb602ff1015ec31014fd8e9b35':
  Update to c0855c6cae of web-socket-js
2013-11-27 13:29:45 +01:00
Peter Åstrand (astrand) 19c8482236 Merge commit '6d27b5d321978586ea1601f757ead73dfba03da7'
* commit '6d27b5d321978586ea1601f757ead73dfba03da7':
  Add 2 arguments to websockify.WSRequestHandler

As of now, only implemented the first command; see #83 for details.
2013-11-27 13:27:38 +01:00
Peter Åstrand (astrand) 33a1bd2337 Merge commit 'b7f255ce0b21dc42189205b1f0e46b4f1d9854f9'
* commit 'b7f255ce0b21dc42189205b1f0e46b4f1d9854f9':
  Clarify messages when optional modules are not found.
2013-11-27 12:27:43 +01:00
Peter Åstrand (astrand) 5f8cb96bab Merge commit '477947ba96a00032ae35ac55fd02a4a5f485497e'
* commit '477947ba96a00032ae35ac55fd02a4a5f485497e':
  Remove wsproxy references. Sync launch.sh from noVNC.
2013-11-27 12:27:31 +01:00
Peter Åstrand (astrand) 6ddfaa86ae Merge commit '33e9a21ce4e38d52f43fe775fb154fc71c09c827'
* commit '33e9a21ce4e38d52f43fe775fb154fc71c09c827':
  Add gimite/web-socket-js submodule for DFSG compliance.
2013-11-27 12:26:53 +01:00
Peter Åstrand (astrand) 4dc7c0da7a Merge commit '2d078e8cd83735dad7d98fadcece652d93e2a037'
* commit '2d078e8cd83735dad7d98fadcece652d93e2a037':
  correctly include include directory in egg.
2013-11-27 11:36:47 +01:00
Joel Martin 34a1b68d79 Clarify ssl module build for old python versions. 2013-11-20 07:30:23 -06:00
Joel Martin a04edfe80f Merge pull request #105 from dosaboy/topic/unit-test-cleanup
Added temp dir for unit test data and cleanup
2013-11-20 05:21:18 -08:00
Edward Hope-Morley 32c1abd5d9 Added temp dir for unit test data and cleanup
Unit test data will now go to a temporary dir that will be deleted
once the test completes. The unit tests also setup a logger which
will persist so that it can be inspected once tests complete.

Also fixes a bug where instance var is missing from decode_hybi()

Co-authored-by: natsume.takashi@lab.ntt.co.jp
2013-11-14 12:38:03 +00:00
Joel Martin a47be21f9f Merge pull request #94 from dosaboy/topic/add-unit-tests
Added unit tests for websocket and websocketproxy
2013-10-29 09:44:48 -07:00
Edward Hope-Morley 5e0ff7d855 Added unit tests for websocket and websocketproxy
To run the unit tests just run tox from the top
level directory which will try to run unit tests
for most versions of python. Requires tox to be
installed. To run tox for a specifice env, run
tox -e<env> e.g. for python 2.7 run 'tox -epy27'.

Co-authored-by: natsume.takashi@lab.ntt.co.jp
2013-10-29 16:19:39 +00:00
Joel Martin c3acdc2e38 Merge pull request #93 from dosaboy/topic/set-keepalive-options
Adds optional TCP_KEEPALIVE to WebSocketServer
2013-10-29 08:18:53 -07:00
Joel Martin 13c99bcf05 Fix search path construction in tests.
This way the tests can be run directly from within the tests directory
and they might also have a chance of running on Windows (where the
path separator is different).
2013-10-29 09:41:26 -05:00
Samuel b4e0b534d5 Merge pull request #104 from stevschmid/patch-1
Fix crash when module is missing
2013-10-24 04:22:44 -07:00
Steven Schmid be4119f84f Fix crash when an import is missing
self.msg is not available when checking the imports.

I had the problem on a host where numpy is missing (python 2.7.3).
2013-10-24 11:42:49 +02:00
Solly 32b0567343 Merge pull request #103 from dellsystem/master
Fix syntax errors in other/websockify.rb
2013-10-21 18:29:45 -07:00
dellsystem 82fe329129 Fix syntax errors in other/websockify.rb 2013-10-21 20:59:16 -04:00
Solly 0e5c3ecfda Merge pull request #102 from DirectXMan12/master
Handle SIGCHLD properly for multiprocessing (fixes #101)
2013-10-21 17:16:09 -07:00
Solly Ross 53f1f1989e Handle SIGCHLD properly for multiprocessing
This commit should fix #101 by enabling a special SIGCHLD
handler for when multiprocessing is in use.  The handler
simply calls `multiprocessing.active_children()` (which in
turn calls `_cleanup()`) upon receiving a SIGCHLD.  Now,
the `fallback_SIGCHLD` is only called when `multiprocessing`
is not in use.  See also #95.
2013-10-21 16:52:42 -04:00
Edward Hope-Morley 081046b6cd Adds optional TCP_KEEPALIVE to WebSocketServer
TCP_KEEPALIVE is now enabled by default. Settings for
KEEPCNT, KEEPINTVL and KEEPIDLE can be supplied when
creating WebSocketServer and KEEPALIVE can also be
disabled if required.

Also adds new unit test for testing.

Co-authored-by: natsume.takashi@lab.ntt.co.jp
2013-10-16 11:24:21 +01:00
Joel Martin a61ae52610 Merge pull request #86 from chrislee35/master
OpenSSL Support for Ruby Module of Websockify
2013-10-15 10:32:57 -07:00
Joel Martin 477dce6cf8 Merge pull request #100 from alonbl/log
websocket: use python logging module
2013-10-15 09:41:35 -07:00
Alon Bar-Lev 8a0a47223d websocket: use python logging module
WebSocketServer is a library module, as such, it cannot assume it can
write output to process stdout.

Python logging module is designed in order to allow subscribers to
handle the output out of modules. It is simple and generic mechanism to
separate between data producer and data handling.

Python logging API also has the nature of log level, so the verbose
parameter can probably be obsoleted in favor of logging level. And of
course the logging API has built in support for exception tracebacks, no
need for manual format.

Per upstream request a wrapper is created around python logging to
enable shorter statements and optional replacement.

Add --traffic parameter for traffic specific debug, this is required as
it uses direct unformatted stdout output.

Signed-off-by: Alon Bar-Lev <alon.barlev@gmail.com>
2013-10-15 19:18:17 +03:00
Alon Bar-Lev 1f798214de websocket: fix exception statement introduced by comment 903e3f06ee557
Signed-off-by: Alon Bar-Lev <alon.barlev@gmail.com>
2013-10-15 19:18:17 +03:00
Joel Martin 4459824cc8 Merge pull request #98 from alonbl/apicleanup
Minor API cleanups
2013-10-14 13:12:40 -07:00
Alon Bar-Lev c2a40d6900 websocket: do not exit at the middle of process
WebSocketServer is a library module, as such it should not exit process
but return from a method, allowing the caller to execute process show
down.

Signed-off-by: Alon Bar-Lev <alon.barlev@gmail.com>
2013-10-14 22:17:19 +03:00
Joel Martin a7fa97f0e1 Merge pull request #99 from alonbl/targetconfig
WebSocketProxy: support non path target_cfg
2013-10-14 11:38:05 -07:00
Alon Bar-Lev 1190fe1204 websocket: restore signals after processing
WebSocketServer is a library module, as such it should try to restore state
after processing, to allow caller to resume normal operation.

Signed-off-by: Alon Bar-Lev <alon.barlev@gmail.com>
2013-10-14 21:13:11 +03:00
Alon Bar-Lev 7026e26d68 websocket: support SIGTERM as exit signal
Similar to SIGINT that is already supported, support SIGTERM in daemon
and non daemon modes.

Signed-off-by: Alon Bar-Lev <alon.barlev@gmail.com>
2013-10-14 21:01:44 +03:00
Alon Bar-Lev 37e40a78f2 WebSocketProxy: support non path target_cfg
The WebSocketProxy class is usable for creating derived applications
with different logic, especially for the target validation.

Current code assumes that target is a path while in other implementation
it can be object that is loaded at initialization.

This change moves the conversion to absolute path into main function, so
that the WebSocketProxy class will not make that assumption.

Signed-off-by: Alon Bar-Lev <alon.barlev@gmail.com>
2013-10-14 20:31:24 +03:00
Samuel edde5cd0ff Merge pull request #91 from MartinF/master
Fixed wiki reference
2013-10-09 06:50:08 -07:00
Joel Martin bff3c373b3 Merge pull request #96 from DirectXMan12/master
Enable Process Reaping in All Conditions
2013-09-27 06:05:55 -07:00
Joel Martin f30ad05c70 Fix #97: rebind.so not found when installed
This should fix the upstream Debian bug:
http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=719889
2013-09-27 07:42:57 -05:00
directxman12 354dd6b0a2 Enable Process Reaping in All Conditions
Process reaping via the SIGCHLD handler is now enabled in all
circumstances, instead of just when os.fork is being used.

Fixes #95
2013-09-20 14:12:35 -04:00
Martin From e06de83295 Fixed wiki reference 2013-09-09 11:32:57 +02:00
Joel Martin ab389d4e71 Merge pull request #85 from dillaman/master
Collect zombie child processes within server loop
2013-07-02 10:55:53 -07:00
chrislee35 eee946d2cf fixed 1.8 compatibility bug for OpenSSL::SSL::SSLSocket#read_nonblock vs #readpartial
tested in 1.8 and 2.0
2013-07-02 22:16:01 +09:00
chrislee35 73af324a3a adding SSL support and Ruby1.9 support 2013-07-02 21:38:22 +09:00
Jason Dillaman 832118e61a Collect zombie child processes within server loop 2013-06-28 15:22:35 -04:00
Joel Martin 264f8fdd7f Update to version 0.5.1
*** NOTE ***

This version of websockify no longer supports the old Hixie protocol.
2013-06-27 16:35:54 -05:00
Joel Martin 36cb8f4676 Move javascript websockify files to other/js 2013-06-27 16:29:17 -05:00
Joel Martin 36fcd5784f Add package file for websockify.js 2013-06-27 16:29:17 -05:00
Joel Martin 46450577c2 Use upstream einaros/ws 0.4.27 with sub-protocol support.
Also, some connection error handling.
2013-06-27 16:29:17 -05:00
Joel Martin d3ba23fa64 Update to c0855c6cae of web-socket-js
Update both the submodule and the swf build. The submodule now
contains the unobfuscated source for swfobject.js which should make
websockify more DFSG compliant.
2013-06-27 16:29:17 -05:00
Joel Martin 6d27b5d321 Merge pull request #84 from nttdatainc-openstackers/master
Fix for issue #83
2013-06-27 14:01:07 -07:00
NTT Data OpenStackers 888e75a8fb Add 2 arguments to websockify.WSRequestHandler
This patch adds 2 arguments to websockify.WSRequestHandler for security:

* file_only: returns 404 response if non-file contents are requested.
             Required to disable directory listing.

* no_parent: returns 403 response if contents out of the web root are
             requested. Required to disable directory traversal.
2013-06-24 06:09:01 -07:00
Joel Martin b7f255ce0b Clarify messages when optional modules are not found. 2013-04-19 17:14:20 -05:00
Joel Martin 477947ba96 Remove wsproxy references. Sync launch.sh from noVNC. 2013-04-15 12:22:08 -05:00
Joel Martin 33e9a21ce4 Add gimite/web-socket-js submodule for DFSG compliance. 2013-04-12 08:24:07 -05:00
Joel Martin 2d078e8cd8 Merge pull request #76 from SlapOS/include
correctly include include directory in egg.
2013-04-03 09:44:37 -07:00
Cédric de Saint Martin d6d960dbe5 correctly include include directory in egg. 2013-04-03 12:12:37 +02:00
Peter Åstrand (astrand) ed109d7ec8 Fix Python3 compatibility when using --libserver. 2013-03-20 15:09:58 +01:00
Peter Åstrand (astrand) bc917863e0 Improved class documentation. 2013-03-20 13:30:16 +01:00
Peter Åstrand (astrand) e964c1edff Let our ProxyRequestHandler be default. This allows you to inherit
from WebSocketProxy without having to specify handler class.
2013-03-20 11:34:46 +01:00
Peter Åstrand (astrand) f5e42ff6f4 Move WebSocketProxy class so that it is defined after the
requesthandler. Removed comments about above/below which does not make
sense any longer. No functional changes.
2013-03-20 11:30:38 +01:00
Peter Åstrand (astrand) f594d70daf Removed unused import of SimpleHTTPRequestHandler. 2013-03-20 11:00:34 +01:00
Peter Åstrand (astrand) d0608a63b6 Make echo.py and load.py work again, after the refactoring of
websocket.py:

* With echo.py, which doesn't need any server configuration, we can
  just switch over our application class to inherit from
  WebSocketRequestHandler instead of WebSocketServer. Also, need to
  use the new method name new_websocket_client.

* With load.py, since we have the "delay" configuration, we need both
  a server class and a request handler.

Note that for both tests, I've removed the raising of
self.EClose(closed). This is incorrect. First of all, it's described
as "An exception before the WebSocket connection was established", so
not suitable for our case. Second, it will cause send_close to be
called twice. Finally, self.EClose is now in the WebSocketServer
class, and not a member of the request handler.
2013-03-20 10:03:04 +01:00
Peter Åstrand (astrand) 09f3ec7125 Rename self.client to self.request, ie adapt to:
>commit b9e1295f7a
>    Prepare for solving https://github.com/kanaka/websockify/issues/71:
>
>    Rename self.client to self.request, since this is what standard
>    SocketServer request handlers are using.
2013-03-20 09:03:18 +01:00
Peter Åstrand (astrand) 95593ac4bf Merge branch 'master' of github.com:astrand/websockify 2013-03-18 14:59:03 +01:00
Peter Åstrand (astrand) 1eecc7b17a Merge remote branch 'upstream/master' 2013-03-18 14:50:46 +01:00
Peter Åstrand (astrand) b05b773bd7 Corrected last commit. 2013-03-18 13:25:53 +01:00
Peter Åstrand (astrand) debc926612 Renamed CustomProxyServer to WebSocketProxy; this was the earlier name.
Also, call the server instance "server", not "httpd", even when using
LibProxyServer.
2013-03-18 13:22:48 +01:00
Peter Åstrand (astrand) 70eb75a3e6 Fix error with modern Python 2.X versions:
TypeError: exceptions must be old-style classes or derived from
BaseException, not str

Thus, we are not allowed to raise a string. Raise Exception instead.
2013-03-18 12:04:50 +01:00
Joel Martin 903198a724 tests: use new module path and remove Hixie code. 2013-03-14 12:24:58 -05:00
Peter Åstrand (astrand) 7b3dd8a6f5 Try to solve https://github.com/kanaka/websockify/issues/71 by
refactoring. Basically, we are dividing WebSocketServer into two
classes: One request handler following the SocketServer Requesthandler
API, and one optional server engine. The standard Python SocketServer
engine can also be used.

websocketproxy.py has been updated to match the API change. I've also
added a new option --libserver in order to use the Python built in
server instead.

I've done a lot of testing with the new code. This includes: verbose,
daemon, run-once, timeout, idle-timeout, ssl, web, libserver. I've
tested both Python 2 and 3. I've also tested websocket.py in another
external service.

Code details follows:

* The new request handler class is called WebSocketRequestHandler,
  inheriting SimpleHTTPRequestHandler.

* The service engine is called WebSocketServer, just like before.

* do_websocket_handshake: Using send_header() etc, instead of manually
  sending HTTP response.

* A new method called handle_websocket() upgrades the connection to
  WebSocket, if requested. Otherwise, it returns False. A typical
  application use is:

    def do_GET(self):
        if not self.handle_websocket():
	   # handle normal requests

* new_client has been renamed to new_websocket_client, in order to
  have a better name in the SocketServer/HTTPServer request handler
  hierarchy.

* Note that in the request handler, configuration variables must be
  provided by the "server" object, ie self.server.target_host.
2013-03-14 16:07:40 +01:00
Peter Åstrand (astrand) 208f83b9a2 Prepare for fixing https://github.com/kanaka/websockify/issues/71:
* Move traffic_legend.

* Since websocket.WebSocketServer.socket is static, don't call it with
  self.socket.
2013-03-14 16:00:11 +01:00
Peter Åstrand (astrand) 4e3388964a Prepare for fixing https://github.com/kanaka/websockify/issues/71:
Move around functions and methods, so that connection-related and
server-related stuff are close together.

This patch just moves things around - it does not change anything at
all. This can be verified with:

git diff websocket.py | grep ^- | cut -c 2- | sort > removed
git diff websocket.py | grep ^+ | cut -c 2- | sort > added
diff -u removed added
2013-03-14 15:50:49 +01:00
Peter Åstrand (astrand) b9e1295f7a Prepare for solving https://github.com/kanaka/websockify/issues/71:
Rename self.client to self.request, since this is what standard
SocketServer request handlers are using.
2013-03-14 15:23:44 +01:00
Joel Martin d3865688c8 README: no longer supporting Hixie.
NOTE: Hixie support is in version 0.4.X. If necessary, I will branch
and apply critical fixes release a new 0.4.X tagged version.
2013-03-12 13:54:11 -05:00
Joel Martin 5a726c2e4d Merge pull request #70 from astrand/master
Remove support for old Hixie protocols
2013-03-12 11:52:30 -07:00
Joel Martin db6a7e3e28 Update to version 0.4.1
*** IMPORTANT NOTE ***

0.4.X is the last minor version to support the Hixie protocol. 0.5.0
will drop the Hixie protocol support. The latest released version of
all major browsers (and web-socket-js) support the IETF 6455 protocol.

- Other changes: add 'include/' sub-dir to source distribution and to
  installed packaged.
2013-03-12 13:46:45 -05:00
Peter Åstrand (astrand) b2fe57c950 Remove support for old Hixie protocols.
Hybi protocols 7 and 8 are still supported, in addition to
protocol 13 -  RFC 6455.
2013-03-04 09:38:29 +01:00
34 changed files with 1925 additions and 804 deletions
+2
View File
@@ -9,3 +9,5 @@ other/node_modules
.pydevproject .pydevproject
target.cfg target.cfg
target.cfg.d target.cfg.d
.tox
*.egg-info
+3
View File
@@ -0,0 +1,3 @@
[submodule "include/web-socket-js-project"]
path = include/web-socket-js-project
url = https://github.com/gimite/web-socket-js.git
+10
View File
@@ -0,0 +1,10 @@
language: python
python:
- 2.6
- 2.7
- 3.3
- 3.4
install: pip install -r test-requirements.txt
script: python setup.py nosetests --verbosity=3
+43
View File
@@ -1,6 +1,49 @@
Changes Changes
======= =======
0.7.0
-----
* Python 3 support fixes (#140, #155, #159)
* Generic token-parsing plugins support (#162)
* Generic authentication plugins support (#172)
* Fixed frame corruption on big-endian systems (#161)
* Support heartbeats (via PING) and automatic responses to PONG (#169)
* Automatically reject unmasked client frames by default (strict mode) (#174)
* Automatically restart interrupted select calls (#175)
* Make 'run' respect environment settings (including virtualenv) (#176)
0.6.1 - May 11, 2015
--------------------
* **PATCH RELEASE**: Fixes a bug causing file_only to not be passed properly
0.6.0 - Feb 18, 2014
--------------------
* **NOTE** : 0.6.0 will break existing code that sub-classes WebsocketProxy
* Refactor to use standard SocketServer RequestHandler design
* Fix zombie process bug on certain systems when using multiprocessing
* Add better unit tests
* Log information via python `logging` module
0.5.1 - Jun 27, 2013
--------------------
* use upstream einaros/ws (>=0.4.27) with websockify.js
* file_only and no_parent security options for WSRequestHandler
* Update build of web-socket-js (c0855c6cae)
* add include/web-socket-js-project submodule to gimite/web-socket-js
for DSFG compliance.
* drop Hixie protocol support
0.4.1 - Mar 12, 2013
--------------------
* ***NOTE*** : 0.5.0 will drop Hixie protocol support
* add include/ directory and remove some dev files from source
distribution.
0.4.0 - Mar 12, 2013 0.4.0 - Mar 12, 2013
-------------------- --------------------
+2 -1
View File
@@ -1 +1,2 @@
include CHANGES.txt *.py README.md LICENSE.txt include CHANGES.txt README.md LICENSE.txt
graft include
+41 -26
View File
@@ -8,26 +8,41 @@ to normal socket traffic. Websockify accepts the WebSockets handshake,
parses it, and then begins forwarding traffic between the client and parses it, and then begins forwarding traffic between the client and
the target in both directions. the target in both directions.
### News/help/contact
Notable commits, announcements and news are posted to
<a href="http://www.twitter.com/noVNC">@noVNC</a>
If you are a websockify developer/integrator/user (or want to be)
please join the <a
href="https://groups.google.com/forum/?fromgroups#!forum/novnc">noVNC/websockify
discussion group</a>
Bugs and feature requests can be submitted via [github
issues](https://github.com/kanaka/websockify/issues).
If you want to show appreciation for websockify you could donate to a great
non-profits such as: [Compassion
International](http://www.compassion.com/), [SIL](http://www.sil.org),
[Habitat for Humanity](http://www.habitat.org), [Electronic Frontier
Foundation](https://www.eff.org/), [Against Malaria
Foundation](http://www.againstmalaria.com/), [Nothing But
Nets](http://www.nothingbutnets.net/), etc. Please tweet <a
href="http://www.twitter.com/noVNC">@noVNC</a> if you do.
### WebSockets binary data ### WebSockets binary data
Websockify supports all versions of the WebSockets protocol (Hixie and Starting with websockify 0.5.0, only the HyBi / IETF
HyBi). The older Hixie versions of the protocol only support UTF-8 6455 WebSocket protocol is supported.
text payloads. In order to transport binary data over UTF-8 an
encoding must used to encapsulate the data within UTF-8.
With Hixie clients, Websockify uses base64 to encode all traffic to Websockify negotiates whether to base64 encode traffic to and from the
and from the client. This does not affect the data between websockify client via the subprotocol header (Sec-WebSocket-Protocol). The valid
and the server. subprotocol values are 'binary' and 'base64' and if the client sends
both then the server (the python implementation) will prefer 'binary'.
With HyBi clients, websockify negotiates whether to base64 encode The 'binary' subprotocol indicates that the data will be sent raw
traffic to and from the client via the subprotocol header using binary WebSocket frames. Some HyBi clients (such as the Flash
(Sec-WebSocket-Protocol). The valid subprotocol values are 'binary' fallback and older Chrome and iOS versions) do not support binary data
and 'base64' and if the client sends both then the server (the python which is why the negotiation is necessary.
implementation) will prefer 'binary'. The 'binary' subprotocol
indicates that the data will be sent raw using binary WebSocket
frames. Some HyBi clients (such as the Flash fallback and older Chrome
and iOS versions) do not support binary data which is why the
negotiation is necessary.
### Encrypted WebSocket connections (wss://) ### Encrypted WebSocket connections (wss://)
@@ -123,7 +138,7 @@ new (moved) port of the program.
The program wrap mode is invoked by replacing the target with `--` The program wrap mode is invoked by replacing the target with `--`
followed by the program command line to wrap. followed by the program command line to wrap.
`./websockify 2023 -- PROGRAM ARGS` `./run 2023 -- PROGRAM ARGS`
The `--wrap-mode` option can be used to indicate what action to take The `--wrap-mode` option can be used to indicate what action to take
when the wrapped program exits or daemonizes. when the wrapped program exits or daemonizes.
@@ -132,16 +147,17 @@ Here is an example of using websockify to wrap the vncserver command
(which backgrounds itself) for use with (which backgrounds itself) for use with
[noVNC](https://github.com/kanaka/noVNC): [noVNC](https://github.com/kanaka/noVNC):
`./websockify 5901 --wrap-mode=ignore -- vncserver -geometry 1024x768 :1` `./run 5901 --wrap-mode=ignore -- vncserver -geometry 1024x768 :1`
Here is an example of wrapping telnetd (from krb5-telnetd).telnetd Here is an example of wrapping telnetd (from krb5-telnetd). telnetd
exits after the connection closes so the wrap mode is set to respawn exits after the connection closes so the wrap mode is set to respawn
the command: the command:
`sudo ./websockify 2023 --wrap-mode=respawn -- telnetd -debug 2023` `sudo ./run 2023 --wrap-mode=respawn -- telnetd -debug 2023`
The `wstelnet.html` page demonstrates a simple WebSockets based telnet The `wstelnet.html` page demonstrates a simple WebSockets based telnet
client. client (use 'localhost' and '2023' for the host and port
respectively).
### Building the Python ssl module (for python 2.5 and older) ### Building the Python ssl module (for python 2.5 and older)
@@ -150,11 +166,10 @@ client.
`sudo aptitude install python-dev bluetooth-dev` `sudo aptitude install python-dev bluetooth-dev`
* Download, build the ssl module and symlink to it: * At the top level of the websockify repostory, download, build and
symlink the ssl module:
`cd websockify/` `wget --no-check-certificate http://pypi.python.org/packages/source/s/ssl/ssl-1.15.tar.gz`
`wget http://pypi.python.org/packages/source/s/ssl/ssl-1.15.tar.gz`
`tar xvzf ssl-1.15.tar.gz` `tar xvzf ssl-1.15.tar.gz`
+1 -1
View File
@@ -12,7 +12,7 @@ Installation
--------------------------- ---------------------------
1. This service requires websockify.exe be in the same directory. Instructions on how to compile websockify python script as a windows executable can be found here: 1. This service requires websockify.exe be in the same directory. Instructions on how to compile websockify python script as a windows executable can be found here:
https://github.com/kanaka/noVNC/wiki/Compiling-Websockify-to-Windows-Executable https://github.com/kanaka/websockify/wiki/Compiling-Websockify-as-Windows-Executable
2.To add this service to a Windows PC you need to run the commandline as administrator and then run this line: 2.To add this service to a Windows PC you need to run the commandline as administrator and then run this line:
+4 -1
View File
@@ -1,4 +1,4 @@
- Update setup.py and CHANGES.txt and commit - Update setup.py, CHANGES.txt and other/package.json and commit
- Create version tag and tarball from tag - Create version tag and tarball from tag
WVER=0.1.0 WVER=0.1.0
git tag v${WVER} git tag v${WVER}
@@ -8,3 +8,6 @@
python setup.py register python setup.py register
- Upload the source distribution to pypi - Upload the source distribution to pypi
python setup.py sdist upload python setup.py sdist upload
- Register with npmjs.org (once)
- Upload websockify.js npmjs.org package
npm publish other/js
Binary file not shown.
+9 -7
View File
@@ -262,7 +262,7 @@ function on(evt, handler) {
eventHandlers[evt] = handler; eventHandlers[evt] = handler;
} }
function init(protocols) { function init(protocols, ws_schema) {
rQ = []; rQ = [];
rQi = 0; rQi = 0;
sQ = []; sQ = [];
@@ -277,12 +277,13 @@ function init(protocols) {
('set' in Uint8Array.prototype)) { ('set' in Uint8Array.prototype)) {
bt = true; bt = true;
} }
// Check for full binary type support in WebSocket
// Check for full binary type support in WebSockets // Inspired by:
// TODO: this sucks, the property should exist on the prototype // https://github.com/Modernizr/Modernizr/issues/370
// but it does not. // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/websockets/binary.js
try { try {
if (bt && ('binaryType' in (new WebSocket("ws://localhost:17523")))) { if (bt && ('binaryType' in WebSocket.prototype ||
!!(new WebSocket(ws_schema + '://.').binaryType))) {
Util.Info("Detected binaryType support in WebSockets"); Util.Info("Detected binaryType support in WebSockets");
wsbt = true; wsbt = true;
} }
@@ -325,7 +326,8 @@ function init(protocols) {
} }
function open(uri, protocols) { function open(uri, protocols) {
protocols = init(protocols); var ws_schema = uri.match(/^([a-z]+):\/\//)[1];
protocols = init(protocols, ws_schema);
if (test_mode) { if (test_mode) {
websocket = {}; websocket = {};
+7
View File
@@ -0,0 +1,7 @@
A JavaScript implementation of the websockify WebSocket-to-TCP bridge/proxy.
Copyright (C) 2013 - Joel Martin (github.com/kanaka)
Licensed under LGPL-3.
See http://github.com/kanaka/websockify for more info.
+24
View File
@@ -0,0 +1,24 @@
{
"author": "Joel Martin <github@martintribe.org> (http://github.com/kanaka)",
"name": "websockify",
"description": "websockify is a WebSocket-to-TCP proxy/bridge",
"license": "LGPL-3",
"version": "0.7.0",
"repository": {
"type": "git",
"url": "git://github.com/kanaka/websockify.git"
},
"files": ["../../docs/LICENSE.LGPL-3","websockify.js"],
"bin": {
"websockify": "./websockify.js"
},
"engines": {
"node": ">=0.8.9"
},
"dependencies": {
"ws": ">=0.4.27",
"base64": "latest",
"optimist": "latest",
"policyfile": "latest"
}
}
+9 -12
View File
@@ -7,14 +7,6 @@
// Known to work with node 0.8.9 // Known to work with node 0.8.9
// Requires node modules: ws, optimist and policyfile // Requires node modules: ws, optimist and policyfile
// npm install ws optimist policyfile // npm install ws optimist policyfile
//
// NOTE:
// This version requires a patched version of einaros/ws that supports
// subprotocol negotiation. You can use the patched version like this:
//
// cd websockify/other
// git clone https://github.com/kanaka/ws
// npm link ./ws
var argv = require('optimist').argv, var argv = require('optimist').argv,
@@ -37,6 +29,7 @@ var argv = require('optimist').argv,
// Handle new WebSocket client // Handle new WebSocket client
new_client = function(client) { new_client = function(client) {
var clientAddr = client._socket.remoteAddress, log; var clientAddr = client._socket.remoteAddress, log;
console.log(client.upgradeReq.url);
log = function (msg) { log = function (msg) {
console.log(' ' + clientAddr + ': '+ msg); console.log(' ' + clientAddr + ': '+ msg);
}; };
@@ -61,6 +54,12 @@ new_client = function(client) {
}); });
target.on('end', function() { target.on('end', function() {
log('target disconnected'); log('target disconnected');
client.close();
});
target.on('error', function() {
log('target connection error');
target.end();
client.close();
}); });
client.on('message', function(msg) { client.on('message', function(msg) {
@@ -126,11 +125,9 @@ http_request = function (request, response) {
// Select 'binary' or 'base64' subprotocol, preferring 'binary' // Select 'binary' or 'base64' subprotocol, preferring 'binary'
selectProtocol = function(protocols, callback) { selectProtocol = function(protocols, callback) {
var plist = protocols ? protocols.split(',') : ""; if (protocols.indexOf('binary') >= 0) {
var plist = protocols.split(',');
if (plist.indexOf('binary') >= 0) {
callback(true, 'binary'); callback(true, 'binary');
} else if (plist.indexOf('base64') >= 0) { } else if (protocols.indexOf('base64') >= 0) {
callback(true, 'base64'); callback(true, 'base64');
} else { } else {
console.log("Client must support 'binary' or 'base64' protocol"); console.log("Client must support 'binary' or 'base64' protocol");
+14 -4
View File
@@ -16,6 +16,8 @@ usage() {
echo " Default: localhost:5900" echo " Default: localhost:5900"
echo " --cert CERT Path to combined cert/key file" echo " --cert CERT Path to combined cert/key file"
echo " Default: self.pem" echo " Default: self.pem"
echo " --web WEB Path to web files (e.g. vnc.html)"
echo " Default: ./"
exit 2 exit 2
} }
@@ -24,6 +26,7 @@ HERE="$(cd "$(dirname "$0")" && pwd)"
PORT="6080" PORT="6080"
VNC_DEST="localhost:5900" VNC_DEST="localhost:5900"
CERT="" CERT=""
WEB=""
proxy_pid="" proxy_pid=""
die() { die() {
@@ -50,6 +53,7 @@ while [ "$*" ]; do
--listen) PORT="${OPTARG}"; shift ;; --listen) PORT="${OPTARG}"; shift ;;
--vnc) VNC_DEST="${OPTARG}"; shift ;; --vnc) VNC_DEST="${OPTARG}"; shift ;;
--cert) CERT="${OPTARG}"; shift ;; --cert) CERT="${OPTARG}"; shift ;;
--web) WEB="${OPTARG}"; shift ;;
-h|--help) usage ;; -h|--help) usage ;;
-*) usage "Unknown chrooter option: ${param}" ;; -*) usage "Unknown chrooter option: ${param}" ;;
*) break ;; *) break ;;
@@ -60,18 +64,24 @@ done
which netstat >/dev/null 2>&1 \ which netstat >/dev/null 2>&1 \
|| die "Must have netstat installed" || die "Must have netstat installed"
netstat -ltn | grep -qs "${PORT}.*LISTEN" \ netstat -ltn | grep -qs "${PORT} .*LISTEN" \
&& die "Port ${PORT} in use. Try --listen PORT" && die "Port ${PORT} in use. Try --listen PORT"
trap "cleanup" TERM QUIT INT EXIT trap "cleanup" TERM QUIT INT EXIT
# Find vnc.html # Find vnc.html
if [ -e "$(pwd)/vnc.html" ]; then if [ -n "${WEB}" ]; then
if [ ! -e "${WEB}/vnc.html" ]; then
die "Could not find ${WEB}/vnc.html"
fi
elif [ -e "$(pwd)/vnc.html" ]; then
WEB=$(pwd) WEB=$(pwd)
elif [ -e "${HERE}/../vnc.html" ]; then elif [ -e "${HERE}/../vnc.html" ]; then
WEB=${HERE}/../ WEB=${HERE}/../
elif [ -e "${HERE}/vnc.html" ]; then elif [ -e "${HERE}/vnc.html" ]; then
WEB=${HERE} WEB=${HERE}
elif [ -e "${HERE}/../share/novnc/vnc.html" ]; then
WEB=${HERE}/../share/novnc/
else else
die "Could not find vnc.html" die "Could not find vnc.html"
fi fi
@@ -92,7 +102,7 @@ else
fi fi
echo "Starting webserver and WebSockets proxy on port ${PORT}" echo "Starting webserver and WebSockets proxy on port ${PORT}"
${HERE}/wsproxy.py --web ${WEB} ${CERT:+--cert ${CERT}} ${PORT} ${VNC_DEST} & ${HERE}/websockify --web ${WEB} ${CERT:+--cert ${CERT}} ${PORT} ${VNC_DEST} &
proxy_pid="$!" proxy_pid="$!"
sleep 1 sleep 1
if ! ps -p ${proxy_pid} >/dev/null; then if ! ps -p ${proxy_pid} >/dev/null; then
@@ -101,7 +111,7 @@ if ! ps -p ${proxy_pid} >/dev/null; then
exit 1 exit 1
fi fi
echo -e "\n\nNavigate to to this URL:\n" echo -e "\n\nNavigate to this URL:\n"
echo -e " http://$(hostname):${PORT}/vnc.html?host=$(hostname)&port=${PORT}\n" echo -e " http://$(hostname):${PORT}/vnc.html?host=$(hostname)&port=${PORT}\n"
echo -e "Press Ctrl-C to exit\n\n" echo -e "Press Ctrl-C to exit\n\n"
+1 -1
View File
@@ -795,7 +795,7 @@ void start_server() {
} }
handler_msg("handler exit\n"); handler_msg("handler exit\n");
} else { } else {
handler_msg("wsproxy exit\n"); handler_msg("websockify exit\n");
} }
} }
+61 -24
View File
@@ -9,11 +9,22 @@
# - http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10 # - http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10
require 'gserver' require 'gserver'
require 'openssl'
require 'stringio' require 'stringio'
require 'digest/md5' require 'digest/md5'
require 'digest/sha1' require 'digest/sha1'
require 'base64' require 'base64'
unless OpenSSL::SSL::SSLSocket.instance_methods.index("read_nonblock")
module OpenSSL
module SSL
class SSLSocket
alias :read_nonblock :readpartial
end
end
end
end
class EClose < Exception class EClose < Exception
end end
@@ -44,7 +55,17 @@ Sec-WebSocket-Accept: %s\r
host = opts['listen_host'] || GServer::DEFAULT_HOST host = opts['listen_host'] || GServer::DEFAULT_HOST
super(port, host) super(port, host)
msg opts.inspect
if opts['server_cert']
msg "creating ssl context"
@sslContext = OpenSSL::SSL::SSLContext.new
@sslContext.cert = OpenSSL::X509::Certificate.new(File.open(opts['server_cert']))
@sslContext.key = OpenSSL::PKey::RSA.new(File.open(opts['server_key']))
@sslContext.ca_file = opts['server_cert']
@sslContext.verify_mode = OpenSSL::SSL::VERIFY_NONE
@sslContext.verify_depth = 0
end
@@client_id = 0 # Track client number total on class @@client_id = 0 # Track client number total on class
@verbose = opts['verbose'] @verbose = opts['verbose']
@@ -53,6 +74,18 @@ Sec-WebSocket-Accept: %s\r
def serve(io) def serve(io)
@@client_id += 1 @@client_id += 1
msg self.inspect
if @sslContext
msg "Enabling SSL context"
ssl = OpenSSL::SSL::SSLSocket.new(io, @sslContext)
#ssl.sync_close = true
#ssl.sync = true
msg "SSL accepting"
ssl.accept
io = ssl # replace the unencrypted handle with the encrypted one
end
msg "initializing thread"
# Initialize per thread state # Initialize per thread state
t = Thread.current t = Thread.current
@@ -61,11 +94,11 @@ Sec-WebSocket-Accept: %s\r
t[:recv_part] = nil t[:recv_part] = nil
t[:base64] = nil t[:base64] = nil
puts "in serve, client: #{t[:client].inspect}" puts "in serve, client: #{t[:my_client_id].inspect}"
begin begin
t[:client] = do_handshake(io) t[:client] = do_handshake(io)
new_client(t[:client]) new_websocket_client(t[:client])
rescue EClose => e rescue EClose => e
msg "Client closed: #{e.message}" msg "Client closed: #{e.message}"
return return
@@ -85,12 +118,12 @@ Sec-WebSocket-Accept: %s\r
if @verbose then print token; STDOUT.flush; end if @verbose then print token; STDOUT.flush; end
end end
def msg(msg) def msg(m)
puts "% 3d: %s" % [Thread.current[:my_client_id], msg] printf("% 3d: %s\n", Thread.current[:my_client_id] || 0, m)
end end
def vmsg(msg) def vmsg(m)
if @verbose then msg(msg) end if @verbose then msg(m) end
end end
# #
@@ -110,12 +143,11 @@ Sec-WebSocket-Accept: %s\r
def unmask(buf, hlen, length) def unmask(buf, hlen, length)
pstart = hlen + 4 pstart = hlen + 4
mask = buf[hlen...hlen+4] mask = buf[hlen...hlen+4].each_byte.map{|b|b}
data = buf[pstart...pstart+length] data = buf[pstart...pstart+length]
#data = data.bytes.zip(mask.bytes.cycle(length)).map { |d,m| d^m } #data = data.bytes.zip(mask.bytes.cycle(length)).map { |d,m| d^m }
for i in (0...data.length) do i=-1
data[i] ^= mask[i%4] data = data.each_byte.map{|b| i+=1; (b ^ mask[i % 4]).chr}.join("")
end
return data return data
end end
@@ -237,7 +269,7 @@ Sec-WebSocket-Accept: %s\r
while t[:send_parts].length > 0 while t[:send_parts].length > 0
buf = t[:send_parts].shift buf = t[:send_parts].shift
sent = t[:client].send(buf, 0) sent = t[:client].write(buf)
if sent == buf.length if sent == buf.length
traffic "<" traffic "<"
@@ -257,7 +289,7 @@ Sec-WebSocket-Accept: %s\r
closed = false closed = false
bufs = [] bufs = []
buf = t[:client].recv(@@Buffer_size) buf = t[:client].read_nonblock(@@Buffer_size)
if buf.length == 0 if buf.length == 0
return bufs, "Client closed abrubtly" return bufs, "Client closed abrubtly"
@@ -286,10 +318,10 @@ Sec-WebSocket-Accept: %s\r
end end
end end
else else
if buf[0...2] == "\xff\x00": if buf[0...2] == "\xff\x00"
closed = "Client sent orderly close frame" closed = "Client sent orderly close frame"
break break
elsif buf[0...2] == "\x00\xff": elsif buf[0...2] == "\x00\xff"
buf = buf[2...buf.length] buf = buf[2...buf.length]
continue # No-op frame continue # No-op frame
elsif buf.count("\xff") == 0 elsif buf.count("\xff") == 0
@@ -308,7 +340,7 @@ Sec-WebSocket-Accept: %s\r
bufs << frame['payload'] bufs << frame['payload']
if frame['left'] > 0: if frame['left'] > 0
buf = buf[-frame['left']...buf.length] buf = buf[-frame['left']...buf.length]
else else
buf = '' buf = ''
@@ -328,10 +360,10 @@ Sec-WebSocket-Accept: %s\r
end end
buf, lenh, lent = encode_hybi(msg, opcode=0x08, base64=false) buf, lenh, lent = encode_hybi(msg, opcode=0x08, base64=false)
t[:client].send(buf, 0) t[:client].write(buf)
elsif t[:version] == "hixie-76" elsif t[:version] == "hixie-76"
buf = "\xff\x00" buf = "\xff\x00"
t[:client].send(buf, 0) t[:client].write(buf)
end end
end end
@@ -344,16 +376,21 @@ Sec-WebSocket-Accept: %s\r
raise EClose, "ignoring socket not ready" raise EClose, "ignoring socket not ready"
end end
handshake = sock.recv(1024, Socket::MSG_PEEK) handshake = ""
#msg "Handshake [#{handshake.inspect}]" msg "About to read from sock [#{sock.inspect}]"
handshake = sock.read_nonblock(1024)
msg "Handshake [#{handshake.inspect}]"
if handshake == "" if handshake == nil or handshake == ""
raise(EClose, "ignoring empty handshake") raise(EClose, "ignoring empty handshake")
else else
stype = "Plain non-SSL (ws://)" stype = "Plain non-SSL (ws://)"
scheme = "ws" scheme = "ws"
if sock.class == OpenSSL::SSL::SSLSocket
stype = "SSL (wss://)"
scheme = "wss"
end
retsock = sock retsock = sock
sock.recv(1024)
end end
h = t[:headers] = {} h = t[:headers] = {}
@@ -365,7 +402,7 @@ Sec-WebSocket-Accept: %s\r
hsplit = hline.match(/^([^:]+):\s*(.+)$/) hsplit = hline.match(/^([^:]+):\s*(.+)$/)
h[hsplit[1].strip.downcase] = hsplit[2] h[hsplit[1].strip.downcase] = hsplit[2]
end end
#puts "Headers: #{h.inspect}" puts "Headers: #{h.inspect}"
unless h.has_key?('upgrade') && unless h.has_key?('upgrade') &&
h['upgrade'].downcase == 'websocket' h['upgrade'].downcase == 'websocket'
@@ -445,7 +482,7 @@ Sec-WebSocket-Accept: %s\r
if t[:path] then msg "Path: '%s'" % [t[:path]] end if t[:path] then msg "Path: '%s'" % [t[:path]] end
#puts "sending reponse #{response.inspect}" #puts "sending reponse #{response.inspect}"
retsock.send(response, 0) retsock.write(response)
# Return the WebSocket socket which may be SSL wrapped # Return the WebSocket socket which may be SSL wrapped
return retsock return retsock
+3 -3
View File
@@ -39,7 +39,7 @@ Traffic Legend:
end end
# Echo back whatever is received # Echo back whatever is received
def new_client(client) def new_websocket_client(client)
msg "connecting to: %s:%s" % [@target_host, @target_port] msg "connecting to: %s:%s" % [@target_host, @target_port]
tsock = TCPSocket.open(@target_host, @target_port) tsock = TCPSocket.open(@target_host, @target_port)
@@ -92,7 +92,7 @@ Traffic Legend:
# Receive target data and queue for the client # Receive target data and queue for the client
if ins && ins.include?(target) if ins && ins.include?(target)
buf = target.recv(@@Buffer_size) buf = target.recv(@@Buffer_size)
if buf.length == 0: if buf.length == 0
raise EClose, "Target closed" raise EClose, "Target closed"
end end
@@ -128,7 +128,7 @@ parser = OptionParser.new do |o|
o.parse! o.parse!
end end
if ARGV.length < 2: if ARGV.length < 2
puts "Too few arguments" puts "Too few arguments"
exit 2 exit 2
end end
+2 -2
View File
@@ -7,8 +7,8 @@
* REBIND_PORT_NEW environment variables are set then bind on the new * REBIND_PORT_NEW environment variables are set then bind on the new
* port (of localhost) instead of the old port. * port (of localhost) instead of the old port.
* *
* This allows a proxy (such as wsproxy) to run on the old port and translate * This allows a bridge/proxy (such as websockify) to run on the old port and
* traffic to/from the new port. * translate traffic to/from the new port.
* *
* Usage: * Usage:
* LD_PRELOAD=./rebind.so \ * LD_PRELOAD=./rebind.so \
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python #!/usr/bin/env python
import websockify import websockify
+15 -1
View File
@@ -1,6 +1,6 @@
from setuptools import setup, find_packages from setuptools import setup, find_packages
version = '0.4.0' version = '0.7.0'
name = 'websockify' name = 'websockify'
long_description = open("README.md").read() + "\n" + \ long_description = open("README.md").read() + "\n" + \
open("CHANGES.txt").read() + "\n" open("CHANGES.txt").read() + "\n"
@@ -11,7 +11,21 @@ setup(name=name,
long_description=long_description, long_description=long_description,
classifiers=[ classifiers=[
"Programming Language :: Python", "Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.6",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4"
], ],
data_files=[('share/websockify/include',
['include/util.js',
'include/base64.js',
'include/websock.js']),
('share/websockify/include/web-socket-js',
['include/web-socket-js/WebSocketMain.swf',
'include/web-socket-js/swfobject.js',
'include/web-socket-js/web_socket.js'])],
keywords='noVNC websockify', keywords='noVNC websockify',
license='LGPLv3', license='LGPLv3',
url="https://github.com/kanaka/websockify", url="https://github.com/kanaka/websockify",
+2
View File
@@ -0,0 +1,2 @@
mox
nose
+12 -11
View File
@@ -10,17 +10,17 @@ openssl req -new -x509 -days 365 -nodes -out self.pem -keyout self.pem
as taken from http://docs.python.org/dev/library/ssl.html#certificates as taken from http://docs.python.org/dev/library/ssl.html#certificates
''' '''
import os, sys, select, optparse import os, sys, select, optparse, logging
sys.path.insert(0,os.path.dirname(__file__) + "/../") sys.path.insert(0,os.path.join(os.path.dirname(__file__), ".."))
from websocket import WebSocketServer from websockify.websocket import WebSocketServer, WebSocketRequestHandler
class WebSocketEcho(WebSocketServer): class WebSocketEcho(WebSocketRequestHandler):
""" """
WebSockets server that echos back whatever is received from the WebSockets server that echos back whatever is received from the
client. """ client. """
buffer_size = 8096 buffer_size = 8096
def new_client(self): def new_websocket_client(self):
""" """
Echo back whatever is received. Echo back whatever is received.
""" """
@@ -28,28 +28,27 @@ class WebSocketEcho(WebSocketServer):
cqueue = [] cqueue = []
c_pend = 0 c_pend = 0
cpartial = "" cpartial = ""
rlist = [self.client] rlist = [self.request]
while True: while True:
wlist = [] wlist = []
if cqueue or c_pend: wlist.append(self.client) if cqueue or c_pend: wlist.append(self.request)
ins, outs, excepts = select.select(rlist, wlist, [], 1) ins, outs, excepts = select.select(rlist, wlist, [], 1)
if excepts: raise Exception("Socket exception") if excepts: raise Exception("Socket exception")
if self.client in outs: if self.request in outs:
# Send queued target data to the client # Send queued target data to the client
c_pend = self.send_frames(cqueue) c_pend = self.send_frames(cqueue)
cqueue = [] cqueue = []
if self.client in ins: if self.request in ins:
# Receive client data, decode it, and send it back # Receive client data, decode it, and send it back
frames, closed = self.recv_frames() frames, closed = self.recv_frames()
cqueue.extend(frames) cqueue.extend(frames)
if closed: if closed:
self.send_close() self.send_close()
raise self.EClose(closed)
if __name__ == '__main__': if __name__ == '__main__':
parser = optparse.OptionParser(usage="%prog [options] listen_port") parser = optparse.OptionParser(usage="%prog [options] listen_port")
@@ -69,7 +68,9 @@ if __name__ == '__main__':
except: except:
parser.error("Invalid arguments") parser.error("Invalid arguments")
logging.basicConfig(level=logging.INFO)
opts.web = "." opts.web = "."
server = WebSocketEcho(**opts.__dict__) server = WebSocketServer(WebSocketEcho, **opts.__dict__)
server.start_server() server.start_server()
+10 -3
View File
@@ -12,7 +12,7 @@ require 'websocket'
class WebSocketEcho < WebSocketServer class WebSocketEcho < WebSocketServer
# Echo back whatever is received # Echo back whatever is received
def new_client(client) def new_websocket_client(client)
cqueue = [] cqueue = []
c_pend = 0 c_pend = 0
@@ -50,10 +50,17 @@ class WebSocketEcho < WebSocketServer
end end
end end
port = ARGV[0].to_i port = ARGV[0].to_i || 8080
puts "Starting server on port #{port}" puts "Starting server on port #{port}"
server_cert = nil
server_key = nil
if ARGV.length > 2
server_cert = ARGV[1]
server_key = ARGV[2]
end
server = WebSocketEcho.new('listen_port' => port, 'verbose' => true) server = WebSocketEcho.new('listen_port' => port, 'verbose' => true,
'server_cert' => server_cert, 'server_key' => server_key)
server.start server.start
server.join server.join
+18 -15
View File
@@ -6,35 +6,37 @@ that has a random payload (length and content) that is checksummed and
given a sequence number. Any errors are reported and counted. given a sequence number. Any errors are reported and counted.
''' '''
import sys, os, select, random, time, optparse import sys, os, select, random, time, optparse, logging
sys.path.insert(0,os.path.dirname(__file__) + "/../") sys.path.insert(0,os.path.join(os.path.dirname(__file__), ".."))
from websocket import WebSocketServer from websockify.websocket import WebSocketServer, WebSocketRequestHandler
class WebSocketLoad(WebSocketServer): class WebSocketLoadServer(WebSocketServer):
buffer_size = 65536
max_packet_size = 10000
recv_cnt = 0 recv_cnt = 0
send_cnt = 0 send_cnt = 0
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.errors = 0
self.delay = kwargs.pop('delay') self.delay = kwargs.pop('delay')
WebSocketServer.__init__(self, *args, **kwargs)
class WebSocketLoad(WebSocketRequestHandler):
max_packet_size = 10000
def new_websocket_client(self):
print "Prepopulating random array" print "Prepopulating random array"
self.rand_array = [] self.rand_array = []
for i in range(0, self.max_packet_size): for i in range(0, self.max_packet_size):
self.rand_array.append(random.randint(0, 9)) self.rand_array.append(random.randint(0, 9))
WebSocketServer.__init__(self, *args, **kwargs) self.errors = 0
def new_client(self):
self.send_cnt = 0 self.send_cnt = 0
self.recv_cnt = 0 self.recv_cnt = 0
try: try:
self.responder(self.client) self.responder(self.request)
except: except:
print "accumulated errors:", self.errors print "accumulated errors:", self.errors
self.errors = 0 self.errors = 0
@@ -61,14 +63,13 @@ class WebSocketLoad(WebSocketServer):
if closed: if closed:
self.send_close() self.send_close()
raise self.EClose(closed)
now = time.time() * 1000 now = time.time() * 1000
if client in outs: if client in outs:
if c_pend: if c_pend:
last_send = now last_send = now
c_pend = self.send_frames() c_pend = self.send_frames()
elif now > (last_send + self.delay): elif now > (last_send + self.server.delay):
last_send = now last_send = now
c_pend = self.send_frames([self.generate()]) c_pend = self.send_frames([self.generate()])
@@ -161,7 +162,9 @@ if __name__ == '__main__':
except: except:
parser.error("Invalid arguments") parser.error("Invalid arguments")
logging.basicConfig(level=logging.INFO)
opts.web = "." opts.web = "."
server = WebSocketLoad(**opts.__dict__) server = WebSocketLoadServer(WebSocketLoad, **opts.__dict__)
server.start_server() server.start_server()
+430
View File
@@ -0,0 +1,430 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright(c)2013 NTT corp. All Rights Reserved.
#
# 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.
""" Unit tests for websocket """
import errno
import os
import logging
import select
import shutil
import socket
import ssl
import stubout
import sys
import tempfile
import unittest
import socket
import signal
from websockify import websocket
try:
from SimpleHTTPServer import SimpleHTTPRequestHandler
except ImportError:
from http.server import SimpleHTTPRequestHandler
try:
from StringIO import StringIO
BytesIO = StringIO
except ImportError:
from io import StringIO
from io import BytesIO
def raise_oserror(*args, **kwargs):
raise OSError('fake error')
class FakeSocket(object):
def __init__(self, data=''):
if isinstance(data, bytes):
self._data = data
else:
self._data = data.encode('latin_1')
def recv(self, amt, flags=None):
res = self._data[0:amt]
if not (flags & socket.MSG_PEEK):
self._data = self._data[amt:]
return res
def makefile(self, mode='r', buffsize=None):
if 'b' in mode:
return BytesIO(self._data)
else:
return StringIO(self._data.decode('latin_1'))
class WebSocketRequestHandlerTestCase(unittest.TestCase):
def setUp(self):
super(WebSocketRequestHandlerTestCase, self).setUp()
self.stubs = stubout.StubOutForTesting()
self.tmpdir = tempfile.mkdtemp('-websockify-tests')
# Mock this out cause it screws tests up
self.stubs.Set(os, 'chdir', lambda *args, **kwargs: None)
self.stubs.Set(SimpleHTTPRequestHandler, 'send_response',
lambda *args, **kwargs: None)
def tearDown(self):
"""Called automatically after each test."""
self.stubs.UnsetAll()
os.rmdir(self.tmpdir)
super(WebSocketRequestHandlerTestCase, self).tearDown()
def _get_server(self, handler_class=websocket.WebSocketRequestHandler,
**kwargs):
web = kwargs.pop('web', self.tmpdir)
return websocket.WebSocketServer(
handler_class, listen_host='localhost',
listen_port=80, key=self.tmpdir, web=web,
record=self.tmpdir, daemon=False, ssl_only=0, idle_timeout=1,
**kwargs)
def test_normal_get_with_only_upgrade_returns_error(self):
server = self._get_server(web=None)
handler = websocket.WebSocketRequestHandler(
FakeSocket('GET /tmp.txt HTTP/1.1'), '127.0.0.1', server)
def fake_send_response(self, code, message=None):
self.last_code = code
self.stubs.Set(SimpleHTTPRequestHandler, 'send_response',
fake_send_response)
handler.do_GET()
self.assertEqual(handler.last_code, 405)
def test_list_dir_with_file_only_returns_error(self):
server = self._get_server(file_only=True)
handler = websocket.WebSocketRequestHandler(
FakeSocket('GET / HTTP/1.1'), '127.0.0.1', server)
def fake_send_response(self, code, message=None):
self.last_code = code
self.stubs.Set(SimpleHTTPRequestHandler, 'send_response',
fake_send_response)
handler.path = '/'
handler.do_GET()
self.assertEqual(handler.last_code, 404)
class WebSocketServerTestCase(unittest.TestCase):
def setUp(self):
super(WebSocketServerTestCase, self).setUp()
self.stubs = stubout.StubOutForTesting()
self.tmpdir = tempfile.mkdtemp('-websockify-tests')
# Mock this out cause it screws tests up
self.stubs.Set(os, 'chdir', lambda *args, **kwargs: None)
def tearDown(self):
"""Called automatically after each test."""
self.stubs.UnsetAll()
os.rmdir(self.tmpdir)
super(WebSocketServerTestCase, self).tearDown()
def _get_server(self, handler_class=websocket.WebSocketRequestHandler,
**kwargs):
return websocket.WebSocketServer(
handler_class, listen_host='localhost',
listen_port=80, key=self.tmpdir, web=self.tmpdir,
record=self.tmpdir, **kwargs)
def test_daemonize_raises_error_while_closing_fds(self):
server = self._get_server(daemon=True, ssl_only=1, idle_timeout=1)
self.stubs.Set(os, 'fork', lambda *args: 0)
self.stubs.Set(signal, 'signal', lambda *args: None)
self.stubs.Set(os, 'setsid', lambda *args: None)
self.stubs.Set(os, 'close', raise_oserror)
self.assertRaises(OSError, server.daemonize, keepfd=None, chdir='./')
def test_daemonize_ignores_ebadf_error_while_closing_fds(self):
def raise_oserror_ebadf(fd):
raise OSError(errno.EBADF, 'fake error')
server = self._get_server(daemon=True, ssl_only=1, idle_timeout=1)
self.stubs.Set(os, 'fork', lambda *args: 0)
self.stubs.Set(os, 'setsid', lambda *args: None)
self.stubs.Set(signal, 'signal', lambda *args: None)
self.stubs.Set(os, 'close', raise_oserror_ebadf)
self.stubs.Set(os, 'open', raise_oserror)
self.assertRaises(OSError, server.daemonize, keepfd=None, chdir='./')
def test_handshake_fails_on_not_ready(self):
server = self._get_server(daemon=True, ssl_only=0, idle_timeout=1)
def fake_select(rlist, wlist, xlist, timeout=None):
return ([], [], [])
self.stubs.Set(select, 'select', fake_select)
self.assertRaises(
websocket.WebSocketServer.EClose, server.do_handshake,
FakeSocket(), '127.0.0.1')
def test_empty_handshake_fails(self):
server = self._get_server(daemon=True, ssl_only=0, idle_timeout=1)
sock = FakeSocket('')
def fake_select(rlist, wlist, xlist, timeout=None):
return ([sock], [], [])
self.stubs.Set(select, 'select', fake_select)
self.assertRaises(
websocket.WebSocketServer.EClose, server.do_handshake,
sock, '127.0.0.1')
def test_handshake_policy_request(self):
# TODO(directxman12): implement
pass
def test_handshake_ssl_only_without_ssl_raises_error(self):
server = self._get_server(daemon=True, ssl_only=1, idle_timeout=1)
sock = FakeSocket('some initial data')
def fake_select(rlist, wlist, xlist, timeout=None):
return ([sock], [], [])
self.stubs.Set(select, 'select', fake_select)
self.assertRaises(
websocket.WebSocketServer.EClose, server.do_handshake,
sock, '127.0.0.1')
def test_do_handshake_no_ssl(self):
class FakeHandler(object):
CALLED = False
def __init__(self, *args, **kwargs):
type(self).CALLED = True
FakeHandler.CALLED = False
server = self._get_server(
handler_class=FakeHandler, daemon=True,
ssl_only=0, idle_timeout=1)
sock = FakeSocket('some initial data')
def fake_select(rlist, wlist, xlist, timeout=None):
return ([sock], [], [])
self.stubs.Set(select, 'select', fake_select)
self.assertEqual(server.do_handshake(sock, '127.0.0.1'), sock)
self.assertTrue(FakeHandler.CALLED, True)
def test_do_handshake_ssl(self):
# TODO(directxman12): implement this
pass
def test_do_handshake_ssl_without_ssl_raises_error(self):
# TODO(directxman12): implement this
pass
def test_do_handshake_ssl_without_cert_raises_error(self):
server = self._get_server(daemon=True, ssl_only=0, idle_timeout=1,
cert='afdsfasdafdsafdsafdsafdas')
sock = FakeSocket("\x16some ssl data")
def fake_select(rlist, wlist, xlist, timeout=None):
return ([sock], [], [])
self.stubs.Set(select, 'select', fake_select)
self.assertRaises(
websocket.WebSocketServer.EClose, server.do_handshake,
sock, '127.0.0.1')
def test_do_handshake_ssl_error_eof_raises_close_error(self):
server = self._get_server(daemon=True, ssl_only=0, idle_timeout=1)
sock = FakeSocket("\x16some ssl data")
def fake_select(rlist, wlist, xlist, timeout=None):
return ([sock], [], [])
def fake_wrap_socket(*args, **kwargs):
raise ssl.SSLError(ssl.SSL_ERROR_EOF)
self.stubs.Set(select, 'select', fake_select)
self.stubs.Set(ssl, 'wrap_socket', fake_wrap_socket)
self.assertRaises(
websocket.WebSocketServer.EClose, server.do_handshake,
sock, '127.0.0.1')
def test_fallback_sigchld_handler(self):
# TODO(directxman12): implement this
pass
def test_start_server_error(self):
server = self._get_server(daemon=False, ssl_only=1, idle_timeout=1)
sock = server.socket('localhost')
def fake_select(rlist, wlist, xlist, timeout=None):
raise Exception("fake error")
self.stubs.Set(websocket.WebSocketServer, 'socket',
lambda *args, **kwargs: sock)
self.stubs.Set(websocket.WebSocketServer, 'daemonize',
lambda *args, **kwargs: None)
self.stubs.Set(select, 'select', fake_select)
server.start_server()
def test_start_server_keyboardinterrupt(self):
server = self._get_server(daemon=False, ssl_only=0, idle_timeout=1)
sock = server.socket('localhost')
def fake_select(rlist, wlist, xlist, timeout=None):
raise KeyboardInterrupt
self.stubs.Set(websocket.WebSocketServer, 'socket',
lambda *args, **kwargs: sock)
self.stubs.Set(websocket.WebSocketServer, 'daemonize',
lambda *args, **kwargs: None)
self.stubs.Set(select, 'select', fake_select)
server.start_server()
def test_start_server_systemexit(self):
server = self._get_server(daemon=False, ssl_only=0, idle_timeout=1)
sock = server.socket('localhost')
def fake_select(rlist, wlist, xlist, timeout=None):
sys.exit()
self.stubs.Set(websocket.WebSocketServer, 'socket',
lambda *args, **kwargs: sock)
self.stubs.Set(websocket.WebSocketServer, 'daemonize',
lambda *args, **kwargs: None)
self.stubs.Set(select, 'select', fake_select)
server.start_server()
def test_socket_set_keepalive_options(self):
keepcnt = 12
keepidle = 34
keepintvl = 56
server = self._get_server(daemon=False, ssl_only=0, idle_timeout=1)
sock = server.socket('localhost',
tcp_keepcnt=keepcnt,
tcp_keepidle=keepidle,
tcp_keepintvl=keepintvl)
self.assertEqual(sock.getsockopt(socket.SOL_TCP,
socket.TCP_KEEPCNT), keepcnt)
self.assertEqual(sock.getsockopt(socket.SOL_TCP,
socket.TCP_KEEPIDLE), keepidle)
self.assertEqual(sock.getsockopt(socket.SOL_TCP,
socket.TCP_KEEPINTVL), keepintvl)
sock = server.socket('localhost',
tcp_keepalive=False,
tcp_keepcnt=keepcnt,
tcp_keepidle=keepidle,
tcp_keepintvl=keepintvl)
self.assertNotEqual(sock.getsockopt(socket.SOL_TCP,
socket.TCP_KEEPCNT), keepcnt)
self.assertNotEqual(sock.getsockopt(socket.SOL_TCP,
socket.TCP_KEEPIDLE), keepidle)
self.assertNotEqual(sock.getsockopt(socket.SOL_TCP,
socket.TCP_KEEPINTVL), keepintvl)
class HyBiEncodeDecodeTestCase(unittest.TestCase):
def test_decode_hybi_text(self):
buf = b'\x81\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58'
res = websocket.WebSocketRequestHandler.decode_hybi(buf)
self.assertEqual(res['fin'], 1)
self.assertEqual(res['opcode'], 0x1)
self.assertEqual(res['masked'], True)
self.assertEqual(res['length'], 5)
self.assertEqual(res['payload'], b'Hello')
self.assertEqual(res['left'], 0)
def test_decode_hybi_binary(self):
buf = b'\x82\x04\x01\x02\x03\x04'
res = websocket.WebSocketRequestHandler.decode_hybi(buf, strict=False)
self.assertEqual(res['fin'], 1)
self.assertEqual(res['opcode'], 0x2)
self.assertEqual(res['length'], 4)
self.assertEqual(res['payload'], b'\x01\x02\x03\x04')
self.assertEqual(res['left'], 0)
def test_decode_hybi_extended_16bit_binary(self):
data = (b'\x01\x02\x03\x04' * 65) # len > 126 -- len == 260
buf = b'\x82\x7e\x01\x04' + data
res = websocket.WebSocketRequestHandler.decode_hybi(buf, strict=False)
self.assertEqual(res['fin'], 1)
self.assertEqual(res['opcode'], 0x2)
self.assertEqual(res['length'], 260)
self.assertEqual(res['payload'], data)
self.assertEqual(res['left'], 0)
def test_decode_hybi_extended_64bit_binary(self):
data = (b'\x01\x02\x03\x04' * 65) # len > 126 -- len == 260
buf = b'\x82\x7f\x00\x00\x00\x00\x00\x00\x01\x04' + data
res = websocket.WebSocketRequestHandler.decode_hybi(buf, strict=False)
self.assertEqual(res['fin'], 1)
self.assertEqual(res['opcode'], 0x2)
self.assertEqual(res['length'], 260)
self.assertEqual(res['payload'], data)
self.assertEqual(res['left'], 0)
def test_decode_hybi_multi(self):
buf1 = b'\x01\x03\x48\x65\x6c'
buf2 = b'\x80\x02\x6c\x6f'
res1 = websocket.WebSocketRequestHandler.decode_hybi(buf1, strict=False)
self.assertEqual(res1['fin'], 0)
self.assertEqual(res1['opcode'], 0x1)
self.assertEqual(res1['length'], 3)
self.assertEqual(res1['payload'], b'Hel')
self.assertEqual(res1['left'], 0)
res2 = websocket.WebSocketRequestHandler.decode_hybi(buf2, strict=False)
self.assertEqual(res2['fin'], 1)
self.assertEqual(res2['opcode'], 0x0)
self.assertEqual(res2['length'], 2)
self.assertEqual(res2['payload'], b'lo')
self.assertEqual(res2['left'], 0)
def test_encode_hybi_basic(self):
res = websocket.WebSocketRequestHandler.encode_hybi(b'Hello', 0x1)
expected = (b'\x81\x05\x48\x65\x6c\x6c\x6f', 2, 0)
self.assertEqual(res, expected)
def test_strict_mode_refuses_unmasked_client_frames(self):
buf = b'\x81\x05\x48\x65\x6c\x6c\x6f'
self.assertRaises(websocket.WebSocketRequestHandler.CClose,
websocket.WebSocketRequestHandler.decode_hybi,
buf)
def test_no_strict_mode_accepts_unmasked_client_frames(self):
buf = b'\x81\x05\x48\x65\x6c\x6c\x6f'
res = websocket.WebSocketRequestHandler.decode_hybi(buf, strict=False)
self.assertEqual(res['fin'], 1)
self.assertEqual(res['opcode'], 0x1)
self.assertEqual(res['masked'], False)
self.assertEqual(res['length'], 5)
self.assertEqual(res['payload'], b'Hello')
+136
View File
@@ -0,0 +1,136 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright(c) 2015 Red Hat, Inc All Rights Reserved.
#
# 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.
""" Unit tests for websocketproxy """
import unittest
import unittest
import socket
import stubout
from websockify import websocket
from websockify import websocketproxy
from websockify import token_plugins
from websockify import auth_plugins
try:
from StringIO import StringIO
BytesIO = StringIO
except ImportError:
from io import StringIO
from io import BytesIO
class FakeSocket(object):
def __init__(self, data=''):
if isinstance(data, bytes):
self._data = data
else:
self._data = data.encode('latin_1')
def recv(self, amt, flags=None):
res = self._data[0:amt]
if not (flags & socket.MSG_PEEK):
self._data = self._data[amt:]
return res
def makefile(self, mode='r', buffsize=None):
if 'b' in mode:
return BytesIO(self._data)
else:
return StringIO(self._data.decode('latin_1'))
class FakeServer(object):
class EClose(Exception):
pass
def __init__(self):
self.token_plugin = None
self.auth_plugin = None
self.wrap_cmd = None
self.ssl_target = None
self.unix_target = None
class ProxyRequestHandlerTestCase(unittest.TestCase):
def setUp(self):
super(ProxyRequestHandlerTestCase, self).setUp()
self.stubs = stubout.StubOutForTesting()
self.handler = websocketproxy.ProxyRequestHandler(
FakeSocket(''), "127.0.0.1", FakeServer())
self.handler.path = "https://localhost:6080/websockify?token=blah"
self.handler.headers = None
self.stubs.Set(websocket.WebSocketServer, 'socket',
staticmethod(lambda *args, **kwargs: None))
def tearDown(self):
self.stubs.UnsetAll()
super(ProxyRequestHandlerTestCase, self).tearDown()
def test_get_target(self):
class TestPlugin(token_plugins.BasePlugin):
def lookup(self, token):
return ("some host", "some port")
host, port = self.handler.get_target(
TestPlugin(None), self.handler.path)
self.assertEqual(host, "some host")
self.assertEqual(port, "some port")
def test_get_target_raises_error_on_unknown_token(self):
class TestPlugin(token_plugins.BasePlugin):
def lookup(self, token):
return None
self.assertRaises(FakeServer.EClose, self.handler.get_target,
TestPlugin(None), "https://localhost:6080/websockify?token=blah")
def test_token_plugin(self):
class TestPlugin(token_plugins.BasePlugin):
def lookup(self, token):
return (self.source + token).split(',')
self.stubs.Set(websocketproxy.ProxyRequestHandler, 'do_proxy',
lambda *args, **kwargs: None)
self.handler.server.token_plugin = TestPlugin("somehost,")
self.handler.new_websocket_client()
self.assertEqual(self.handler.server.target_host, "somehost")
self.assertEqual(self.handler.server.target_port, "blah")
def test_auth_plugin(self):
class TestPlugin(auth_plugins.BasePlugin):
def authenticate(self, headers, target_host, target_port):
if target_host == self.source:
raise auth_plugins.AuthenticationError("some error")
self.stubs.Set(websocketproxy.ProxyRequestHandler, 'do_proxy',
staticmethod(lambda *args, **kwargs: None))
self.handler.server.auth_plugin = TestPlugin("somehost")
self.handler.server.target_host = "somehost"
self.handler.server.target_port = "someport"
self.assertRaises(auth_plugins.AuthenticationError,
self.handler.new_websocket_client)
self.handler.server.target_host = "someotherhost"
self.handler.new_websocket_client()
+4 -7
View File
@@ -5,18 +5,15 @@ Display UTF-8 encoding for 0-255.'''
import sys, os, socket, ssl, time, traceback import sys, os, socket, ssl, time, traceback
from select import select from select import select
sys.path.insert(0,os.path.join(os.path.dirname(__file__), ".."))
sys.path.insert(0,os.path.dirname(__file__) + "/../") from websockify.websocket import WebSocketServer
from websocket import WebSocketServer
if __name__ == '__main__': if __name__ == '__main__':
print "val: hixie | hybi_base64 | hybi_binary" print "val: hybi_base64 | hybi_binary"
for c in range(0, 256): for c in range(0, 256):
hixie = WebSocketServer.encode_hixie(chr(c))
hybi_base64 = WebSocketServer.encode_hybi(chr(c), opcode=1, hybi_base64 = WebSocketServer.encode_hybi(chr(c), opcode=1,
base64=True) base64=True)
hybi_binary = WebSocketServer.encode_hybi(chr(c), opcode=2, hybi_binary = WebSocketServer.encode_hybi(chr(c), opcode=2,
base64=False) base64=False)
print "%d: %s | %s | %s" % (c, repr(hixie), repr(hybi_base64), print "%d: %s | %s" % (c, repr(hybi_base64), repr(hybi_binary))
repr(hybi_binary))
+17
View File
@@ -0,0 +1,17 @@
# Tox (http://tox.testrun.org/) is a tool for running tests
# in multiple virtualenvs. This configuration file will run the
# test suite on all supported python versions. To use it, "pip install tox"
# and then run "tox" from this directory.
[tox]
envlist = py24,py26,py27,py33,py34
[testenv]
commands = nosetests {posargs}
deps = -r{toxinidir}/test-requirements.txt
# At some point we should enable this since tox expects it to exist but
# the code will need pep8ising first.
#[testenv:pep8]
#commands = flake8
#dep = flake8
+2 -2
View File
@@ -1,2 +1,2 @@
from websocket import * from websockify.websocket import *
from websocketproxy import * from websockify.websocketproxy import *
+33
View File
@@ -0,0 +1,33 @@
class BasePlugin(object):
def __init__(self, src=None):
self.source = src
def authenticate(self, headers, target_host, target_port):
pass
class AuthenticationError(Exception):
pass
class InvalidOriginError(AuthenticationError):
def __init__(self, expected, actual):
self.expected_origin = expected
self.actual_origin = actual
super(InvalidOriginError, self).__init__(
"Invalid Origin Header: Expected one of "
"%s, got '%s'" % (expected, actual))
class ExpectOrigin(object):
def __init__(self, src=None):
if src is None:
self.source = []
else:
self.source = src.split()
def authenticate(self, headers, target_host, target_port):
origin = headers.getheader('Origin', None)
if origin is None or origin not in self.source:
raise InvalidOriginError(expected=self.source, actual=origin)
+82
View File
@@ -0,0 +1,82 @@
import os
class BasePlugin(object):
def __init__(self, src):
self.source = src
def lookup(self, token):
return None
class ReadOnlyTokenFile(BasePlugin):
# source is a token file with lines like
# token: host:port
# or a directory of such files
def __init__(self, *args, **kwargs):
super(ReadOnlyTokenFile, self).__init__(*args, **kwargs)
self._targets = None
def _load_targets(self):
if os.path.isdir(self.source):
cfg_files = [os.path.join(self.source, f) for
f in os.listdir(self.source)]
else:
cfg_files = [self.source]
self._targets = {}
for f in cfg_files:
for line in [l.strip() for l in open(f).readlines()]:
if line and not line.startswith('#'):
tok, target = line.split(': ')
self._targets[tok] = target.strip().split(':')
def lookup(self, token):
if self._targets is None:
self._load_targets()
if token in self._targets:
return self._targets[token]
else:
return None
# the above one is probably more efficient, but this one is
# more backwards compatible (although in most cases
# ReadOnlyTokenFile should suffice)
class TokenFile(ReadOnlyTokenFile):
# source is a token file with lines like
# token: host:port
# or a directory of such files
def lookup(self, token):
self._load_targets()
return super(TokenFile, self).lookup(token)
class BaseTokenAPI(BasePlugin):
# source is a url with a '%s' in it where the token
# should go
# we import things on demand so that other plugins
# in this file can be used w/o unecessary dependencies
def process_result(self, resp):
return resp.text.split(':')
def lookup(self, token):
import requests
resp = requests.get(self.source % token)
if resp.ok:
return self.process_result(resp)
else:
return None
class JSONTokenApi(BaseTokenAPI):
# source is a url with a '%s' in it where the token
# should go
def process_result(self, resp):
return (resp.json['host'], resp.json['port'])
+588 -504
View File
File diff suppressed because it is too large Load Diff
+338 -178
View File
@@ -11,24 +11,20 @@ as taken from http://docs.python.org/dev/library/ssl.html#certificates
''' '''
import signal, socket, optparse, time, os, sys, subprocess import signal, socket, optparse, time, os, sys, subprocess, logging, errno
from select import select try: from socketserver import ForkingMixIn
import websocket except: from SocketServer import ForkingMixIn
try: from http.server import HTTPServer
except: from BaseHTTPServer import HTTPServer
import select
from websockify import websocket
try: try:
from urllib.parse import parse_qs, urlparse from urllib.parse import parse_qs, urlparse
except: except:
from cgi import parse_qs from cgi import parse_qs
from urlparse import urlparse from urlparse import urlparse
class WebSocketProxy(websocket.WebSocketServer): class ProxyRequestHandler(websocket.WebSocketRequestHandler):
"""
Proxy traffic to and from a WebSockets client to a normal TCP
socket server target. All traffic to/from the client is base64
encoded/decoded to allow binary data to be sent/received to/from
the target.
"""
buffer_size = 65536
traffic_legend = """ traffic_legend = """
Traffic Legend: Traffic Legend:
@@ -42,7 +38,171 @@ Traffic Legend:
<. - Client send partial <. - Client send partial
""" """
def __init__(self, *args, **kwargs): def new_websocket_client(self):
"""
Called after a new WebSocket connection has been established.
"""
# Checks if we receive a token, and look
# for a valid target for it then
if self.server.token_plugin:
(self.server.target_host, self.server.target_port) = self.get_target(self.server.token_plugin, self.path)
if self.server.auth_plugin:
self.server.auth_plugin.authenticate(
headers=self.headers, target_host=self.server.target_host,
target_port=self.server.target_port)
# Connect to the target
if self.server.wrap_cmd:
msg = "connecting to command: '%s' (port %s)" % (" ".join(self.server.wrap_cmd), self.server.target_port)
elif self.server.unix_target:
msg = "connecting to unix socket: %s" % self.server.unix_target
else:
msg = "connecting to: %s:%s" % (
self.server.target_host, self.server.target_port)
if self.server.ssl_target:
msg += " (using SSL)"
self.log_message(msg)
tsock = websocket.WebSocketServer.socket(self.server.target_host,
self.server.target_port,
connect=True, use_ssl=self.server.ssl_target, unix_socket=self.server.unix_target)
self.print_traffic(self.traffic_legend)
# Start proxying
try:
self.do_proxy(tsock)
except:
if tsock:
tsock.shutdown(socket.SHUT_RDWR)
tsock.close()
if self.verbose:
self.log_message("%s:%s: Closed target",
self.server.target_host, self.server.target_port)
raise
def get_target(self, target_plugin, path):
"""
Parses the path, extracts a token, and looks up a target
for that token using the token plugin. Sets
target_host and target_port if successful
"""
# The files in targets contain the lines
# in the form of token: host:port
# Extract the token parameter from url
args = parse_qs(urlparse(path)[4]) # 4 is the query from url
if not 'token' in args or not len(args['token']):
raise self.server.EClose("Token not present")
token = args['token'][0].rstrip('\n')
result_pair = target_plugin.lookup(token)
if result_pair is not None:
return result_pair
else:
raise self.server.EClose("Token '%s' not found" % token)
def do_proxy(self, target):
"""
Proxy client WebSocket to normal target socket.
"""
cqueue = []
c_pend = 0
tqueue = []
rlist = [self.request, target]
if self.server.heartbeat:
now = time.time()
self.heartbeat = now + self.server.heartbeat
else:
self.heartbeat = None
while True:
wlist = []
if self.heartbeat is not None:
now = time.time()
if now > self.heartbeat:
self.heartbeat = now + self.server.heartbeat
self.send_ping()
if tqueue: wlist.append(target)
if cqueue or c_pend: wlist.append(self.request)
try:
ins, outs, excepts = select.select(rlist, wlist, [], 1)
except (select.error, OSError):
exc = sys.exc_info()[1]
if hasattr(exc, 'errno'):
err = exc.errno
else:
err = exc[0]
if err != errno.EINTR:
raise
else:
continue
if excepts: raise Exception("Socket exception")
if self.request in outs:
# Send queued target data to the client
c_pend = self.send_frames(cqueue)
cqueue = []
if self.request in ins:
# Receive client data, decode it, and queue for target
bufs, closed = self.recv_frames()
tqueue.extend(bufs)
if closed:
# TODO: What about blocking on client socket?
if self.verbose:
self.log_message("%s:%s: Client closed connection",
self.server.target_host, self.server.target_port)
raise self.CClose(closed['code'], closed['reason'])
if target in outs:
# Send queued client data to the target
dat = tqueue.pop(0)
sent = target.send(dat)
if sent == len(dat):
self.print_traffic(">")
else:
# requeue the remaining data
tqueue.insert(0, dat[sent:])
self.print_traffic(".>")
if target in ins:
# Receive target data, encode it and queue for client
buf = target.recv(self.buffer_size)
if len(buf) == 0:
if self.verbose:
self.log_message("%s:%s: Target closed connection",
self.server.target_host, self.server.target_port)
raise self.CClose(1000, "Target closed")
cqueue.append(buf)
self.print_traffic("{")
class WebSocketProxy(websocket.WebSocketServer):
"""
Proxy traffic to and from a WebSockets client to a normal TCP
socket server target. All traffic to/from the client is base64
encoded/decoded to allow binary data to be sent/received to/from
the target.
"""
buffer_size = 65536
def __init__(self, RequestHandlerClass=ProxyRequestHandler, *args, **kwargs):
# Save off proxy specific options # Save off proxy specific options
self.target_host = kwargs.pop('target_host', None) self.target_host = kwargs.pop('target_host', None)
self.target_port = kwargs.pop('target_port', None) self.target_port = kwargs.pop('target_port', None)
@@ -50,12 +210,19 @@ Traffic Legend:
self.wrap_mode = kwargs.pop('wrap_mode', None) self.wrap_mode = kwargs.pop('wrap_mode', None)
self.unix_target = kwargs.pop('unix_target', None) self.unix_target = kwargs.pop('unix_target', None)
self.ssl_target = kwargs.pop('ssl_target', None) self.ssl_target = kwargs.pop('ssl_target', None)
self.target_cfg = kwargs.pop('target_cfg', None) self.heartbeat = kwargs.pop('heartbeat', None)
self.token_plugin = kwargs.pop('token_plugin', None)
self.auth_plugin = kwargs.pop('auth_plugin', None)
# Last 3 timestamps command was run # Last 3 timestamps command was run
self.wrap_times = [0, 0, 0] self.wrap_times = [0, 0, 0]
if self.wrap_cmd: if self.wrap_cmd:
rebinder_path = ['./', os.path.dirname(sys.argv[0])] wsdir = os.path.dirname(sys.argv[0])
rebinder_path = [os.path.join(wsdir, "..", "lib"),
os.path.join(wsdir, "..", "lib", "websockify"),
wsdir]
self.rebinder = None self.rebinder = None
for rdir in rebinder_path: for rdir in rebinder_path:
@@ -80,13 +247,10 @@ Traffic Legend:
"REBIND_OLD_PORT": str(kwargs['listen_port']), "REBIND_OLD_PORT": str(kwargs['listen_port']),
"REBIND_NEW_PORT": str(self.target_port)}) "REBIND_NEW_PORT": str(self.target_port)})
if self.target_cfg: websocket.WebSocketServer.__init__(self, RequestHandlerClass, *args, **kwargs)
self.target_cfg = os.path.abspath(self.target_cfg)
websocket.WebSocketServer.__init__(self, *args, **kwargs)
def run_wrap_cmd(self): def run_wrap_cmd(self):
print("Starting '%s'" % " ".join(self.wrap_cmd)) self.msg("Starting '%s'", " ".join(self.wrap_cmd))
self.wrap_times.append(time.time()) self.wrap_times.append(time.time())
self.wrap_times.pop(0) self.wrap_times.pop(0)
self.cmd = subprocess.Popen( self.cmd = subprocess.Popen(
@@ -106,9 +270,9 @@ Traffic Legend:
else: else:
dst_string = "%s:%s" % (self.target_host, self.target_port) dst_string = "%s:%s" % (self.target_host, self.target_port)
if self.target_cfg: if self.token_plugin:
msg = " - proxying from %s:%s to targets in %s" % ( msg = " - proxying from %s:%s to targets generated by %s" % (
self.listen_host, self.listen_port, self.target_cfg) self.listen_host, self.listen_port, type(self.token_plugin).__name__)
else: else:
msg = " - proxying from %s:%s to %s" % ( msg = " - proxying from %s:%s to %s" % (
self.listen_host, self.listen_port, dst_string) self.listen_host, self.listen_port, dst_string)
@@ -116,7 +280,7 @@ Traffic Legend:
if self.ssl_target: if self.ssl_target:
msg += " (using SSL)" msg += " (using SSL)"
print(msg + "\n") self.msg("%s", msg)
if self.wrap_cmd: if self.wrap_cmd:
self.run_wrap_cmd() self.run_wrap_cmd()
@@ -142,158 +306,11 @@ Traffic Legend:
if (now - avg) < 10: if (now - avg) < 10:
# 3 times in the last 10 seconds # 3 times in the last 10 seconds
if self.spawn_message: if self.spawn_message:
print("Command respawning too fast") self.warn("Command respawning too fast")
self.spawn_message = False self.spawn_message = False
else: else:
self.run_wrap_cmd() self.run_wrap_cmd()
#
# Routines above this point are run in the master listener
# process.
#
#
# Routines below this point are connection handler routines and
# will be run in a separate forked process for each connection.
#
def new_client(self):
"""
Called after a new WebSocket connection has been established.
"""
# Checks if we receive a token, and look
# for a valid target for it then
if self.target_cfg:
(self.target_host, self.target_port) = self.get_target(self.target_cfg, self.path)
# Connect to the target
if self.wrap_cmd:
msg = "connecting to command: '%s' (port %s)" % (" ".join(self.wrap_cmd), self.target_port)
elif self.unix_target:
msg = "connecting to unix socket: %s" % self.unix_target
else:
msg = "connecting to: %s:%s" % (
self.target_host, self.target_port)
if self.ssl_target:
msg += " (using SSL)"
self.msg(msg)
tsock = self.socket(self.target_host, self.target_port,
connect=True, use_ssl=self.ssl_target, unix_socket=self.unix_target)
if self.verbose and not self.daemon:
print(self.traffic_legend)
# Start proxying
try:
self.do_proxy(tsock)
except:
if tsock:
tsock.shutdown(socket.SHUT_RDWR)
tsock.close()
self.vmsg("%s:%s: Closed target" %(
self.target_host, self.target_port))
raise
def get_target(self, target_cfg, path):
"""
Parses the path, extracts a token, and looks for a valid
target for that token in the configuration file(s). Sets
target_host and target_port if successful
"""
# The files in targets contain the lines
# in the form of token: host:port
# Extract the token parameter from url
args = parse_qs(urlparse(path)[4]) # 4 is the query from url
if not args.has_key('token') or not len(args['token']):
raise self.EClose("Token not present")
token = args['token'][0].rstrip('\n')
# target_cfg can be a single config file or directory of
# config files
if os.path.isdir(target_cfg):
cfg_files = [os.path.join(target_cfg, f)
for f in os.listdir(target_cfg)]
else:
cfg_files = [target_cfg]
targets = {}
for f in cfg_files:
for line in [l.strip() for l in file(f).readlines()]:
if line and not line.startswith('#'):
ttoken, target = line.split(': ')
targets[ttoken] = target.strip()
self.vmsg("Target config: %s" % repr(targets))
if targets.has_key(token):
return targets[token].split(':')
else:
raise self.EClose("Token '%s' not found" % token)
def do_proxy(self, target):
"""
Proxy client WebSocket to normal target socket.
"""
cqueue = []
c_pend = 0
tqueue = []
rlist = [self.client, target]
while True:
wlist = []
if tqueue: wlist.append(target)
if cqueue or c_pend: wlist.append(self.client)
ins, outs, excepts = select(rlist, wlist, [], 1)
if excepts: raise Exception("Socket exception")
if self.client in outs:
# Send queued target data to the client
c_pend = self.send_frames(cqueue)
cqueue = []
if self.client in ins:
# Receive client data, decode it, and queue for target
bufs, closed = self.recv_frames()
tqueue.extend(bufs)
if closed:
# TODO: What about blocking on client socket?
self.vmsg("%s:%s: Client closed connection" %(
self.target_host, self.target_port))
raise self.CClose(closed['code'], closed['reason'])
if target in outs:
# Send queued client data to the target
dat = tqueue.pop(0)
sent = target.send(dat)
if sent == len(dat):
self.traffic(">")
else:
# requeue the remaining data
tqueue.insert(0, dat[sent:])
self.traffic(".>")
if target in ins:
# Receive target data, encode it and queue for client
buf = target.recv(self.buffer_size)
if len(buf) == 0:
self.vmsg("%s:%s: Target closed connection" %(
self.target_host, self.target_port))
raise self.CClose(1000, "Target closed")
cqueue.append(buf)
self.traffic("{")
def _subprocess_setup(): def _subprocess_setup():
# Python installs a SIGPIPE handler by default. This is usually not what # Python installs a SIGPIPE handler by default. This is usually not what
@@ -301,14 +318,28 @@ def _subprocess_setup():
signal.signal(signal.SIGPIPE, signal.SIG_DFL) signal.signal(signal.SIGPIPE, signal.SIG_DFL)
def logger_init():
logger = logging.getLogger(WebSocketProxy.log_prefix)
logger.propagate = False
logger.setLevel(logging.INFO)
h = logging.StreamHandler()
h.setLevel(logging.DEBUG)
h.setFormatter(logging.Formatter("%(message)s"))
logger.addHandler(h)
def websockify_init(): def websockify_init():
logger_init()
usage = "\n %prog [options]" usage = "\n %prog [options]"
usage += " [source_addr:]source_port [target_addr:target_port]" usage += " [source_addr:]source_port [target_addr:target_port]"
usage += "\n %prog [options]" usage += "\n %prog [options]"
usage += " [source_addr:]source_port -- WRAP_COMMAND_LINE" usage += " [source_addr:]source_port -- WRAP_COMMAND_LINE"
parser = optparse.OptionParser(usage=usage) parser = optparse.OptionParser(usage=usage)
parser.add_option("--verbose", "-v", action="store_true", parser.add_option("--verbose", "-v", action="store_true",
help="verbose messages and per frame traffic") help="verbose messages")
parser.add_option("--traffic", action="store_true",
help="per frame traffic")
parser.add_option("--record", parser.add_option("--record",
help="record sessions to FILE.[session_number]", metavar="FILE") help="record sessions to FILE.[session_number]", metavar="FILE")
parser.add_option("--daemon", "-D", parser.add_option("--daemon", "-D",
@@ -340,15 +371,56 @@ def websockify_init():
parser.add_option("--prefer-ipv6", "-6", parser.add_option("--prefer-ipv6", "-6",
action="store_true", dest="source_is_ipv6", action="store_true", dest="source_is_ipv6",
help="prefer IPv6 when resolving source_addr") help="prefer IPv6 when resolving source_addr")
parser.add_option("--libserver", action="store_true",
help="use Python library SocketServer engine")
parser.add_option("--target-config", metavar="FILE", parser.add_option("--target-config", metavar="FILE",
dest="target_cfg", dest="target_cfg",
help="Configuration file containing valid targets " help="Configuration file containing valid targets "
"in the form 'token: host:port' or, alternatively, a " "in the form 'token: host:port' or, alternatively, a "
"directory containing configuration files of this form") "directory containing configuration files of this form "
"(DEPRECATED: use `--token-plugin TokenFile --token-source "
" path/to/token/file` instead)")
parser.add_option("--token-plugin", default=None, metavar="PLUGIN",
help="use the given Python class to process tokens "
"into host:port pairs")
parser.add_option("--token-source", default=None, metavar="ARG",
help="an argument to be passed to the token plugin"
"on instantiation")
parser.add_option("--auth-plugin", default=None, metavar="PLUGIN",
help="use the given Python class to determine if "
"a connection is allowed")
parser.add_option("--auth-source", default=None, metavar="ARG",
help="an argument to be passed to the auth plugin"
"on instantiation")
parser.add_option("--auto-pong", action="store_true",
help="Automatically respond to ping frames with a pong")
parser.add_option("--heartbeat", type=int, default=0,
help="send a ping to the client every HEARTBEAT seconds")
(opts, args) = parser.parse_args() (opts, args) = parser.parse_args()
if opts.verbose:
logging.getLogger(WebSocketProxy.log_prefix).setLevel(logging.DEBUG)
if opts.token_source and not opts.token_plugin:
parser.error("You must use --token-plugin to use --token-source")
if opts.auth_source and not opts.auth_plugin:
parser.error("You must use --auth-plugin to use --auth-source")
# Transform to absolute path as daemon may chdir
if opts.target_cfg:
opts.target_cfg = os.path.abspath(opts.target_cfg)
if opts.target_cfg:
opts.token_plugin = 'TokenFile'
opts.token_source = opts.target_cfg
del opts.target_cfg
# Sanity checks # Sanity checks
if len(args) < 2 and not (opts.target_cfg or opts.unix_target): if len(args) < 2 and not (opts.token_plugin or opts.unix_target):
parser.error("Too few arguments") parser.error("Too few arguments")
if sys.argv.count('--'): if sys.argv.count('--'):
opts.wrap_cmd = args[1:] opts.wrap_cmd = args[1:]
@@ -373,7 +445,7 @@ def websockify_init():
try: opts.listen_port = int(opts.listen_port) try: opts.listen_port = int(opts.listen_port)
except: parser.error("Error parsing listen port") except: parser.error("Error parsing listen port")
if opts.wrap_cmd or opts.unix_target or opts.target_cfg: if opts.wrap_cmd or opts.unix_target or opts.token_plugin:
opts.target_host = None opts.target_host = None
opts.target_port = None opts.target_port = None
else: else:
@@ -385,9 +457,97 @@ def websockify_init():
try: opts.target_port = int(opts.target_port) try: opts.target_port = int(opts.target_port)
except: parser.error("Error parsing target port") except: parser.error("Error parsing target port")
if opts.token_plugin is not None:
if '.' not in opts.token_plugin:
opts.token_plugin = (
'websockify.token_plugins.%s' % opts.token_plugin)
token_plugin_module, token_plugin_cls = opts.token_plugin.rsplit('.', 1)
__import__(token_plugin_module)
token_plugin_cls = getattr(sys.modules[token_plugin_module], token_plugin_cls)
opts.token_plugin = token_plugin_cls(opts.token_source)
del opts.token_source
if opts.auth_plugin is not None:
if '.' not in opts.auth_plugin:
opts.auth_plugin = 'websockify.auth_plugins.%s' % opts.auth_plugin
auth_plugin_module, auth_plugin_cls = opts.auth_plugin.rsplit('.', 1)
__import__(auth_plugin_module)
auth_plugin_cls = getattr(sys.modules[auth_plugin_module], auth_plugin_cls)
opts.auth_plugin = auth_plugin_cls(opts.auth_source)
del opts.auth_source
# Create and start the WebSockets proxy # Create and start the WebSockets proxy
server = WebSocketProxy(**opts.__dict__) libserver = opts.libserver
server.start_server() del opts.libserver
if libserver:
# Use standard Python SocketServer framework
server = LibProxyServer(**opts.__dict__)
server.serve_forever()
else:
# Use internal service framework
server = WebSocketProxy(**opts.__dict__)
server.start_server()
class LibProxyServer(ForkingMixIn, HTTPServer):
"""
Just like WebSocketProxy, but uses standard Python SocketServer
framework.
"""
def __init__(self, RequestHandlerClass=ProxyRequestHandler, **kwargs):
# Save off proxy specific options
self.target_host = kwargs.pop('target_host', None)
self.target_port = kwargs.pop('target_port', None)
self.wrap_cmd = kwargs.pop('wrap_cmd', None)
self.wrap_mode = kwargs.pop('wrap_mode', None)
self.unix_target = kwargs.pop('unix_target', None)
self.ssl_target = kwargs.pop('ssl_target', None)
self.token_plugin = kwargs.pop('token_plugin', None)
self.auth_plugin = kwargs.pop('auth_plugin', None)
self.heartbeat = kwargs.pop('heartbeat', None)
self.token_plugin = None
self.auth_plugin = None
self.daemon = False
# Server configuration
listen_host = kwargs.pop('listen_host', '')
listen_port = kwargs.pop('listen_port', None)
web = kwargs.pop('web', '')
# Configuration affecting base request handler
self.only_upgrade = not web
self.verbose = kwargs.pop('verbose', False)
record = kwargs.pop('record', '')
if record:
self.record = os.path.abspath(record)
self.run_once = kwargs.pop('run_once', False)
self.handler_id = 0
for arg in kwargs.keys():
print("warning: option %s ignored when using --libserver" % arg)
if web:
os.chdir(web)
HTTPServer.__init__(self, (listen_host, listen_port),
RequestHandlerClass)
def process_request(self, request, client_address):
"""Override process_request to implement a counter"""
self.handler_id += 1
ForkingMixIn.process_request(self, request, client_address)
if __name__ == '__main__': if __name__ == '__main__':
websockify_init() websockify_init()