diff --git a/examples/cowsay.cgi b/examples/cgi/cowsay.cgi similarity index 100% rename from examples/cowsay.cgi rename to examples/cgi/cowsay.cgi diff --git a/examples/debug.cgi b/examples/cgi/debug.cgi similarity index 100% rename from examples/debug.cgi rename to examples/cgi/debug.cgi diff --git a/examples/echo.py b/examples/echo.py new file mode 100644 index 0000000..1b8e91f --- /dev/null +++ b/examples/echo.py @@ -0,0 +1,32 @@ +""" +A bare-bones server that with echo back the request to the client. + +This example demonstrates the simplest proof-of-concept of how you can write +your own application from scratch instead of sub-classing from the provided +JetforceApplication. The server/application interface is almost identical to +WSGI defined in PEP-3333 [1]. + +Unless you're feeling adventurous, you probably want to stick to the +JetforceApplication instead of going this low-level. + +[1] https://www.python.org/dev/peps/pep-3333/#id20 +""" +import jetforce + + +def app(environ, send_status): + """ + Arguments: + environ: A dictionary containing information about the request + send_status: A callback function that takes two parameters: The + response status (int) and the response meta text (str). + + Returns: A generator containing the response body. + """ + send_status(10, "text/gemini") + yield f"Received path: {environ['GEMINI_URL']}" + + +if __name__ == "__main__": + server = jetforce.GeminiServer(app) + server.run() diff --git a/examples/echo_server.py b/examples/echo_server.py deleted file mode 100644 index 1056887..0000000 --- a/examples/echo_server.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -A simple Gemini server that echos back the request to the client. -""" -import asyncio - -import jetforce - - -def echo(environ, send_status): - url = environ["GEMINI_URL"] - send_status(jetforce.Status.SUCCESS, "text/gemini") - yield f"Received path: {url}".encode() - - -if __name__ == "__main__": - args = jetforce.command_line_parser().parse_args() - ssl_context = jetforce.make_ssl_context( - args.hostname, args.certfile, args.keyfile, args.cafile, args.capath - ) - server = jetforce.GeminiServer( - host=args.host, - port=args.port, - ssl_context=ssl_context, - hostname=args.hostname, - app=echo, - ) - asyncio.run(server.run()) diff --git a/jetforce/protocol.py b/jetforce/protocol.py index 0b20233..3feffcd 100644 --- a/jetforce/protocol.py +++ b/jetforce/protocol.py @@ -59,16 +59,32 @@ class GeminiProtocol(LineOnlyReceiver): def lineReceived(self, line): """ This method is invoked by LineOnlyReceiver for every incoming line. - - Because Gemini requests are only ever a single line long, this will - only be called once and we can use it to handle the lifetime of the - connection without managing any state. """ self.request = line return ensureDeferred(self._handle_request_noblock()) async def _handle_request_noblock(self): + """ + Handle the gemini request and write the raw response to the socket. + This method is implemented using an async coroutine, which has been + supported by twisted since python 3.5 by wrapping the method in + ensureDeferred(). Twisted + coroutines is a bitch to figure out, but + once it clicks it really does turn out to be an elegant solution. + + Any time that we call into the application code, we wrap the call with + deferToThread() which will execute the code in a separate thread using + twisted's thread pool. deferToThread() will return a future object + that we can then `await` to get the result when the thread finishes. + This is important because we don't want application code to block the + twisted event loop from serving other requests at the same time. + + In the future, I would like to add the capability for applications to + implement proper coroutines that can call `await` on directly without + needing to wrap them in threads. Conceptually, this shouldn't be too + difficult, but it will require implementing an alternate version of + the JetforceApplication that's async-compatible. + """ try: self.parse_header() except Exception: diff --git a/jetforce/tls.py b/jetforce/tls.py index 3145844..aea659d 100644 --- a/jetforce/tls.py +++ b/jetforce/tls.py @@ -124,7 +124,8 @@ class GeminiCertificateOptions(CertificateOptions): keyfile: typing.Optional[str] = None, cafile: typing.Optional[str] = None, capath: typing.Optional[str] = None, - ): + ) -> None: + self.certfile = certfile self.keyfile = keyfile self.cafile = cafile @@ -136,7 +137,7 @@ class GeminiCertificateOptions(CertificateOptions): fixBrokenPeers=True, ) - def _makeContext(self): + def _makeContext(self) -> OpenSSL.SSL.Context: """ Most of this code is copied directly from the parent class method. """