Added --web-auth option to require authentication to access the webserver.
BasicHTTPAuth plugin now issues 401 on bad credentials to allow the user to try again.
This commit is contained in:
parent
38b77714a9
commit
8964adf111
|
|
@ -139,8 +139,8 @@ class ProxyRequestHandlerTestCase(unittest.TestCase):
|
||||||
self.handler.server.target_port = "someport"
|
self.handler.server.target_port = "someport"
|
||||||
|
|
||||||
self.assertRaises(auth_plugins.AuthenticationError,
|
self.assertRaises(auth_plugins.AuthenticationError,
|
||||||
self.handler.validate_connection)
|
self.handler.auth_connection)
|
||||||
|
|
||||||
self.handler.server.target_host = "someotherhost"
|
self.handler.server.target_host = "someotherhost"
|
||||||
self.handler.validate_connection()
|
self.handler.auth_connection()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,29 +40,28 @@ class BasicHTTPAuth(object):
|
||||||
auth_header = headers.get('Authorization')
|
auth_header = headers.get('Authorization')
|
||||||
if auth_header:
|
if auth_header:
|
||||||
if not auth_header.startswith('Basic '):
|
if not auth_header.startswith('Basic '):
|
||||||
raise AuthenticationError(response_code=403)
|
self.auth_error()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user_pass_raw = base64.b64decode(auth_header[6:])
|
user_pass_raw = base64.b64decode(auth_header[6:])
|
||||||
except TypeError:
|
except TypeError:
|
||||||
raise AuthenticationError(response_code=403)
|
self.auth_error()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# http://stackoverflow.com/questions/7242316/what-encoding-should-i-use-for-http-basic-authentication
|
# 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')
|
user_pass_as_text = user_pass_raw.decode('ISO-8859-1')
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
raise AuthenticationError(response_code=403)
|
self.auth_error()
|
||||||
|
|
||||||
user_pass = user_pass_as_text.split(':', 1)
|
user_pass = user_pass_as_text.split(':', 1)
|
||||||
if len(user_pass) != 2:
|
if len(user_pass) != 2:
|
||||||
raise AuthenticationError(response_code=403)
|
self.auth_error()
|
||||||
|
|
||||||
if not self.validate_creds(*user_pass):
|
if not self.validate_creds(*user_pass):
|
||||||
raise AuthenticationError(response_code=403)
|
self.demand_auth()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise AuthenticationError(response_code=401,
|
self.demand_auth()
|
||||||
response_headers={'WWW-Authenticate': 'Basic realm="Websockify"'})
|
|
||||||
|
|
||||||
def validate_creds(self, username, password):
|
def validate_creds(self, username, password):
|
||||||
if '%s:%s' % (username, password) == self.src:
|
if '%s:%s' % (username, password) == self.src:
|
||||||
|
|
@ -70,6 +69,13 @@ class BasicHTTPAuth(object):
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def auth_error(self):
|
||||||
|
raise AuthenticationError(response_code=403)
|
||||||
|
|
||||||
|
def demand_auth(self):
|
||||||
|
raise AuthenticationError(response_code=401,
|
||||||
|
response_headers={'WWW-Authenticate': 'Basic realm="Websockify"'})
|
||||||
|
|
||||||
class ExpectOrigin(object):
|
class ExpectOrigin(object):
|
||||||
def __init__(self, src=None):
|
def __init__(self, src=None):
|
||||||
if src is None:
|
if src is None:
|
||||||
|
|
|
||||||
|
|
@ -56,38 +56,42 @@ Traffic Legend:
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|
||||||
def validate_connection(self):
|
def validate_connection(self):
|
||||||
if self.server.token_plugin:
|
if not self.server.token_plugin:
|
||||||
host, port = self.get_target(self.server.token_plugin, self.path)
|
return
|
||||||
if host == 'unix_socket':
|
|
||||||
self.server.unix_target = port
|
|
||||||
|
|
||||||
else:
|
host, port = self.get_target(self.server.token_plugin)
|
||||||
self.server.target_host = host
|
if host == 'unix_socket':
|
||||||
self.server.target_port = port
|
self.server.unix_target = port
|
||||||
|
|
||||||
if self.server.auth_plugin:
|
else:
|
||||||
|
self.server.target_host = host
|
||||||
|
self.server.target_port = port
|
||||||
|
|
||||||
|
def auth_connection(self):
|
||||||
|
if not self.server.auth_plugin:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# get client certificate data
|
||||||
|
client_cert_data = self.request.getpeercert()
|
||||||
|
# extract subject information
|
||||||
|
client_cert_subject = client_cert_data['subject']
|
||||||
|
# flatten data structure
|
||||||
|
client_cert_subject = dict([x[0] for x in client_cert_subject])
|
||||||
|
# add common name to headers (apache +StdEnvVars style)
|
||||||
|
self.headers['SSL_CLIENT_S_DN_CN'] = client_cert_subject['commonName']
|
||||||
|
except (TypeError, AttributeError, KeyError):
|
||||||
|
# not a SSL connection or client presented no certificate with valid data
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# get client certificate data
|
self.server.auth_plugin.authenticate(
|
||||||
client_cert_data = self.request.getpeercert()
|
headers=self.headers, target_host=self.server.target_host,
|
||||||
# extract subject information
|
target_port=self.server.target_port)
|
||||||
client_cert_subject = client_cert_data['subject']
|
except auth.AuthenticationError:
|
||||||
# flatten data structure
|
ex = sys.exc_info()[1]
|
||||||
client_cert_subject = dict([x[0] for x in client_cert_subject])
|
self.send_auth_error(ex)
|
||||||
# add common name to headers (apache +StdEnvVars style)
|
raise
|
||||||
self.headers['SSL_CLIENT_S_DN_CN'] = client_cert_subject['commonName']
|
|
||||||
except (TypeError, AttributeError, KeyError):
|
|
||||||
# not a SSL connection or client presented no certificate with valid data
|
|
||||||
pass
|
|
||||||
|
|
||||||
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):
|
def new_websocket_client(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -424,6 +428,8 @@ def websockify_init():
|
||||||
help="inetd mode, receive listening socket from stdin", action="store_true")
|
help="inetd mode, receive listening socket from stdin", action="store_true")
|
||||||
parser.add_option("--web", default=None, metavar="DIR",
|
parser.add_option("--web", default=None, metavar="DIR",
|
||||||
help="run webserver on same port. Serve files from DIR.")
|
help="run webserver on same port. Serve files from DIR.")
|
||||||
|
parser.add_option("--web-auth", action="store_true",
|
||||||
|
help="require authentication to access webserver.")
|
||||||
parser.add_option("--wrap-mode", default="exit", metavar="MODE",
|
parser.add_option("--wrap-mode", default="exit", metavar="MODE",
|
||||||
choices=["exit", "ignore", "respawn"],
|
choices=["exit", "ignore", "respawn"],
|
||||||
help="action to take when the wrapped program exits "
|
help="action to take when the wrapped program exits "
|
||||||
|
|
@ -479,6 +485,12 @@ def websockify_init():
|
||||||
if opts.auth_source and not opts.auth_plugin:
|
if opts.auth_source and not opts.auth_plugin:
|
||||||
parser.error("You must use --auth-plugin to use --auth-source")
|
parser.error("You must use --auth-plugin to use --auth-source")
|
||||||
|
|
||||||
|
if opts.web_auth and not opts.auth_plugin:
|
||||||
|
parser.error("You must use --auth-plugin to use --web-auth")
|
||||||
|
|
||||||
|
if opts.web_auth and not opts.web:
|
||||||
|
parser.error("You must use --web to use --web-auth")
|
||||||
|
|
||||||
|
|
||||||
# Transform to absolute path as daemon may chdir
|
# Transform to absolute path as daemon may chdir
|
||||||
if opts.target_cfg:
|
if opts.target_cfg:
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ class WebSockifyRequestHandler(WebSocketRequestHandler, SimpleHTTPRequestHandler
|
||||||
self.handler_id = getattr(server, "handler_id", False)
|
self.handler_id = getattr(server, "handler_id", False)
|
||||||
self.file_only = getattr(server, "file_only", False)
|
self.file_only = getattr(server, "file_only", False)
|
||||||
self.traffic = getattr(server, "traffic", False)
|
self.traffic = getattr(server, "traffic", False)
|
||||||
|
self.web_auth = getattr(server, "web_auth", False)
|
||||||
|
|
||||||
self.logger = getattr(server, "logger", None)
|
self.logger = getattr(server, "logger", None)
|
||||||
if self.logger is None:
|
if self.logger is None:
|
||||||
|
|
@ -217,6 +218,7 @@ class WebSockifyRequestHandler(WebSocketRequestHandler, SimpleHTTPRequestHandler
|
||||||
def handle_upgrade(self):
|
def handle_upgrade(self):
|
||||||
# ensure connection is authorized, and determine the target
|
# ensure connection is authorized, and determine the target
|
||||||
self.validate_connection()
|
self.validate_connection()
|
||||||
|
self.auth_connection()
|
||||||
|
|
||||||
WebSocketRequestHandler.handle_upgrade(self)
|
WebSocketRequestHandler.handle_upgrade(self)
|
||||||
|
|
||||||
|
|
@ -263,6 +265,10 @@ class WebSockifyRequestHandler(WebSocketRequestHandler, SimpleHTTPRequestHandler
|
||||||
self.send_close(exc.args[0], exc.args[1])
|
self.send_close(exc.args[0], exc.args[1])
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
|
if self.web_auth:
|
||||||
|
# ensure connection is authorized, this seems to apply to list_directory() as well
|
||||||
|
self.auth_connection()
|
||||||
|
|
||||||
if self.only_upgrade:
|
if self.only_upgrade:
|
||||||
self.send_error(405, "Method Not Allowed")
|
self.send_error(405, "Method Not Allowed")
|
||||||
else:
|
else:
|
||||||
|
|
@ -279,10 +285,17 @@ class WebSockifyRequestHandler(WebSocketRequestHandler, SimpleHTTPRequestHandler
|
||||||
raise Exception("WebSocketRequestHandler.new_websocket_client() must be overloaded")
|
raise Exception("WebSocketRequestHandler.new_websocket_client() must be overloaded")
|
||||||
|
|
||||||
def validate_connection(self):
|
def validate_connection(self):
|
||||||
""" Ensure that the connection is a valid connection, and set the target. """
|
""" Ensure that the connection has a valid token, and set the target. """
|
||||||
|
pass
|
||||||
|
|
||||||
|
def auth_connection(self):
|
||||||
|
""" Ensure that the connection is authorized. """
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def do_HEAD(self):
|
def do_HEAD(self):
|
||||||
|
if self.web_auth:
|
||||||
|
self.auth_connection()
|
||||||
|
|
||||||
if self.only_upgrade:
|
if self.only_upgrade:
|
||||||
self.send_error(405, "Method Not Allowed")
|
self.send_error(405, "Method Not Allowed")
|
||||||
else:
|
else:
|
||||||
|
|
@ -328,7 +341,7 @@ class WebSockifyServer(object):
|
||||||
listen_host='', listen_port=None, source_is_ipv6=False,
|
listen_host='', listen_port=None, source_is_ipv6=False,
|
||||||
verbose=False, cert='', key='', ssl_only=None,
|
verbose=False, cert='', key='', ssl_only=None,
|
||||||
verify_client=False, cafile=None,
|
verify_client=False, cafile=None,
|
||||||
daemon=False, record='', web='',
|
daemon=False, record='', web='', web_auth=False,
|
||||||
file_only=False,
|
file_only=False,
|
||||||
run_once=False, timeout=0, idle_timeout=0, traffic=False,
|
run_once=False, timeout=0, idle_timeout=0, traffic=False,
|
||||||
tcp_keepalive=True, tcp_keepcnt=None, tcp_keepidle=None,
|
tcp_keepalive=True, tcp_keepcnt=None, tcp_keepidle=None,
|
||||||
|
|
@ -349,6 +362,7 @@ class WebSockifyServer(object):
|
||||||
self.idle_timeout = idle_timeout
|
self.idle_timeout = idle_timeout
|
||||||
self.traffic = traffic
|
self.traffic = traffic
|
||||||
self.file_only = file_only
|
self.file_only = file_only
|
||||||
|
self.web_auth = web_auth
|
||||||
|
|
||||||
self.launch_time = time.time()
|
self.launch_time = time.time()
|
||||||
self.ws_connection = False
|
self.ws_connection = False
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue