diff options
author | Simo Sorce <simo@redhat.com> | 2015-03-25 15:45:19 -0400 |
---|---|---|
committer | Simo Sorce <simo@redhat.com> | 2015-03-25 15:47:08 -0400 |
commit | d40890b01fb600f09127cff0285472dfbba30442 (patch) | |
tree | db39bdd85a3b9156d26274ebe92c2d95de72405c /custodia/httpd | |
parent | 98085f982e98466b994c033be55047ce370dcac5 (diff) | |
download | custodia-d40890b01fb600f09127cff0285472dfbba30442.tar.gz custodia-d40890b01fb600f09127cff0285472dfbba30442.tar.xz custodia-d40890b01fb600f09127cff0285472dfbba30442.zip |
Adjust the code to be python3 happy
This required the renaming of the http directory to avoid clashes with the
python3 own http/server module.
Diffstat (limited to 'custodia/httpd')
-rw-r--r-- | custodia/httpd/__init__.py | 0 | ||||
-rw-r--r-- | custodia/httpd/authenticators.py | 81 | ||||
-rw-r--r-- | custodia/httpd/consumer.py | 38 | ||||
-rw-r--r-- | custodia/httpd/server.py | 191 |
4 files changed, 310 insertions, 0 deletions
diff --git a/custodia/httpd/__init__.py b/custodia/httpd/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/custodia/httpd/__init__.py diff --git a/custodia/httpd/authenticators.py b/custodia/httpd/authenticators.py new file mode 100644 index 0000000..8a1bff5 --- /dev/null +++ b/custodia/httpd/authenticators.py @@ -0,0 +1,81 @@ +# Copyright (C) 2015 Custodia Project Contributors - see LICENSE file + +from custodia.httpd.server import HTTPError +import os + + +class HTTPAuthenticator(object): + + def __init__(self, config=None): + self.config = config + + def handle(self, request): + raise HTTPError(403) + + +class SimpleCredsAuth(HTTPAuthenticator): + + def __init__(self, config=None): + super(SimpleCredsAuth, self).__init__(config) + self._uid = 0 + self._gid = 0 + if 'uid' in self.config: + self._uid = int(self.config['uid']) + if 'gid' in self.config: + self._gid = int(self.config['gid']) + + def handle(self, request): + uid = int(request['creds']['gid']) + gid = int(request['creds']['uid']) + if self._gid == gid or self._uid == uid: + request['valid_auth'] = True + + +class SimpleHeaderAuth(HTTPAuthenticator): + + def __init__(self, config=None): + super(SimpleHeaderAuth, self).__init__(config) + self.name = 'REMOTE_USER' + self.value = None + if 'header' in self.config: + self.name = self.config['header'] + if 'value' in self.config: + self.value = self.config['value'] + + def handle(self, request): + if self.name not in request['headers']: + return + value = request['headers'][self.name] + if self.value is None: + # Any value is accepted + pass + elif isinstance(self.value, str): + if value != self.value: + return + elif isinstance(self.value, list): + if value not in self.value: + return + else: + return + + request['valid_auth'] = True + request['valid_header'] = value + + +class SimpleNULLAuth(HTTPAuthenticator): + + def __init__(self, config=None): + super(SimpleNULLAuth, self).__init__(config) + self.paths = [] + if 'paths' in self.config: + self.paths = self.config['paths'].split() + + def handle(self, request): + path = request.get('path', '') + while path != '': + if path in self.paths: + request['valid_auth'] = True + if path == '/': + path = '' + else: + path, _ = os.path.split(path) diff --git a/custodia/httpd/consumer.py b/custodia/httpd/consumer.py new file mode 100644 index 0000000..1947b29 --- /dev/null +++ b/custodia/httpd/consumer.py @@ -0,0 +1,38 @@ +# Copyright (C) 2015 Custodia Project Contributors - see LICENSE file + +from custodia.httpd.server import HTTPError + + +DEFAULT_CTYPE = 'text/html; charset=utf-8' + + +class HTTPConsumer(object): + + def __init__(self, config=None): + self.config = config + + def handle(self, request): + command = request.get('command', 'GET') + if not hasattr(self, command): + raise HTTPError(400) + + handler = getattr(self, command) + response = {'headers': dict()} + + # Handle request + output = handler(request, response) + + if 'Content-type' not in response['headers']: + response['headers']['Content-type'] = DEFAULT_CTYPE + + if output is not None: + response['output'] = output + + if 'Content-Length' not in response['headers']: + if hasattr(output, 'read'): + # LOG: warning file-type objects should set Content-Length + pass + else: + response['headers']['Content-Length'] = str(len(output)) + + return response diff --git a/custodia/httpd/server.py b/custodia/httpd/server.py new file mode 100644 index 0000000..ed38ce6 --- /dev/null +++ b/custodia/httpd/server.py @@ -0,0 +1,191 @@ +# Copyright (C) 2015 Custodia Project Contributors - see LICENSE file + +try: + # pylint: disable=import-error + from BaseHTTPServer import BaseHTTPRequestHandler + from SocketServer import ForkingMixIn, UnixStreamServer +except ImportError: + # pylint: disable=import-error + from http.server import BaseHTTPRequestHandler + from socketserver import ForkingMixIn, UnixStreamServer +import io +import os +import shutil +import six +import socket +import struct +import sys +import traceback + +SO_PEERCRED = 17 + + +class HTTPError(Exception): + + def __init__(self, code=None, message=None): + self.code = code if code is not None else 500 + self.mesg = message + super(HTTPError, self).__init__('%d: %s' % (self.code, self.mesg)) + + +def stacktrace(): + with io.BytesIO() as f: + _, _, tb = sys.exc_info() + traceback.print_tb(tb, None, file=f) + del tb + return f.getvalue() + + +class ForkingLocalHTTPServer(ForkingMixIn, UnixStreamServer): + + server_string = "Custodia/0.1" + allow_reuse_address = True + socket_file = None + + def __init__(self, server_address, handler_class, config): + UnixStreamServer.__init__(self, server_address, handler_class) + if 'consumers' not in config: + raise ValueError('Configuration does not provide any consumer') + self.config = config + if 'server_string' in self.config: + self.server_string = self.config['server_string'] + + def server_bind(self): + UnixStreamServer.server_bind(self) + self.socket_file = self.socket.getsockname() + + def pipeline(self, request): + + # auth framework here + authers = self.config.get('authenticators') + if authers is None: + raise HTTPError(403) + for auth in authers: + authers[auth].handle(request) + if 'valid_auth' not in request or request['valid_auth'] is not True: + raise HTTPError(403) + + # Select consumer + path = request.get('path', '') + if not os.path.isabs(path): + raise HTTPError(400) + + trail = [] + while path != '': + if path in self.config['consumers']: + con = self.config['consumers'][path] + if len(trail) != 0: + request['trail'] = trail + return con.handle(request) + if path == '/': + path = '' + else: + head, tail = os.path.split(path) + trail.insert(0, tail) + path = head + + raise HTTPError(404) + + +class LocalHTTPRequestHandler(BaseHTTPRequestHandler): + + protocol_version = "HTTP/1.1" + + def __init__(self, *args, **kwargs): + BaseHTTPRequestHandler.__init__(self, *args, **kwargs) + self.requestline = '' + self.request_version = '' + self.command = '' + self.raw_requestline = None + self.close_connection = 0 + + def version_string(self): + return self.server.server_string + + @property + def peer_creds(self): + + creds = self.request.getsockopt(socket.SOL_SOCKET, SO_PEERCRED, + struct.calcsize('3i')) + pid, uid, gid = struct.unpack('3i', creds) + return {'pid': pid, 'uid': uid, 'gid': gid} + + def handle_one_request(self): + # Set a fake client address to make log functions happy + self.client_address = ['127.0.0.1', 0] + try: + if not self.server.pipeline: + self.close_connection = 1 + return + self.raw_requestline = self.rfile.readline(65537) + if not self.raw_requestline: + self.close_connection = 1 + return + if len(self.raw_requestline) > 65536: + self.requestline = '' + self.request_version = '' + self.command = '' + self.send_error(414) + self.wfile.flush() + return + if not self.parse_request(): + return + request = {'creds': self.peer_creds, + 'command': self.command, + 'path': self.path, + 'version': self.request_version, + 'headers': self.headers} + try: + response = self.server.pipeline(request) + if response is None: + raise HTTPError(500) + except HTTPError as e: + self.send_error(e.code, e.mesg) + self.wfile.flush() + return + except socket.timeout as e: + self.log_error("Request timed out: %r", e) + self.close_connection = 1 + return + except Exception as e: # pylint: disable=broad-except + self.log_error("Handler failed: %r", e) + self.log_traceback() + self.send_error(500) + self.wfile.flush() + return + self.send_response(response.get('code', 200)) + for header, value in six.iteritems(response.get('headers', {})): + self.send_header(header, value) + self.end_headers() + output = response.get('output', None) + if hasattr(output, 'read'): + shutil.copyfileobj(output, self.wfile) + output.close() + else: + self.wfile.write(output.encode('utf-8')) + self.wfile.flush() + return + except socket.timeout as e: + self.log_error("Request timed out: %r", e) + self.close_connection = 1 + return + + def log_traceback(self): + self.log_error('Traceback:\n%s' % stacktrace()) + + +class LocalHTTPServer(object): + + def __init__(self, address, config): + if address[0] != '/': + raise ValueError('Must use absolute unix socket name') + if os.path.exists(address): + os.remove(address) + self.httpd = ForkingLocalHTTPServer(address, LocalHTTPRequestHandler, + config) + + def get_socket(self): + return (self.httpd.socket, self.httpd.socket_file) + + def serve(self): + return self.httpd.serve_forever() |