Skip to content

Commit

Permalink
Add support for dual HTTP/HTTPS server operation
Browse files Browse the repository at this point in the history
Signed-off-by: Jean Snyman <[email protected]>
  • Loading branch information
stringlytyped committed Feb 15, 2024
1 parent a0d3cf7 commit 1b42ee2
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 54 deletions.
7 changes: 1 addition & 6 deletions keylime/cmd/registrar.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,8 @@ def main() -> None:
# Prepare to use the registrar database
db_manager.make_engine("registrar")

# Get config values needed to start the HTTP server
host = config.get("registrar", "ip")
port = config.get("registrar", "port")
max_upload_size = config.getint("registrar", "max_upload_size", None, 0)

# Start HTTP server
server = RegistrarServer(host, port, max_upload_size)
server = RegistrarServer()
tornado.process.fork_processes(0)
asyncio.run(server.start())

Expand Down
13 changes: 7 additions & 6 deletions keylime/web/base/action_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,14 @@ def initialize(self, server):
self._controller = DefaultController(self)
self._finished = False

def prepare(self):
method = self.request.method.lower()
path = self.request.path

def prepare(self):
# Find highest-priority route which matches the request
route = self.server.first_matching_route(method, path)
route = self.server.first_matching_route(self.request.method, self.request.path)

# Handle situations where a matching route does not exist
if not route:
# Check if any route with that path exists
route_with_path = self.server.first_matching_route(None, path)
route_with_path = self.server.first_matching_route(None, self.request.path)

if route_with_path:
self.controller.method_not_allowed()
Expand All @@ -42,6 +39,10 @@ def prepare(self):

return

# Handle situation where HTTP is used to access an HTTPS-only route
if self.request.protocol == "http" and not route.allow_insecure:
self.controller.https_required()

# Save found route in object attribute
self._matching_route = route
# Create a new instance of the controller for the current ActionHandler instance
Expand Down
5 changes: 4 additions & 1 deletion keylime/web/base/default_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ def not_found(self, **params):
self.send_response(404, "Not Found")

def method_not_allowed(self, **params):
self.send_response(405, "Method Not Allowed")
self.send_response(405, "Method Not Allowed")

def https_required(self, **params):
self.send_response(400, "Bad Request")
14 changes: 10 additions & 4 deletions keylime/web/base/route.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,15 @@ def _split_path(path):

return segments

def __init__(self, method, pattern, controller, action):
def __init__(self, method, pattern, controller, action, allow_insecure=False):
"""Instantiates a newly created route with the given method, pattern, controller and action. Typically, this
should be done by using the helper methods in the ``Server`` abstract base class.
:param method: The HTTP method which the route will match (e.g., ``"get"``, ``"post"``, etc.)
:param pattern: The pattern which the route will match against a given path
:param controller: A class which inherits from ``Controller`` and contains the actions for this route
:param action: The name of an instance method in the controller which will be called on a matching request
:param allow_insecure: Whether this route should accept requests made over insecure HTTP (default: ``False``)
:raises: :class:`TypeError`: An argument is of an incorrect type
:raises: :class:`InvalidMethod`: The given HTTP method is not one accepted by the Routes class
Expand Down Expand Up @@ -153,6 +154,7 @@ def __init__(self, method, pattern, controller, action):
self._method = method
self._controller = controller
self._action = action
self._allow_insecure = bool(allow_insecure)

try:
self._parse_pattern(pattern)
Expand Down Expand Up @@ -276,7 +278,6 @@ def matches_path(self, path):
:param path: The path to check against the route pattern
:raises: :class:`InvalidPathOrPattern`: The given path is invalid
:raises: :class:`PatternMismatch`: The given path does not match route's pattern
:returns: ``True`` if the path matches the route pattern, ``False`` otherwise
"""
Expand All @@ -297,10 +298,11 @@ def matches(self, method, path):
:raises: :class:`InvalidMethod`: The given method is invalid
:raises: :class:`InvalidPathOrPattern`: The given path is invalid
:raises: :class:`PatternMismatch`: The given path does not match route's pattern
:returns: ``True`` if the method and path both match the route, ``False`` otherwise
"""
method = method.lower()

if method not in Route.ALLOWABLE_METHODS:
raise InvalidMethod(f"method '{method}' is not an allowable HTTP method")

Expand Down Expand Up @@ -367,4 +369,8 @@ def controller(self):

@property
def action(self):
return self._action
return self._action

@property
def allow_insecure(self):
return self._allow_insecure
133 changes: 101 additions & 32 deletions keylime/web/base/server.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from abc import ABC, abstractmethod
from functools import wraps
from ssl import CERT_OPTIONAL
import asyncio
import tornado
from keylime import config, web_util
from keylime.web.base.route import Route
from keylime.web.base.action_handler import ActionHandler

Expand Down Expand Up @@ -35,6 +37,13 @@ def _routes(self):
server = ExampleServer()
server.start()
To spawn multiple worker processes for handling requests, you can call Tornado's ``fork_processes`` function after
instantiating the server, but before starting it:
server = ExampleServer()
tornado.process.fork_processes(0)
server.start()
Decorators
----------
Expand Down Expand Up @@ -93,8 +102,8 @@ def version_scope_wrapper(obj, *args, **kwargs):
if route not in initial_routes:
# Define routes scoped to the API version specified by major_version
new_routes_list.extend([
Route(route.method, f"/v{major_version}{route.pattern}", route.controller, route.action),
Route(route.method, f"/v{major_version}.:minor{route.pattern}", route.controller, route.action)
Route(route.method, f"/v{major_version}{route.pattern}", route.controller, route.action, route.allow_insecure),
Route(route.method, f"/v{major_version}.:minor{route.pattern}", route.controller, route.action, route.allow_insecure)
])

# Replace the Server instance's list of routes with a new list consisting of the routes which were
Expand All @@ -106,12 +115,39 @@ def version_scope_wrapper(obj, *args, **kwargs):
return version_scope_wrapper
return version_scope_decorator

def __init__(self, host, port, max_upload_size=None):
self._host = host
self._port = port
self._max_upload_size = max_upload_size
def __init__(self, **options):
"""Initialise server with provided configuration options or default values and bind to sockets for HTTP and/or
HTTPS connections. This does not start the server to start accepting requests (this is done by calling the
``server.start()`` instance method).
# Initialise empty list of routes
If you wish to create multiple server processes, first instantiate a new server and then fork the process
before starting the server with `server.start()`.
"""
# Set defaults for server options
self._host = "127.0.0.1"
self._http_port = 80
self._https_port = 443
self._max_upload_size = 104857600 # 100MiB
self._ssl_ctx = None

# Override defaults with values given by the implementing class
self._setup()

# If options are set by the caller, use these to override the defaults and those set by the implementing class
for opt in ["host", "http_port", "https_port", "max_upload_size", "ssl_ctx"]:
if opt in options:
setattr(f"_{opt}", options[opt])

if not self.host:
raise ValueError(f"server '{self.__class__.__name__}' cannot be initialised without a value for 'host'")

if not self.http_port or (not self.https_port or not self.ssl_ctx):
raise ValueError(
f"server '{self.__class__.__name__}' cannot be initialised without either 'http_port' or 'https_port'"
f"and 'ssl_ctx'"
)

# Initialise empty list for routes
self.__routes = []

# Add routes defined by the implementing class
Expand All @@ -122,75 +158,100 @@ def __init__(self, host, port, max_upload_size=None):
(r".*", ActionHandler, {"server": self})
])

# Bind socket to user-configured port and address
self.__tornado_sockets = tornado.netutil.bind_sockets(int(self.port), address=self.host)
# Bind socket for HTTP connections
self.__tornado_http_sockets = tornado.netutil.bind_sockets(int(self.http_port), address=self.host)

# TODO: Evaluate init_mtls function:
# self.__ssl_ctx = web_util.init_mtls("verifier", logger=logger)
self.__ssl_ctx = None
# Bind socket for HTTPS connections
self.__tornado_https_sockets = tornado.netutil.bind_sockets(int(self.https_port), address=self.host)

async def start(self):
"""Instantiates and starts the HTTP server.
"""Instantiates and starts the server (with one Tornado HTTPServer instance to handle HTTP connections
and another to handle HTTPS connections).
This should be done once per process. When new processes are created by forking, this method
should be called after the fork.
"""

