30 Commits

Author SHA1 Message Date
Solly Ross 1f132f9d84 Update to 0.8.0
See CHANGES.txt for more information
2016-02-18 14:11:15 -05:00
Solly Ross 4e6938e9c2 Merge pull request #226 abhishekkekane/bug/138
Make websockify respond to SIGTERM
2016-02-18 13:43:30 -05:00
Abhishek Kekane 68b1587ada Make websockify respect SIGTERM
Child processes were not terminated when the parent websockify
was killed.

This commit makes websockify send a SIGTERM to all active children
when the parent process is terminated.

Fixes #138
2016-02-18 13:42:53 -05:00
Solly Ross 0b906a1c3e Merge pull request #219 from noah8713/master
Remove logging in signal handlers
2016-01-11 16:28:02 -05:00
Aliasgar Ginwala 6f0e06772a Remove additional signal calls in websockify that causes novnc to hang.
Openstack nova novnc-proxy services uses websockify to provide support
for nova vms using novnc proxy. At present, novnc hangs every couple of
weeks. It only resumes post restart of the novnc-proxy which is not
good. Hence, this code in websockify is updated to get rid of additional
signal calls to avoid novnc going in hang state even though process is
running. Basically, we are getting rid of existing msg and vmsg calls in
the websocket.py. This is kind of quick fix but we will need an
additional way of figuring out the logging to make it easy to trace in
case of any further failures in future.

closes bug: https://github.com/kanaka/noVNC/issues/556
2016-01-08 17:46:35 -08:00
Samuel 8a13c48428 Merge pull request #215 from miguelxpn/logfile
Add information about new logging files functionality in README
2015-12-05 13:19:44 +01:00
Miguel Xavier Penha Neto b2a93bf709 Add information about new logging files functionality in README
Fix minor typo in mini-webserver section
2015-12-04 22:41:02 -02:00
Solly Ross 9bc002dcf9 Merge pull request #214 from miguelxpn/logfile
Fix log files when daemonizing
2015-12-03 12:51:56 -08:00
Miguel Xavier Penha Neto dc979f042c Fix log files when daemonizing
Save log file descriptor so it's not closed when calling daemonize()
2015-12-02 15:21:21 -02:00
Solly Ross 72b629838a Merge pull request #205 from miguelxpn/logfile
Add support for log files
2015-12-01 10:25:23 -08:00
Miguel Xavier Penha Neto d947fb6e30 Add support for log files
Override log_message in websocket.py so messages from send_error are properly saved into the log files

Closes #204
2015-12-01 15:25:17 -02:00
Solly Ross 8dacbc974c Bump NPM Version
This commit is prep for releasing the Node version for v0.7.1
on NPM.

Closes #199.
2015-10-12 15:37:03 -04:00
Solly c415521214 Merge pull request #201 from quinox/basicauth
Verify username/password with BasicAuth plugin
2015-10-06 22:39:27 -04:00
Ceesjan Luiten 5bd6554651 Verify username/password with BasicAuth plugin 2015-10-06 12:09:22 +02:00
Joel Martin 50cc65f1e4 Merge pull request #198 from halfgaar/master
Clarified SSL section in README
2015-10-05 12:15:50 -05:00
Wiebe Cazemier 3c04cf8c45 Clarified SSL section in README 2015-10-05 11:27:36 +02:00
Solly 62c3a59192 Merge pull request #197 from miguelxpn/issue196
Add support for IPv6 addresses in tokens
2015-09-30 16:53:56 -04:00
Miguel Xavier Penha Neto e401701311 Added support for IPv6 addresses in tokens
Fixes #196
2015-09-30 16:04:19 -03:00
Solly 714aa34e4e Merge pull request #194 from kanaka/feature/http-auth-plugins
Rework Auth Plugins to Support HTTP Auth
2015-08-28 13:52:58 -04:00
Solly Ross 05cac26257 Merge pull request #192 from grz0/master
Fix bug in JSONTokenApi plugin

Closes #192
2015-08-27 18:09:58 -04:00
grz0 487db5f7c9 Fix bug in JSONTokenApi plugin
`Response#json` is a method, not a property, in the
requests library.

[directxman12: removed unnecessary calls to str]
2015-08-27 18:08:39 -04:00
Arata Notsu f693871c58 Avoid UnboundLocalError for exc
Previously exc was set only when an exception occurs in the inner
try (the one contains call to self.poll), so an error in the other
part causes an UnboundLocalError.

