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)
|
### 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)
|
||||||
|
|
||||||
|
|
|
@ -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):
|
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.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in New Issue