self.__tornado_server = tornado.httpserver.HTTPServer(
self.__tornado_http_server = tornado.httpserver.HTTPServer(
self.__tornado_app,
ssl_options=None,
max_buffer_size=self.max_upload_size
)

self.__tornado_https_server = tornado.httpserver.HTTPServer(
self.__tornado_app,
ssl_options=self.__ssl_ctx,
ssl_options=self.ssl_ctx,
max_buffer_size=self.max_upload_size
)

self.__tornado_server.add_sockets(self.__tornado_sockets)
self.__tornado_http_server.add_sockets(self.__tornado_http_sockets)
self.__tornado_https_server.add_sockets(self.__tornado_https_sockets)

await asyncio.Event().wait()

def _setup(self):
"""Defines values to use in place of the defaults for the various server options. It is suggested that this is
overriden by the implementing class."""
pass

@abstractmethod
def _routes(self):
"""Defines the routes accepted the server. Must be overridden by the implementing class and include one
or more calls to the ``_get``, ``_head``, ``_post``, ``_put``, ``_patch``, ``_delete`` and/or ``_options``
helper methods.
"""
pass

def _get(self, pattern, controller, action):
def _use_config(self, component):
"""Sets server options to values found in the config."""
self._host = config.get(component, "ip")
self._http_port = config.getint(component, "port", fallback=0)
self._https_port = config.getint(component, "tls_port", fallback=0)
self._max_upload_size = config.getint(component, "max_upload_size", fallback=104857600)
self._ssl_ctx = web_util.init_mtls(component)
self._ssl_ctx.verify_mode = CERT_OPTIONAL

def _get(self, pattern, controller, action, allow_insecure=False):
"""Creates a new route to handle incoming GET requests issued for paths which match the given
pattern. Must be called from a Server subclass's ``self._routes`` method.
"""
self.__routes.append(Route("get", pattern, controller, action))
self.__routes.append(Route("get", pattern, controller, action, allow_insecure))

def _head(self, pattern, controller, action):
def _head(self, pattern, controller, action, allow_insecure=False):
"""Creates a new route to handle incoming HEAD requests issued for paths which match the given
pattern. Must be called from a Server subclass's ``self._routes`` method.
"""
self.__routes.append(Route("head", pattern, controller, action))
self.__routes.append(Route("head", pattern, controller, action, allow_insecure))

def _post(self, pattern, controller, action):
def _post(self, pattern, controller, action, allow_insecure=False):
"""Creates a new route to handle incoming POST requests issued for paths which match the given
pattern. Must be called from a Server subclass's ``self._routes`` method.
"""
self.__routes.append(Route("post", pattern, controller, action))
self.__routes.append(Route("post", pattern, controller, action, allow_insecure))

def _put(self, pattern, controller, action):
def _put(self, pattern, controller, action, allow_insecure=False):
"""Creates a new route to handle incoming PUT requests issued for paths which match the given
pattern. Must be called from a Server subclass's ``self._routes`` method.
"""
self.__routes.append(Route("put", pattern, controller, action))
self.__routes.append(Route("put", pattern, controller, action, allow_insecure))

def _patch(self, pattern, controller, action):
def _patch(self, pattern, controller, action, allow_insecure=False):
"""Creates a new route to handle incoming PATCH requests issued for paths which match the given
pattern. Must be called from a Server subclass's ``self._routes`` method.
"""
self.__routes.append(Route("patch", pattern, controller, action))
self.__routes.append(Route("patch", pattern, controller, action, allow_insecure))

def _delete(self, pattern, controller, action):
def _delete(self, pattern, controller, action, allow_insecure=False):
"""Creates a new route to handle incoming DELETE requests issued for paths which match the given
pattern. Must be called from a Server subclass's ``self._routes`` method.
"""
self.__routes.append(Route("delete", pattern, controller, action))
self.__routes.append(Route("delete", pattern, controller, action, allow_insecure))

def _options(self, pattern, controller, action):
def _options(self, pattern, controller, action, allow_insecure=False):
"""Creates a new route to handle incoming OPTIONS requests issued for paths which match the given
pattern. Must be called from a Server subclass's ``self._routes`` method.
"""
self.__routes.append(Route("options", pattern, controller, action))
self.__routes.append(Route("options", pattern, controller, action, allow_insecure))

def first_matching_route(self, method, path):
"""Gets the highest-priority route which matches the given ``method`` and ``path``.
Expand All @@ -206,8 +267,12 @@ def first_matching_route(self, method, path):
return None

@property
def port(self):
return self._port
def http_port(self):
return self._http_port

@property
def https_port(self):
return self._https_port

@property
def host(self):
Expand All @@ -216,6 +281,10 @@ def host(self):
@property
def max_upload_size(self):
return self._max_upload_size

@property
def ssl_ctx(self):
return self._ssl_ctx

@property
def routes(self):
Expand Down
16 changes: 11 additions & 5 deletions keylime/web/registrar_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@
from keylime.web.registrar.agents_controller import AgentsController

class RegistrarServer(Server):

def _setup(self):
self._use_config("registrar")

def _routes(self):
self._v2_routes()

@Server.version_scope(2)
def _v2_routes(self):
# Routes used by the tenant to manage registered agents
self._get("/agents", AgentsController, "index")
self._get("/agents/:agent_id", AgentsController, "show")
self._post("/agents", AgentsController, "create")
self._delete("/agents/:agent_id", AgentsController, "delete")
self._post("/agents/:agent_id/activate", AgentsController, "activate")

# These routes are kept for backwards compatibility but are less semantically correct according to RFC 9110
self._post("/agents/:agent_id", AgentsController, "create")
self._put("/agents/:agent_id/activate", AgentsController, "activate")
# Routes used by agents to register (which happens over HTTP without TLS)
self._post("/agents", AgentsController, "create", allow_insecure=True)
self._post("/agents/:agent_id/activate", AgentsController, "activate", allow_insecure=True)

# Routes which are kept for backwards compatibility but do not adhere to RFC 9110 semantics
self._post("/agents/:agent_id", AgentsController, "create", allow_insecure=True)
self._put("/agents/:agent_id/activate", AgentsController, "activate", allow_insecure=True)

0 comments on commit 1b42ee2

Please sign in to comment.