diff --git a/CHANGELOG.md b/CHANGELOG.md index 56a139e..f423bdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,75 @@ ### v0.3.0 (pre-release) -- Allow a client certificate subject's CN to be blank. -- The ``jetforce-diagnostics`` script has been split off into a separate - repository at [gemini-diagnostics](https://github.com/michael-lazar/gemini-diagnostics). +This release brings some major improvements and necessary refactoring of the +jetforce package. Please read the release notes carefully and exercise caution +when upgrading from previous versions of jetforce. + +#### For users of the static file server + +If you are running jetforce only as a static file & CGI server (i.e. you +are using the command-line and haven't written any custom python applications), +you should not need to make any changes. + +There have been some minor updates to the CGI variables, and new CGI variables +have been added with additional TLS information. Check out the README for the +new list. + +This package now has third-party python dependencies. If you installed jetforce +through pip, you should be already fine. If you were running the ``jetforce.py`` +script directly from the git repository, you will likely either want to switch +to installing from pip (recommended), or setup a virtual environment and run +``python setup.py install``. This will install the dependencies and stick a +``jetforce`` executable into your system path. + +#### jetforce-diagnostics + +The ``jetforce-diagnostics`` script is no longer included as part of jetforce. +It has been moved to its own repository at +[gemini-diagnostics](https://github.com/michael-lazar/gemini-diagnostics). + +#### Code Structure + +The underlying TCP server framework has been switched from asyncio+ssl to +twisted+PyOpenSSL. This change was necessary to allow support for self-signed +client certificates. The new framework provides more access to hook into the +OpenSSL library and implement non-standard TLS behavior. + +I tried to isolate the framework changes to the ``GeminiServer`` layer. This +means that if you subclassed from the ``JetforceApplication``, you will likely +not need to change anything in your application code. Launching a jetforce +server from inside of python code has been simplified (no more setting up the +asyncio event loop!). + +``` +server = GeminiServer(app) +server.run() +``` + +Check out the updated examples in the examples/ directory for more details. + +#### TLS Client Certificates + +Jetforce will now accept self-signed and unvalidated client certificates. The +``capath`` and ``cafile`` arguments can still be provided, and will attempt to +validate the certificate using of the underlying OpenSSL library. The result +of this validation will be saved in the ``TLS_CLIENT_VERIFIED`` environment +variable so that each application can decide how it wants to accept/reject the +connection. + +In order to facilitate TOFU verification schemes, a fingerprint of the client +certificate is now computed and saved in the ``TLS_CLIENT_HASH`` environment +variable. + +#### Other Changes + +- A client certificate can now have an empty ``commonName`` field. +- ``JetforceApplication``: Named capture groups in a route's regex pattern + will now be passed as keyword arguments to the wrapped function. See + examples/pagination.py for an example of how to use this feature. +- A new ``CompositeApplication`` class is included to support virtual hosting + by combining multiple applications behind the same jetforce server. See + examples/vhost.py for an example of how to use this class. ### v0.2.2 (2012-03-31) diff --git a/examples/pagination.py b/examples/pagination.py new file mode 100644 index 0000000..e7f9cb9 --- /dev/null +++ b/examples/pagination.py @@ -0,0 +1,64 @@ +""" +This example demonstrates displaying search results using pagination. + +Named capture groups (?P...) will be passed along as additional keyword +arguments to the wrapped function. You can apply two routes to the same view in +order to make the page number optional in the URL. + +Try to think outside of the box when you design your UX. Will users need to +navigate forward/backward through the results, or only forward? Should the +number or results returned per-page be controllable? + +> jetforce-client https://localhost/docs/ssl/p/1 +""" +import math +import ssl + +from jetforce import GeminiServer, JetforceApplication, Response, Status + +modules = {"ssl": ssl.__doc__, "math": math.__doc__} +paginate_by = 10 + +app = JetforceApplication() + + +@app.route("", strict_trailing_slash=False) +def index(request): + lines = ["View documentation for the following modules:", ""] + for module in modules: + lines.append(f"=>/docs/{module} {module}") + return Response(Status.SUCCESS, "text/gemini", "\n".join(lines)) + + +@app.route("/docs/(?P[a-z]+)") +@app.route("/docs/(?P[a-z]+)/p/(?P[0-9]+)") +def docs(request, module, page=1): + page = int(page) + + if module not in modules: + return Response(Status.NOT_FOUND, f"Invalid module {module}") + + help_text_lines = modules[module].splitlines() + + page_count = math.ceil(len(help_text_lines) / paginate_by) + page_count = max(page_count, 1) + if page > page_count: + return Response(Status.NOT_FOUND, "Invalid page number") + + offset = (page - 1) * paginate_by + items = help_text_lines[offset : offset + paginate_by] + + lines = [f"[{module}]", f"(page {page} of {page_count})", ""] + lines.extend(items) + lines.append("") + if page < page_count: + lines.append(f"=>/docs/{module}/p/{page+1} Next {paginate_by} lines") + if page > 1: + lines.append(f"=>/docs/{module} Back to the beginning") + + return Response(Status.SUCCESS, "text/gemini", "\n".join(lines)) + + +if __name__ == "__main__": + server = GeminiServer(app) + server.run() diff --git a/jetforce/app/base.py b/jetforce/app/base.py index 2adc9c0..73fc320 100644 --- a/jetforce/app/base.py +++ b/jetforce/app/base.py @@ -132,7 +132,7 @@ class JetforceApplication: def __init__(self): self.routes: typing.List[ - typing.Tuple[RoutePattern, typing.Callable[[Request], Response]] + typing.Tuple[RoutePattern, typing.Callable[[Request, ...], Response]] ] = [] def __call__( @@ -145,12 +145,15 @@ class JetforceApplication: return for route_pattern, callback in self.routes[::-1]: + match = route_pattern.match(request) if route_pattern.match(request): + callback_kwargs = match.groupdict() break else: callback = self.default_callback + callback_kwargs = {} - response = callback(request) + response = callback(request, **callback_kwargs) send_status(response.status, response.meta) if isinstance(response.body, (bytes, str)): @@ -185,7 +188,7 @@ class JetforceApplication: return wrap - def default_callback(self, request: Request) -> Response: + def default_callback(self, request: Request, **_) -> Response: """ Set the error response based on the URL type. """ diff --git a/jetforce/app/static.py b/jetforce/app/static.py index bc9a282..713db6e 100644 --- a/jetforce/app/static.py +++ b/jetforce/app/static.py @@ -162,7 +162,7 @@ class StaticDirectoryApplication(JetforceApplication): else: return mime or "text/plain" - def default_callback(self, request: Request) -> Response: + def default_callback(self, request: Request, **_) -> Response: """ Since the StaticDirectoryApplication only serves gemini URLs, return a proxy request refused for suspicious URLs.