Send regex named capture groups as keyword arguments

This commit is contained in:
Michael Lazar 2020-05-24 00:39:47 -04:00
parent b4fd0919eb
commit 701e16fe2a
4 changed files with 140 additions and 7 deletions

View File

@ -2,9 +2,75 @@
### v0.3.0 (pre-release) ### v0.3.0 (pre-release)
- Allow a client certificate subject's CN to be blank. This release brings some major improvements and necessary refactoring of the
- The ``jetforce-diagnostics`` script has been split off into a separate jetforce package. Please read the release notes carefully and exercise caution
repository at [gemini-diagnostics](https://github.com/michael-lazar/gemini-diagnostics). 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) ### v0.2.2 (2012-03-31)

64
examples/pagination.py Normal file
View File

@ -0,0 +1,64 @@
"""
This example demonstrates displaying search results using pagination.
Named capture groups (?P<name>...) 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<module>[a-z]+)")
@app.route("/docs/(?P<module>[a-z]+)/p/(?P<page>[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()

View File

@ -132,7 +132,7 @@ class JetforceApplication:
def __init__(self): def __init__(self):
self.routes: typing.List[ self.routes: typing.List[
typing.Tuple[RoutePattern, typing.Callable[[Request], Response]] typing.Tuple[RoutePattern, typing.Callable[[Request, ...], Response]]
] = [] ] = []
def __call__( def __call__(
@ -145,12 +145,15 @@ class JetforceApplication:
return return
for route_pattern, callback in self.routes[::-1]: for route_pattern, callback in self.routes[::-1]:
match = route_pattern.match(request)
if route_pattern.match(request): if route_pattern.match(request):
callback_kwargs = match.groupdict()
break break
else: else:
callback = self.default_callback callback = self.default_callback
callback_kwargs = {}
response = callback(request) response = callback(request, **callback_kwargs)
send_status(response.status, response.meta) send_status(response.status, response.meta)
if isinstance(response.body, (bytes, str)): if isinstance(response.body, (bytes, str)):
@ -185,7 +188,7 @@ class JetforceApplication:
return wrap 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. Set the error response based on the URL type.
""" """

View File

@ -162,7 +162,7 @@ class StaticDirectoryApplication(JetforceApplication):
else: else:
return mime or "text/plain" 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 Since the StaticDirectoryApplication only serves gemini URLs, return
a proxy request refused for suspicious URLs. a proxy request refused for suspicious URLs.