Jetforce


NameJetforce JSON
Version 0.10.1 PyPI version JSON
download
home_pagehttps://github.com/michael-lazar/jetforce
SummaryAn Experimental Gemini Server
upload_time2023-10-16 13:17:37
maintainer
docs_urlNone
authorMichael Lazar
requires_python>=3.7
licenseOther/Proprietary License
keywords gemini server tcp gopher asyncio
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Jetforce

An experimental TCP server for the new, under development Gemini Protocol.
Learn more about Gemini [here](https://portal.mozz.us/).

![Rocket Launch](logo.jpg)

[![pypi](https://img.shields.io/pypi/v/jetforce.svg?label=version)](https://pypi.python.org/pypi/jetforce/)
[![version](https://img.shields.io/badge/python-3.7+-blue.svg)](https://pypi.python.org/pypi/jetforce/)
[![build](https://github.com/michael-lazar/jetforce/workflows/Jetforce/badge.svg)](https://github.com/michael-lazar/jetforce/actions?query=workflow%3AJetforce)

## Table of Contents

* [Features](#features)
* [Installation](#installation)
* [Usage](#usage)
* [Deployment](#deployment)
* [Releases](#releases)
* [License](#license)

## Features

- A built-in static file server with support for gemini directories and CGI scripts.
- A complete framework for writing robust gemini applications like [astrobotany](https://github.com/michael-lazar/astrobotany).
- A lean, modern codebase with type hints and black formatting.
- A solid foundation built on top of the [twisted](https://twistedmatrix.com/trac/) asynchronous networking engine.

## Installation

Requires Python 3.7 or newer.

The latest stable release can be installed from [PyPI](https://pypi.org/project/Jetforce/):

```bash
$ pip install jetforce
```

Or, install from source:

```bash
$ git clone https://github.com/michael-lazar/jetforce
$ cd jetforce
$ pip install .
```

Or, install into a python virtual environment:

```bash
# Create a project directory somewhere
$ mkdir /opt/jetforce

# Activate a virtual environment and install jetforce
$ python3 -m virtualenv /opt/jetforce/venv
$ source /opt/jetforce/venv/bin/activate
$ pip install jetforce

# The launch script will be installed here
$ /opt/jetforce/venv/bin/jetforce
```

## Usage

Use the ``--help`` flag to view command-line options:

```bash
usage: jetforce [-h] [-V] [--host HOST] [--port PORT] [--hostname HOSTNAME]
                [--tls-certfile FILE] [--tls-keyfile FILE] [--tls-cafile FILE]
                [--tls-capath DIR] [--dir DIR] [--cgi-dir DIR] [--index-file FILE]
                [--default-lang DEFAULT_LANG] [--rate-limit RATE_LIMIT]

An Experimental Gemini Protocol Server

optional arguments:
  -h, --help            show this help message and exit
  -V, --version         show program's version number and exit

server configuration:
  --host HOST           Server address to bind to (default: 127.0.0.1)
  --port PORT           Server port to bind to (default: 1965)
  --hostname HOSTNAME   Server hostname (default: localhost)
  --tls-certfile FILE   Server TLS certificate file (default: None)
  --tls-keyfile FILE    Server TLS private key file (default: None)
  --tls-cafile FILE     A CA file to use for validating clients (default: None)
  --tls-capath DIR      A directory containing CA files for validating clients (default:
                        None)

fileserver configuration:
  --dir DIR             Root directory on the filesystem 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)
  --default-lang DEFAULT_LANG
                        A lang parameter that will be used for all text/gemini responses
                        (default: None)
  --rate-limit RATE_LIMIT
                        Enable IP rate limiting, e.g. '60/5m' (60 requests per 5 minutes)
                        (default: None)
```

### Setting the ``hostname``

The server's hostname should be set to the *DNS* name that you expect to
receive traffic from. For example, if your jetforce server is running on
"gemini://cats.com", you should set the hostname to "cats.com". Any URLs
that do not match this hostname will be refused by the server, including
URLs that use a direct IP address such as "gemini://174.138.124.169".

IDNs (domain names that contain unicode characters) should be defined using
their ASCII punycode representation. For example, the domain name
*café.mozz.us* should be represented as ``--hostname xn--caf-dma.mozz.us``.

### Setting the ``host``

The server's host should be set to the local socket that you want to
bind to:

- ``--host "127.0.0.1"`` - Accept local connections only
- ``--host "0.0.0.0"`` - Accept remote connections over IPv4
- ``--host "::"`` - Accept remote connections over IPv6
- ``--host ""`` - Accept remote connections over any interface (IPv4 + IPv6)

### TLS Certificates

The gemini specification *requires* that all connections be sent over TLS.

If you do not provide a TLS certificate file using the ``--tls-certfile`` flag,
jetforce will automatically generate a temporary cert for you to use. This is
great for making development easier, but before you expose your server to the
public internet you should setup something more permanent. You can generate
your own self-signed server certificate, or obtain one from a Certificate
Authority like [Let's Encrypt](https://letsencrypt.org).

Here's an example `openssl` command that you can use to generate a self-signed certificate:

```
$ openssl req -newkey rsa:2048 -nodes -keyout {hostname}.key \
    -nodes -x509 -out {hostname}.crt -subj "/CN={hostname}"
```

Jetforce also supports TLS client certificates (both self-signed and CA authorised).
Requests that are made with client certificates will include additional
CGI/environment variables with information about the TLS connection.

You can specify a CA for client validation with the ``--tls-cafile`` or ``--tls-capath``
flags. Connections validated by the CA will have the ``TLS_CLIENT_AUTHORISED`` environment
variable set to True. Instructions on how to generate CA's are outside of the scope of
this readme, but you can find many helpful tutorials
[online](https://www.makethenmakeinstall.com/2014/05/ssl-client-authentication-step-by-step/).

### Static Files

Jetforce will serve static files in the ``/var/gemini/`` directory by default.
Files ending with ***.gmi** will be interpreted as the *text/gemini* type. If
a directory is requested, jetforce will look for a file named **index.gmi** in that
directory to return. Otherwise, a directory file listing will be automatically
generated.

### Virtual Hosting

For the sake of keeping the command line arguments straightforward and easy
to understand, configuring virtual hosting is not supported via the command
line. However, it is readily available using only a few lines of python and a
custom launch script. Check out [examples/vhost.py](examples/vhost.py) for more
information.

Jetforce does not (yet) support virtual hosting at the TLS-layer using SNI.
This means that you cannot return different server TLS certificates for
different domains. The suggested workaround is to use a single certificate with
multiple ``subjectAltName`` attributes. There is also an
[sni_callback()](https://github.com/michael-lazar/jetforce/blob/9ac80a986c6ed8a62951c857315ca04b6d127c32/jetforce/tls.py#L140)
hook in the server codebase that can be subclassed to implement custom TLS
behavior.

### CGI

Jetforce supports a simplified version of CGI scripting. It doesn't
exactly follow the [RFC 3875](https://tools.ietf.org/html/rfc3875)
specification for CGI, but it gets the job done for the purposes of Gemini.

Any executable file placed in the server's ``cgi-bin/`` directory will be
considered a CGI script. When a CGI script is requested by a gemini client,
the jetforce server will execute the script and pass along information about
the request using environment variables.

The CGI script must then write the gemini response to the *stdout* stream.
This includes the status code and meta string on the first line, and the
optional response body on subsequent lines. The bytes generated by the
CGI script will be forwarded *verbatim* to the gemini client, without any
additional modification by the server.

#### CGI Environment Variables

| Name | Example Value |
| --- | --- |
| GATEWAY_INTERFACE | ``CGI/1.1`` (for compatibility with RFC 3875) |
| SERVER_PROTOCOL | ``GEMINI`` |
| SERVER_SOFTWARE | ``jetforce/0.0.7`` |
| GEMINI_URL | ``gemini://mozz.us/cgi-bin/example.cgi/extra?hello%20world`` |
| SCRIPT_NAME | ``/cgi-bin/example.cgi`` |
| PATH_INFO | ``/extra`` |
| QUERY_STRING | ``hello%20world`` |
| SERVER_NAME | ``mozz.us`` |
| SERVER_PORT | ``1965`` |
| REMOTE_HOST | ``10.10.0.2`` |
| REMOTE_ADDR | ``10.10.0.2`` |
| TLS_CIPHER | ``TLS_AES_256_GCM_SHA384`` |
| TLS_VERSION | ``TLSv1.3`` |


#### CGI Environment Variables - Authenticated

Additional CGI variables will be included only when the client connection uses a TLS client certificate:

| Name | Example Value |
| --- | --- |
| AUTH_TYPE | ``CERTIFICATE`` |
| REMOTE_USER | ``mozz123`` (the certificate subject CN attribute) |
| TLS_CLIENT_HASH | ``SHA256:86341FB480BFE333C343530D75ABF99D1437F69338F36C684C8831B63C993A96`` |
| TLS_CLIENT_NOT_BEFORE | ``2020-04-05T04:18:22Z`` |
| TLS_CLIENT_NOT_AFTER | ``2021-04-05T04:18:22Z`` |
| TLS_CLIENT_SERIAL_NUMBER | ``73629018972631`` |
| TLS_CLIENT_AUTHORISED | ``0`` (not authorised) / ``1`` (authorised) † |

† Requires the server to be configured with a CA for validating client certificates.

## Deployment

Jetforce is intended to be run behind a process manager that handles
*daemonizing* the script, redirecting output to system logs, etc. I prefer
to use [systemd](https://www.freedesktop.org/wiki/Software/systemd/) for this
because it's installed on my operating system and easy to set up.

Here's how I configure my server over at **gemini://mozz.us**:

```
# /etc/systemd/system/jetforce.service
[Unit]
Description=Jetforce Server

[Service]
Type=simple
Restart=always
RestartSec=5
Environment="PYTHONUNBUFFERED=1"
ExecStart=/usr/local/bin/jetforce \
    --host 0.0.0.0 \
    --port 1965 \
    --hostname mozz.us \
    --dir /var/gemini \
    --tls-certfile /etc/letsencrypt/live/mozz.us/fullchain.pem \
    --tls-keyfile /etc/letsencrypt/live/mozz.us/privkey.pem \
    --tls-cafile /etc/pki/tls/jetforce_client/ca.cer

[Install]
WantedBy=default.target
```

- ``--host 0.0.0.0`` allows the server to accept external connections from any
  IP address over IPv4.
- ``PYTHONUNBUFFERED=1`` disables buffering `stderr` and is sometimes necessary
  for logging to work.
- ``--tls-certfile`` and ``--tls-keyfile`` point to my WWW server's Let's Encrypt
  certificate chain.
- ``--tls-cafile`` points to a self-signed CA that I created in order to test
  accepting client TLS connections.

With this service installed, I can start and stop the server using

```
systemctl start jetforce
systemctl stop jetforce
```

And I can view the server logs using

```
journalctl -u jetforce -f
```

*WARNING*

You are exposing a server to the internet. You (yes you!) are responsible for
securing your server and setting up appropriate access permissions. This likely means
*not* running jetforce as the root user. Security best practices are outside of the
scope of this document and largely depend on your individual threat model.

## Releases

To view the project's release history, see the [CHANGELOG](CHANGELOG.md) file.

## License

This project is licensed under the [Floodgap Free Software License](https://www.floodgap.com/software/ffsl/license.html).

> The Floodgap Free Software License (FFSL) has one overriding mandate: that software
> using it, or derivative works based on software that uses it, must be free. By free
> we mean simply "free as in beer" -- you may put your work into open or closed source
> packages as you see fit, whether or not you choose to release your changes or updates
> publicly, but you must not ask any fee for it.

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/michael-lazar/jetforce",
    "name": "Jetforce",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.7",
    "maintainer_email": "",
    "keywords": "gemini server tcp gopher asyncio",
    "author": "Michael Lazar",
    "author_email": "lazar.michael22@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/ed/cf/c7f09dbb8e78b87e60d2ca0b4669980fa763bf0045b39563f4bd11008ef1/Jetforce-0.10.1.tar.gz",
    "platform": null,
    "description": "# Jetforce\n\nAn experimental TCP server for the new, under development Gemini Protocol.\nLearn more about Gemini [here](https://portal.mozz.us/).\n\n![Rocket Launch](logo.jpg)\n\n[![pypi](https://img.shields.io/pypi/v/jetforce.svg?label=version)](https://pypi.python.org/pypi/jetforce/)\n[![version](https://img.shields.io/badge/python-3.7+-blue.svg)](https://pypi.python.org/pypi/jetforce/)\n[![build](https://github.com/michael-lazar/jetforce/workflows/Jetforce/badge.svg)](https://github.com/michael-lazar/jetforce/actions?query=workflow%3AJetforce)\n\n## Table of Contents\n\n* [Features](#features)\n* [Installation](#installation)\n* [Usage](#usage)\n* [Deployment](#deployment)\n* [Releases](#releases)\n* [License](#license)\n\n## Features\n\n- A built-in static file server with support for gemini directories and CGI scripts.\n- A complete framework for writing robust gemini applications like [astrobotany](https://github.com/michael-lazar/astrobotany).\n- A lean, modern codebase with type hints and black formatting.\n- A solid foundation built on top of the [twisted](https://twistedmatrix.com/trac/) asynchronous networking engine.\n\n## Installation\n\nRequires Python 3.7 or newer.\n\nThe latest stable release can be installed from [PyPI](https://pypi.org/project/Jetforce/):\n\n```bash\n$ pip install jetforce\n```\n\nOr, install from source:\n\n```bash\n$ git clone https://github.com/michael-lazar/jetforce\n$ cd jetforce\n$ pip install .\n```\n\nOr, install into a python virtual environment:\n\n```bash\n# Create a project directory somewhere\n$ mkdir /opt/jetforce\n\n# Activate a virtual environment and install jetforce\n$ python3 -m virtualenv /opt/jetforce/venv\n$ source /opt/jetforce/venv/bin/activate\n$ pip install jetforce\n\n# The launch script will be installed here\n$ /opt/jetforce/venv/bin/jetforce\n```\n\n## Usage\n\nUse the ``--help`` flag to view command-line options:\n\n```bash\nusage: jetforce [-h] [-V] [--host HOST] [--port PORT] [--hostname HOSTNAME]\n                [--tls-certfile FILE] [--tls-keyfile FILE] [--tls-cafile FILE]\n                [--tls-capath DIR] [--dir DIR] [--cgi-dir DIR] [--index-file FILE]\n                [--default-lang DEFAULT_LANG] [--rate-limit RATE_LIMIT]\n\nAn Experimental Gemini Protocol Server\n\noptional arguments:\n  -h, --help            show this help message and exit\n  -V, --version         show program's version number and exit\n\nserver configuration:\n  --host HOST           Server address to bind to (default: 127.0.0.1)\n  --port PORT           Server port to bind to (default: 1965)\n  --hostname HOSTNAME   Server hostname (default: localhost)\n  --tls-certfile FILE   Server TLS certificate file (default: None)\n  --tls-keyfile FILE    Server TLS private key file (default: None)\n  --tls-cafile FILE     A CA file to use for validating clients (default: None)\n  --tls-capath DIR      A directory containing CA files for validating clients (default:\n                        None)\n\nfileserver configuration:\n  --dir DIR             Root directory on the filesystem to serve (default: /var/gemini)\n  --cgi-dir DIR         CGI script directory, relative to the server's root directory\n                        (default: cgi-bin)\n  --index-file FILE     If a directory contains a file with this name, that file will be\n                        served instead of auto-generating an index page (default: index.gmi)\n  --default-lang DEFAULT_LANG\n                        A lang parameter that will be used for all text/gemini responses\n                        (default: None)\n  --rate-limit RATE_LIMIT\n                        Enable IP rate limiting, e.g. '60/5m' (60 requests per 5 minutes)\n                        (default: None)\n```\n\n### Setting the ``hostname``\n\nThe server's hostname should be set to the *DNS* name that you expect to\nreceive traffic from. For example, if your jetforce server is running on\n\"gemini://cats.com\", you should set the hostname to \"cats.com\". Any URLs\nthat do not match this hostname will be refused by the server, including\nURLs that use a direct IP address such as \"gemini://174.138.124.169\".\n\nIDNs (domain names that contain unicode characters) should be defined using\ntheir ASCII punycode representation. For example, the domain name\n*caf\u00e9.mozz.us* should be represented as ``--hostname xn--caf-dma.mozz.us``.\n\n### Setting the ``host``\n\nThe server's host should be set to the local socket that you want to\nbind to:\n\n- ``--host \"127.0.0.1\"`` - Accept local connections only\n- ``--host \"0.0.0.0\"`` - Accept remote connections over IPv4\n- ``--host \"::\"`` - Accept remote connections over IPv6\n- ``--host \"\"`` - Accept remote connections over any interface (IPv4 + IPv6)\n\n### TLS Certificates\n\nThe gemini specification *requires* that all connections be sent over TLS.\n\nIf you do not provide a TLS certificate file using the ``--tls-certfile`` flag,\njetforce will automatically generate a temporary cert for you to use. This is\ngreat for making development easier, but before you expose your server to the\npublic internet you should setup something more permanent. You can generate\nyour own self-signed server certificate, or obtain one from a Certificate\nAuthority like [Let's Encrypt](https://letsencrypt.org).\n\nHere's an example `openssl` command that you can use to generate a self-signed certificate:\n\n```\n$ openssl req -newkey rsa:2048 -nodes -keyout {hostname}.key \\\n    -nodes -x509 -out {hostname}.crt -subj \"/CN={hostname}\"\n```\n\nJetforce also supports TLS client certificates (both self-signed and CA authorised).\nRequests that are made with client certificates will include additional\nCGI/environment variables with information about the TLS connection.\n\nYou can specify a CA for client validation with the ``--tls-cafile`` or ``--tls-capath``\nflags. Connections validated by the CA will have the ``TLS_CLIENT_AUTHORISED`` environment\nvariable set to True. Instructions on how to generate CA's are outside of the scope of\nthis readme, but you can find many helpful tutorials\n[online](https://www.makethenmakeinstall.com/2014/05/ssl-client-authentication-step-by-step/).\n\n### Static Files\n\nJetforce will serve static files in the ``/var/gemini/`` directory by default.\nFiles ending with ***.gmi** will be interpreted as the *text/gemini* type. If\na directory is requested, jetforce will look for a file named **index.gmi** in that\ndirectory to return. Otherwise, a directory file listing will be automatically\ngenerated.\n\n### Virtual Hosting\n\nFor the sake of keeping the command line arguments straightforward and easy\nto understand, configuring virtual hosting is not supported via the command\nline. However, it is readily available using only a few lines of python and a\ncustom launch script. Check out [examples/vhost.py](examples/vhost.py) for more\ninformation.\n\nJetforce does not (yet) support virtual hosting at the TLS-layer using SNI.\nThis means that you cannot return different server TLS certificates for\ndifferent domains. The suggested workaround is to use a single certificate with\nmultiple ``subjectAltName`` attributes. There is also an\n[sni_callback()](https://github.com/michael-lazar/jetforce/blob/9ac80a986c6ed8a62951c857315ca04b6d127c32/jetforce/tls.py#L140)\nhook in the server codebase that can be subclassed to implement custom TLS\nbehavior.\n\n### CGI\n\nJetforce supports a simplified version of CGI scripting. It doesn't\nexactly follow the [RFC 3875](https://tools.ietf.org/html/rfc3875)\nspecification for CGI, but it gets the job done for the purposes of Gemini.\n\nAny executable file placed in the server's ``cgi-bin/`` directory will be\nconsidered a CGI script. When a CGI script is requested by a gemini client,\nthe jetforce server will execute the script and pass along information about\nthe request using environment variables.\n\nThe CGI script must then write the gemini response to the *stdout* stream.\nThis includes the status code and meta string on the first line, and the\noptional response body on subsequent lines. The bytes generated by the\nCGI script will be forwarded *verbatim* to the gemini client, without any\nadditional modification by the server.\n\n#### CGI Environment Variables\n\n| Name | Example Value |\n| --- | --- |\n| GATEWAY_INTERFACE | ``CGI/1.1`` (for compatibility with RFC 3875) |\n| SERVER_PROTOCOL | ``GEMINI`` |\n| SERVER_SOFTWARE | ``jetforce/0.0.7`` |\n| GEMINI_URL | ``gemini://mozz.us/cgi-bin/example.cgi/extra?hello%20world`` |\n| SCRIPT_NAME | ``/cgi-bin/example.cgi`` |\n| PATH_INFO | ``/extra`` |\n| QUERY_STRING | ``hello%20world`` |\n| SERVER_NAME | ``mozz.us`` |\n| SERVER_PORT | ``1965`` |\n| REMOTE_HOST | ``10.10.0.2`` |\n| REMOTE_ADDR | ``10.10.0.2`` |\n| TLS_CIPHER | ``TLS_AES_256_GCM_SHA384`` |\n| TLS_VERSION | ``TLSv1.3`` |\n\n\n#### CGI Environment Variables - Authenticated\n\nAdditional CGI variables will be included only when the client connection uses a TLS client certificate:\n\n| Name | Example Value |\n| --- | --- |\n| AUTH_TYPE | ``CERTIFICATE`` |\n| REMOTE_USER | ``mozz123`` (the certificate subject CN attribute) |\n| TLS_CLIENT_HASH | ``SHA256:86341FB480BFE333C343530D75ABF99D1437F69338F36C684C8831B63C993A96`` |\n| TLS_CLIENT_NOT_BEFORE | ``2020-04-05T04:18:22Z`` |\n| TLS_CLIENT_NOT_AFTER | ``2021-04-05T04:18:22Z`` |\n| TLS_CLIENT_SERIAL_NUMBER | ``73629018972631`` |\n| TLS_CLIENT_AUTHORISED | ``0`` (not authorised) / ``1`` (authorised) \u2020 |\n\n\u2020 Requires the server to be configured with a CA for validating client certificates.\n\n## Deployment\n\nJetforce is intended to be run behind a process manager that handles\n*daemonizing* the script, redirecting output to system logs, etc. I prefer\nto use [systemd](https://www.freedesktop.org/wiki/Software/systemd/) for this\nbecause it's installed on my operating system and easy to set up.\n\nHere's how I configure my server over at **gemini://mozz.us**:\n\n```\n# /etc/systemd/system/jetforce.service\n[Unit]\nDescription=Jetforce Server\n\n[Service]\nType=simple\nRestart=always\nRestartSec=5\nEnvironment=\"PYTHONUNBUFFERED=1\"\nExecStart=/usr/local/bin/jetforce \\\n    --host 0.0.0.0 \\\n    --port 1965 \\\n    --hostname mozz.us \\\n    --dir /var/gemini \\\n    --tls-certfile /etc/letsencrypt/live/mozz.us/fullchain.pem \\\n    --tls-keyfile /etc/letsencrypt/live/mozz.us/privkey.pem \\\n    --tls-cafile /etc/pki/tls/jetforce_client/ca.cer\n\n[Install]\nWantedBy=default.target\n```\n\n- ``--host 0.0.0.0`` allows the server to accept external connections from any\n  IP address over IPv4.\n- ``PYTHONUNBUFFERED=1`` disables buffering `stderr` and is sometimes necessary\n  for logging to work.\n- ``--tls-certfile`` and ``--tls-keyfile`` point to my WWW server's Let's Encrypt\n  certificate chain.\n- ``--tls-cafile`` points to a self-signed CA that I created in order to test\n  accepting client TLS connections.\n\nWith this service installed, I can start and stop the server using\n\n```\nsystemctl start jetforce\nsystemctl stop jetforce\n```\n\nAnd I can view the server logs using\n\n```\njournalctl -u jetforce -f\n```\n\n*WARNING*\n\nYou are exposing a server to the internet. You (yes you!) are responsible for\nsecuring your server and setting up appropriate access permissions. This likely means\n*not* running jetforce as the root user. Security best practices are outside of the\nscope of this document and largely depend on your individual threat model.\n\n## Releases\n\nTo view the project's release history, see the [CHANGELOG](CHANGELOG.md) file.\n\n## License\n\nThis project is licensed under the [Floodgap Free Software License](https://www.floodgap.com/software/ffsl/license.html).\n\n> The Floodgap Free Software License (FFSL) has one overriding mandate: that software\n> using it, or derivative works based on software that uses it, must be free. By free\n> we mean simply \"free as in beer\" -- you may put your work into open or closed source\n> packages as you see fit, whether or not you choose to release your changes or updates\n> publicly, but you must not ask any fee for it.\n",
    "bugtrack_url": null,
    "license": "Other/Proprietary License",
    "summary": "An Experimental Gemini Server",
    "version": "0.10.1",
    "project_urls": {
        "Homepage": "https://github.com/michael-lazar/jetforce"
    },
    "split_keywords": [
        "gemini",
        "server",
        "tcp",
        "gopher",
        "asyncio"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "bb8d55c7a593b1d666d4d0f752e5b145ff04b99cd639cfcb6df62d7734998ae0",
                "md5": "3e8886bcf9e357b1835be5446e80496a",
                "sha256": "6c683fa13258d3976b9032a248962e50531a3787d80fbc005a346cf1a6bd2b7b"
            },
            "downloads": -1,
            "filename": "Jetforce-0.10.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "3e8886bcf9e357b1835be5446e80496a",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.7",
            "size": 31882,
            "upload_time": "2023-10-16T13:17:36",
            "upload_time_iso_8601": "2023-10-16T13:17:36.071695Z",
            "url": "https://files.pythonhosted.org/packages/bb/8d/55c7a593b1d666d4d0f752e5b145ff04b99cd639cfcb6df62d7734998ae0/Jetforce-0.10.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "edcfc7f09dbb8e78b87e60d2ca0b4669980fa763bf0045b39563f4bd11008ef1",
                "md5": "12ad7275380da20e9bcf83b4b9cddb9c",
                "sha256": "1a8a8520da1dce691431dfa05bbdca22ef21932303f2a64624293af3aefea1b0"
            },
            "downloads": -1,
            "filename": "Jetforce-0.10.1.tar.gz",
            "has_sig": false,
            "md5_digest": "12ad7275380da20e9bcf83b4b9cddb9c",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.7",
            "size": 35041,
            "upload_time": "2023-10-16T13:17:37",
            "upload_time_iso_8601": "2023-10-16T13:17:37.502556Z",
            "url": "https://files.pythonhosted.org/packages/ed/cf/c7f09dbb8e78b87e60d2ca0b4669980fa763bf0045b39563f4bd11008ef1/Jetforce-0.10.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-10-16 13:17:37",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "michael-lazar",
    "github_project": "jetforce",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "jetforce"
}
        
Elapsed time: 1.25538s