Closes #178
2015-08-27 18:03:38 -04:00
Solly b445296816 Merge pull request #185 from timkurvers/fix/js/drop-base64-dependency
Drop pkrumins/node-base64 dependency
2015-08-27 17:58:24 -04:00
Solly Ross 1e2b5c2256 Rework Auth Plugins to Support HTTP Auth
This commit reworks auth plugins slightly to enable
support for HTTP authentication.  By raising an
AuthenticationError, auth plugins can now return
HTTP responses to the upgrade request (such as 401).

Related to kanaka/noVNC#522
2015-08-25 17:52:20 -04:00
Solly Ross 6c1543c05b Fix auth plugins on Python 3.x
Python 3 does not have a `getheader` method on the header object,
and instead uses just `get`.
2015-08-25 17:50:22 -04:00
Solly b7684e0914 Merge pull request #186 from timkurvers/fix/js/node-modules-ignore
Correct .gitignore path for js node_modules
2015-08-11 13:37:57 -04:00
Solly 12432c9b06 Merge pull request #187 from timkurvers/fix/js/spdx-license-identifier
Correct SPDX license identifier for LGPL-3.0
2015-08-05 15:32:56 -04:00
Tim Kurvers 2051c2419f Correct SPDX license identifier for LGPL-3.0 2015-08-01 16:32:56 +02:00
Tim Kurvers cb6b309dad Ignore node_modules correctly 2015-08-01 16:19:30 +02:00
Tim Kurvers addc2e6b20 Drop pkrumins/node-base64 dependency 2015-08-01 16:10:58 +02:00
11 changed files with 217 additions and 42 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
other/.lein-deps-sum
other/classes
other/lib
other/node_modules
other/js/node_modules
.project
.pydevproject
target.cfg
+11
View File
@@ -1,6 +1,17 @@
Changes
=======
0.8.0
-----
* Make websockify properly terminate children on SIGTERM (#226)
* Remove logging in signal handlers (this can cause Python to hang under certain conditions) (#219)
* Make it easier to log to a file (#205)
* Add support for IPv6 addresses in tokens in the TokenFile token plugins (#197)
* Improve auth plugin framework to enable better support for HTTP auth (#194, #201)
* Fix bug in JSONTokenAPI token plugin (#192)
* Fix a missing variable in the exception handler (#178)
0.7.0
-----
+22 -7
View File
@@ -47,17 +47,29 @@ which is why the negotiation is necessary.
### Encrypted WebSocket connections (wss://)
To encrypt the traffic using the WebSocket 'wss://' URI scheme you
need to generate a certificate for websockify to load. By default websockify
loads a certificate file name `self.pem` but the `--cert=CERT` option can
override the file name. You can generate a self-signed certificate using
openssl. When asked for the common name, use the hostname of the server where
the proxy will be running:
To encrypt the traffic using the WebSocket 'wss://' URI scheme you need to
generate a certificate and key for Websockify to load. By default, Websockify
loads a certificate file name `self.pem` but the `--cert=CERT` and `--key=KEY`
options can override the file name. You can generate a self-signed certificate
using openssl. When asked for the common name, use the hostname of the server
where the proxy will be running:
```
openssl req -new -x509 -days 365 -nodes -out self.pem -keyout self.pem
```
For a self-signed certificate to work, you need to make your client/browser
understand it. You can do this by installing it as accepted certificate, or by
using that same certificate for a HTTPS connection to which you navigate first
and approve. Browsers generally don't give you the "trust certificate?" prompt
by opening a WSS socket with invalid certificate, hence you need to have it
acccept it by either of those two methods.
If you have a commercial/valid SSL certificate with one ore more intermediate
certificates, concat them into one file, server certificate first, then the
intermediate(s) from the CA, etc. Point to this file with the `--cert` option
and then also to the key with `--key`. Finally, use `--ssl-only` as needed.
### Websock Javascript library
@@ -103,11 +115,14 @@ These are not necessary for the basic operation.
* Mini-webserver: websockify can detect and respond to normal web
requests on the same port as the WebSockets proxy and Flash security
policy. This functionality is activate with the `--web DIR` option
policy. This functionality is activated with the `--web DIR` option
where DIR is the root of the web directory to serve.
* Wrap a program: see the "Wrap a Program" section below.
* Log files: websockify can save all logging information in a file.
This functionality is activated with the `--log-file FILE` option
where FILE is the file where the logs should be saved.
### Implementations of websockify
+2 -3
View File
@@ -2,8 +2,8 @@
"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",
"license": "LGPL-3.0",
"version": "0.8.0",
"repository": {
"type": "git",
"url": "git://github.com/kanaka/websockify.git"
@@ -17,7 +17,6 @@
},
"dependencies": {
"ws": ">=0.4.27",
"base64": "latest",
"optimist": "latest",
"policyfile": "latest"
}
+1 -1
View File
@@ -1,6 +1,6 @@
from setuptools import setup, find_packages
version = '0.7.0'
version = '0.8.0'
name = 'websockify'
long_description = open("README.md").read() + "\n" + \
open("CHANGES.txt").read() + "\n"
+28
View File
@@ -0,0 +1,28 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
""" Unit tests for Authentication plugins"""
from websockify.auth_plugins import BasicHTTPAuth, AuthenticationError
import unittest
class BasicHTTPAuthTestCase(unittest.TestCase):
def setUp(self):
self.plugin = BasicHTTPAuth('Aladdin:open sesame')
def test_no_auth(self):
headers = {}
self.assertRaises(AuthenticationError, self.plugin.authenticate, headers, 'localhost', '1234')
def test_invalid_password(self):
headers = {'Authorization': 'Basic QWxhZGRpbjpzZXNhbWUgc3RyZWV0'}
self.assertRaises(AuthenticationError, self.plugin.authenticate, headers, 'localhost', '1234')
def test_valid_password(self):
headers = {'Authorization': 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=='}
self.plugin.authenticate(headers, 'localhost', '1234')
def test_garbage_auth(self):
headers = {'Authorization': 'Basic xxxxxxxxxxxxxxxxxxxxxxxxxxxx'}
self.assertRaises(AuthenticationError, self.plugin.authenticate, headers, 'localhost', '1234')
+7 -7
View File
@@ -106,11 +106,11 @@ class ProxyRequestHandlerTestCase(unittest.TestCase):
def lookup(self, token):
return (self.source + token).split(',')
self.stubs.Set(websocketproxy.ProxyRequestHandler, 'do_proxy',
lambda *args, **kwargs: None)
self.stubs.Set(websocketproxy.ProxyRequestHandler, 'send_auth_error',
staticmethod(lambda *args, **kwargs: None))
self.handler.server.token_plugin = TestPlugin("somehost,")
self.handler.new_websocket_client()
self.handler.validate_connection()
self.assertEqual(self.handler.server.target_host, "somehost")
self.assertEqual(self.handler.server.target_port, "blah")
@@ -119,9 +119,9 @@ class ProxyRequestHandlerTestCase(unittest.TestCase):
class TestPlugin(auth_plugins.BasePlugin):
def authenticate(self, headers, target_host, target_port):
if target_host == self.source:
raise auth_plugins.AuthenticationError("some error")
raise auth_plugins.AuthenticationError(response_msg="some_error")
self.stubs.Set(websocketproxy.ProxyRequestHandler, 'do_proxy',
self.stubs.Set(websocketproxy.ProxyRequestHandler, 'send_auth_error',
staticmethod(lambda *args, **kwargs: None))
self.handler.server.auth_plugin = TestPlugin("somehost")
@@ -129,8 +129,8 @@ class ProxyRequestHandlerTestCase(unittest.TestCase):
self.handler.server.target_port = "someport"
self.assertRaises(auth_plugins.AuthenticationError,
self.handler.new_websocket_client)
self.handler.validate_connection)
self.handler.server.target_host = "someotherhost"
self.handler.new_websocket_client()
self.handler.validate_connection()
+54 -4
View File
@@ -7,7 +7,15 @@ class BasePlugin(object):
class AuthenticationError(Exception):
pass
def __init__(self, log_msg=None, response_code=403, response_headers={}, response_msg=None):
self.code = response_code
self.headers = response_headers
self.msg = response_msg
if log_msg is None:
log_msg = response_msg
super(AuthenticationError, self).__init__('%s %s' % (self.code, log_msg))
class InvalidOriginError(AuthenticationError):
@@ -16,10 +24,52 @@ class InvalidOriginError(AuthenticationError):
self.actual_origin = actual
super(InvalidOriginError, self).__init__(
"Invalid Origin Header: Expected one of "
"%s, got '%s'" % (expected, actual))
response_msg='Invalid Origin',
log_msg="Invalid Origin Header: Expected one of "
"%s, got '%s'" % (expected, actual))
class BasicHTTPAuth(object):
"""Verifies Basic Auth headers. Specify src as username:password"""
def __init__(self, src=None):
self.src = src
def authenticate(self, headers, target_host, target_port):
import base64
auth_header = headers.get('Authorization')
if auth_header:
if not auth_header.startswith('Basic '):
raise AuthenticationError(response_code=403)
try:
user_pass_raw = base64.b64decode(auth_header[6:])
except TypeError:
raise AuthenticationError(response_code=403)
try:
# http://stackoverflow.com/questions/7242316/what-encoding-should-i-use-for-http-basic-authentication
user_pass_as_text = user_pass_raw.decode('ISO-8859-1')
except UnicodeDecodeError:
raise AuthenticationError(response_code=403)
user_pass = user_pass_as_text.split(':', 1)
if len(user_pass) != 2:
raise AuthenticationError(response_code=403)
if not self.validate_creds(*user_pass):
raise AuthenticationError(response_code=403)
else:
raise AuthenticationError(response_code=401,
response_headers={'WWW-Authenticate': 'Basic realm="Websockify"'})
def validate_creds(self, username, password):
if '%s:%s' % (username, password) == self.src:
return True
else:
return False
class ExpectOrigin(object):
def __init__(self, src=None):
if src is None:
@@ -28,6 +78,6 @@ class ExpectOrigin(object):
self.source = src.split()
def authenticate(self, headers, target_host, target_port):
origin = headers.getheader('Origin', None)
origin = headers.get('Origin', None)
if origin is None or origin not in self.source:
raise InvalidOriginError(expected=self.source, actual=origin)
+3 -2
View File
@@ -28,7 +28,7 @@ class ReadOnlyTokenFile(BasePlugin):
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(':')
self._targets[tok] = target.strip().rsplit(':', 1)
def lookup(self, token):
if self._targets is None:
@@ -79,4 +79,5 @@ class JSONTokenApi(BaseTokenAPI):
# should go
def process_result(self, resp):
return (resp.json['host'], resp.json['port'])
resp_json = resp.json()
return (resp_json['host'], resp_json['port'])
+51 -8
View File
@@ -113,6 +113,9 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler):
SimpleHTTPRequestHandler.__init__(self, req, addr, server)
def log_message(self, format, *args):
self.logger.info("%s - - [%s] %s" % (self.address_string(), self.log_date_time_string(), format % args))
@staticmethod
def unmask(buf, hlen, plen):
pstart = hlen + 4
@@ -474,9 +477,13 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler):
"""Upgrade a connection to Websocket, if requested. If this succeeds,
new_websocket_client() will be called. Otherwise, False is returned.
"""
if (self.headers.get('upgrade') and
self.headers.get('upgrade').lower() == 'websocket'):
# ensure connection is authorized, and determine the target
self.validate_connection()
if not self.do_websocket_handshake():
return False
@@ -549,6 +556,10 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler):
""" Do something with a WebSockets client connection. """
raise Exception("WebSocketRequestHandler.new_websocket_client() must be overloaded")
def validate_connection(self):
""" Ensure that the connection is a valid connection, and set the target. """
pass
def do_HEAD(self):
if self.only_upgrade:
self.send_error(405, "Method Not Allowed")
@@ -737,6 +748,10 @@ class WebSocketServer(object):
@staticmethod
def daemonize(keepfd=None, chdir='/'):
if keepfd is None:
keepfd = []
os.umask(0)
if chdir:
os.chdir(chdir)
@@ -759,7 +774,7 @@ class WebSocketServer(object):
if maxfd == resource.RLIM_INFINITY: maxfd = 256
for fd in reversed(range(maxfd)):
try:
if fd != keepfd:
if fd not in keepfd:
os.close(fd)
except OSError:
_, exc, _ = sys.exc_info()
@@ -789,7 +804,7 @@ class WebSocketServer(object):
"""
ready = select.select([sock], [], [], 3)[0]
if not ready:
raise self.EClose("ignoring socket not ready")
# Peek, but do not read the data so that we have a opportunity
@@ -880,11 +895,14 @@ class WebSocketServer(object):
raise self.Terminate()
def multiprocessing_SIGCHLD(self, sig, stack):
self.vmsg('Reaping zombies, active child count is %s', len(multiprocessing.active_children()))
# TODO: figure out a way to actually log this information without
# calling `log` in the signal handlers
multiprocessing.active_children()
def fallback_SIGCHLD(self, sig, stack):
# Reap zombies when using os.fork() (python 2.4)
self.vmsg("Got SIGCHLD, reaping zombies")
# TODO: figure out a way to actually log this information without
# calling `log` in the signal handlers
try:
result = os.waitpid(-1, os.WNOHANG)
while result[0]:
@@ -894,16 +912,18 @@ class WebSocketServer(object):
pass
def do_SIGINT(self, sig, stack):
self.msg("Got SIGINT, exiting")
# TODO: figure out a way to actually log this information without
# calling `log` in the signal handlers
self.terminate()
def do_SIGTERM(self, sig, stack):
self.msg("Got SIGTERM, exiting")
# TODO: figure out a way to actually log this information without
# calling `log` in the signal handlers
self.terminate()
def top_new_client(self, startsock, address):
""" Do something with a WebSockets client connection. """
# handler process
# handler process
client = None
try:
try:
@@ -926,6 +946,18 @@ class WebSocketServer(object):
# Original socket closed by caller
client.close()
def get_log_fd(self):
"""
Get file descriptors for the loggers.
They should not be closed when the process is forked.
"""
descriptors = []
for handler in self.logger.parent.handlers:
if isinstance(handler, logging.FileHandler):
descriptors.append(handler.stream.fileno())
return descriptors
def start_server(self):
"""
Daemonize if requested. Listen for for connections. Run
@@ -941,7 +973,9 @@ class WebSocketServer(object):
tcp_keepintvl=self.tcp_keepintvl)
if self.daemon:
self.daemonize(keepfd=lsock.fileno(), chdir=self.web)
keepfd = self.get_log_fd()
keepfd.append(lsock.fileno())
self.daemonize(keepfd=keepfd, chdir=self.web)
self.started() # Some things need to happen after daemonizing
@@ -1045,8 +1079,17 @@ class WebSocketServer(object):
except (self.Terminate, SystemExit, KeyboardInterrupt):
self.msg("In exit")
# terminate all child processes
if multiprocessing and not self.run_once:
children = multiprocessing.active_children()
for child in children:
self.msg("Terminating child %s" % child.pid)
child.terminate()
break
except Exception:
exc = sys.exc_info()[1]
self.msg("handler exception: %s", str(exc))
self.vmsg("exception", exc_info=True)
+37 -9
View File
@@ -18,6 +18,7 @@ try: from http.server import HTTPServer
except: from BaseHTTPServer import HTTPServer
import select
from websockify import websocket
from websockify import auth_plugins as auth
try:
from urllib.parse import parse_qs, urlparse
except:
@@ -37,20 +38,34 @@ Traffic Legend:
< - Client send
<. - Client send partial
"""
def send_auth_error(self, ex):
self.send_response(ex.code, ex.msg)
self.send_header('Content-Type', 'text/html')
for name, val in ex.headers.items():
self.send_header(name, val)
self.end_headers()
def validate_connection(self):
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:
try:
self.server.auth_plugin.authenticate(
headers=self.headers, target_host=self.server.target_host,
target_port=self.server.target_port)
except auth.AuthenticationError:
ex = sys.exc_info()[1]
self.send_auth_error(ex)
raise
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)
# Checking for a token is done in validate_connection()
# Connect to the target
if self.server.wrap_cmd:
@@ -396,9 +411,22 @@ def websockify_init():
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")
parser.add_option("--log-file", metavar="FILE",
dest="log_file",
help="File where logs will be saved")
(opts, args) = parser.parse_args()
if opts.log_file:
opts.log_file = os.path.abspath(opts.log_file)
handler = logging.FileHandler(opts.log_file)
handler.setLevel(logging.DEBUG)
handler.setFormatter(logging.Formatter("%(message)s"))
logging.getLogger(WebSocketProxy.log_prefix).addHandler(handler)
del opts.log_file
if opts.verbose:
logging.getLogger(WebSocketProxy.log_prefix).setLevel(logging.DEBUG)