diff --git a/README.md b/README.md index 0e77a86..23daf95 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,63 @@ # Jetforce -An experimental python server for the new, under development Gemini Protocol. - -Learn more about Project Gemini [here](https://gopher.commons.host/gopher://zaibatsu.circumlunar.space/1/~solderpunk/gemini). +An experimental TCP server for the new, under development +[Gemini Protocol](https://gopher.commons.host/gopher://zaibatsu.circumlunar.space/1/~solderpunk/gemini). ![Rocket Launch](resources/rocket.jpg) ## Features -- A modern python codebase with type hinting and black formatting. -- A built-in static file server with support for gemini directory files. -- Lightweight, single-file framework with zero dependencies. +- A built-in static file server with support for gemini directories and + CGI scripts. +- Lightweight, single-file framework with zero external dependencies. +- Modern python codebase with type hinting and black style formatting. - Supports concurrent connections using an asynchronous event loop. -- Extendable - loosely implements the [WSGI](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface) +- Extendable components that loosely implement the [WSGI](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface) server/application pattern. ## Installation -Requires Python 3.7+ and OpenSSL. +Requires Python 3.7+ -The latest release can be installed from [PyPI](https://pypi.org/project/Jetforce/) +The latest release can be installed from [PyPI](https://pypi.org/project/Jetforce/): -``` +```bash $ pip install jetforce ``` -Or, simply clone the repository and run the script directly +Or, clone the repository and run the script directly: -``` +```bash $ git clone https://github.com/michael-lazar/jetforce $ cd jetforce -$ ./jetforce.py +$ python3 jetforce.py ``` ## Usage Use the ``--help`` flag to view command-line options: - -``` +```bash $ jetforce --help usage: jetforce [-h] [--host HOST] [--port PORT] [--tls-certfile FILE] [--tls-keyfile FILE] [--hostname HOSTNAME] [--dir DIR] - [--index-file INDEX_FILE] + [--cgi-dir DIR] [--index-file FILE] An Experimental Gemini Protocol Server optional arguments: - -h, --help show this help message and exit - --host HOST Address to bind server to (default: 127.0.0.1) - --port PORT Port to bind server to (default: 1965) - --tls-certfile FILE TLS certificate file (default: None) - --tls-keyfile FILE TLS private key file (default: None) - --hostname HOSTNAME Server hostname (default: localhost) - --dir DIR Path on the filesystem to serve (default: /var/gemini) - --index-file INDEX_FILE - The gemini directory index file (default: index.gmi) + -h, --help show this help message and exit + --host HOST Server address to bind to (default: 127.0.0.1) + --port PORT Server port to bind to (default: 1965) + --tls-certfile FILE Server TLS certificate file (default: None) + --tls-keyfile FILE Server TLS private key file (default: None) + --hostname HOSTNAME Server hostname (default: localhost) + --dir DIR Local directory to serve (default: /var/gemini) + --cgi-dir DIR CGI script directory, relative to the server's root + directory (default: cgi-bin) + --index-file FILE If a directory contains a file with this name, that + file will be served instead of auto-generating an index + page (default: index.gmi) If the TLS cert/keyfile is not provided, a self-signed certificate will automatically be generated and saved to your temporary directory. @@ -78,8 +80,6 @@ $ openssl req -newkey rsa:2048 -nodes -keyout {hostname}.key \ -nodes -x509 -out {hostname}.crt -subj "/CN={hostname}" ``` -#### TLS Client Certificates - There are currently no plans to support transient self-signed client certificates. This is due to a techinical limitation of the python standard library's ``ssl`` module, which is described in detail @@ -90,13 +90,13 @@ Support for verified client certificates will be added in a future version. ### Hostname Because the gemini protocol sends the *whole* URL in the request, it's required -that you declare which hostname your server is expecting to receive traffic under. -Jetforce will respond to any request containing a URL that don't match your hostname -with a status of ``Proxy Request Refused``. +that you declare the hostname that your server is expecting to receive traffic +under. Jetforce will reject any request that doesn't match your hostname with a +status of ``Proxy Request Refused``. -Using python, you can modify this behavior to do fancy things like building a proxy -server for HTTP requests. See [http_proxy.py](examples/http_proxy.py) for -an example of how to accomplish this. +Using python, you can modify this behavior to do fancy things like building a +proxy server for HTTP requests. See [http_proxy.py](examples/http_proxy.py) for +an example of how this is done. ### Serving Files @@ -107,7 +107,20 @@ Jetforce serves files from the ``/var/gemini/`` directory by default: - Directories will look for a file with the name **index.gmi**. - If an index file does not exist, a directory listing will be generated. -CGI scripts are not currently supported. This feature might be added in a future version. +### CGI Scripts + +Jetforce implements a slightly modified version of the official CGI +specification. Because Gemini is a less complex than HTTP, the CGI interface is +also inherently easier and more straightforward to use. + +The main difference in this implementation is that the CGI script is expected +to write the entire gemini response *verbetim* to stdout: + +1. The status code and meta on the first line +2. Any additional response body on subsequent lines + +Unlike HTTP's CGI, there are no request/response headers or other special +fields to perform actions like redirects. ## License diff --git a/examples/cowsay.cgi b/examples/cowsay.cgi index b75fff9..07b6aaa 100755 --- a/examples/cowsay.cgi +++ b/examples/cowsay.cgi @@ -19,7 +19,7 @@ import urllib.parse query = os.environ["QUERY_STRING"] if not query: - print("10 Enter your cowsay message") + print("10 Enter your cowsay message: ") sys.exit() text = urllib.parse.unquote(query) diff --git a/jetforce.py b/jetforce.py index a2f3b0b..51982d4 100755 --- a/jetforce.py +++ b/jetforce.py @@ -204,7 +204,7 @@ class StaticDirectoryApplication(JetforceApplication): self, root_directory: str = "/var/gemini", index_file: str = "index.gmi", - cgi_directory: str = "/cgi-bin", + cgi_directory: str = "cgi-bin", ): super().__init__() self.routes.append((RoutePattern(), self.serve_static_file)) @@ -568,13 +568,19 @@ def command_line_parser() -> argparse.ArgumentParser: epilog=EPILOG, formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) - parser.add_argument("--host", help="Address to bind server to", default="127.0.0.1") - parser.add_argument("--port", help="Port to bind server to", type=int, default=1965) + parser.add_argument("--host", help="Server address to bind to", default="127.0.0.1") + parser.add_argument("--port", help="Server port to bind to", type=int, default=1965) parser.add_argument( - "--tls-certfile", dest="certfile", help="TLS certificate file", metavar="FILE" + "--tls-certfile", + dest="certfile", + help="Server TLS certificate file", + metavar="FILE", ) parser.add_argument( - "--tls-keyfile", dest="keyfile", help="TLS private key file", metavar="FILE" + "--tls-keyfile", + dest="keyfile", + help="Server TLS private key file", + metavar="FILE", ) parser.add_argument("--hostname", help="Server hostname", default="localhost") return parser @@ -586,15 +592,23 @@ def run_server() -> None: """ parser = command_line_parser() parser.add_argument( - "--dir", help="Root path on the filesystem to serve", default="/var/gemini" + "--dir", + help="Root directory on the filesystem to serve", + default="/var/gemini", + metavar="DIR", ) parser.add_argument( "--cgi-dir", - help="CGI script folder, relative to the server's root directory", - default="/cgi-bin", + help="CGI script directory, relative to the server's root directory", + default="cgi-bin", + metavar="DIR", ) parser.add_argument( - "--index-file", help="The gemini directory index file", default="index.gmi" + "--index-file", + help="If a directory contains a file with this name, that file will be " + "served instead of auto-generating an index page", + default="index.gmi", + metavar="FILE", ) args = parser.parse_args()