Send regex named capture groups as keyword arguments
This commit is contained in:
parent
b4fd0919eb
commit
701e16fe2a
72
CHANGELOG.md
72
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)
|
||||
|
||||
|
|
|
@ -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()
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue