diff options
author | Starfall <us@starfall.systems> | 2023-12-05 09:32:03 -0600 |
---|---|---|
committer | Starfall <us@starfall.systems> | 2023-12-05 09:35:14 -0600 |
commit | cd5699fc227d13896ea88239fd8fe1e8396cca20 (patch) | |
tree | 47c6e13164ec16de44dc2bb215218f3bc6bfd901 /ophiculus | |
parent | f69526d8b85d408124fc268ce4e8d06f9db82089 (diff) |
ophiculus: abandoned Gemini client written in python
Diffstat (limited to 'ophiculus')
-rwxr-xr-x | ophiculus/README.md | 91 | ||||
-rwxr-xr-x | ophiculus/ophiculus.py | 60 |
2 files changed, 151 insertions, 0 deletions
diff --git a/ophiculus/README.md b/ophiculus/README.md new file mode 100755 index 0000000..6763af9 --- /dev/null +++ b/ophiculus/README.md @@ -0,0 +1,91 @@ +Ophiculus + +Gemini client in Python + +Notes on spec v0.14.2 / 2020-07-02 + +One, single-request, transaction type. + C: connect + S: accept + <TLS handshake> + C: Validate server cert + C: send request + S: send response header, close connection here for failures + S: send response body + S: close connection + C: handle response + +URI scheme: RFC3986, authority required but userinfo disallowed, host required, port defaults to 1965 +spaces as %20 not + + +THE REQUEST: <URL><CR><LF> +URL in utf-8, 1024 bytes, always absolute, gemini:// scheme optional + +RESPONSE HEADER: <STATUS><SPACE><META><CR><LF> +STATUS: 2 digits +META: UTF-8, 1024 bytes + +STATUS CODES +1x INPUT +Server is requesting a query parameter. Show META to the user as the prompt, request again as a query parameter. +2x SUCCESS +Response body to follow. META is mime type +3x REDIRECT +Temporary redirect to META +4x TEMPORARY FAILURE +META probably contains more information, show it +5x PERMANENT FAILURE +META probably contains more information, show it. Do not repeat this exact request. +6x CLIENT CERTIFICATE REQUIRED +Your certificate was not accepted or you forgot it. Try again with a different one. META may be useful. + +RESPONSE BODIES +Only for 2x statuses, META is MIME type per RFC 2046. Default to "text/gemini; charset=utf-8"; assume UTF-8 if not specified for text/. LF is allowed instead of CRLF to end lines in text/. + +TLS +TLS 1.2+ is required, TLS 1.3 is SHOULD. +Trust On First Use is recommended, cache self-signed certs. +some requests will require client certs - on-demand or longer-lived. server caches the hash but client controls when it can be deleted +client certs are scoped to that hostname, and that path & below. example.com/foo -> example.com/foo/bar but not the top level? + +text/gemini MIME type: + has charset, default UTF-8 + has lang parameter, values RFC4646, do not assume a default + line oriented +CORE LINE TYPES + text: default case, use as you will. do not collapse blank lines. SHOULD wrap to fit, MUST NOT combine + link: => URL FRIENDLY-LINK-NAME. any amount of whitespace. MUST NOT automatically make network connections + preformating toggle: ```. further text to be interpreted as alt text, e.g. for caption or screen reader or syntax highlighting + preformatted lines (between preformat toggles) +ADVANCED LINE TYPES + headings: #, ##, ###. + unordered list: "* ". style only, basically. + quotes: ">". + +EXTENSION STATUS CODES +10 INPUT +11 SENSITIVE INPUT (e.g. passwords), client should hide input field +20 SUCCESS +30 TEMPORARY REDIRECT +31 PERMANENT REDIRECT +40 TEMPORARY FAILURE +41 SERVER UNAVAILABLE +42 CGI ERROR (dynamic content failed) +43 PROXY ERROR +44 SLOW DOWN (rate limited for META seconds) +50 PERMANENT FAILURE +51 NOT FOUND +52 GONE +53 PROXY REQUREST REFUSED (wrong domain) +59 BAD REQUEST +60 CLIENT CERTIFICATE REQUIRED +61 CERTIFICATE NOT AUTHORIZED (for this resource, at least) +62 CERTIFICATE NOT VALID (your problem) + + + + +CLIENT RECOMMENDATIONS +- follow no more than 5 redirects in a row +- handle cross protocol redirects +- TLS 1.2 ciphers: only DGE ECDHE for key agreement, AES or ChaCha20 for bulk cipers, SHA2/SHA3 hashes \ No newline at end of file diff --git a/ophiculus/ophiculus.py b/ophiculus/ophiculus.py new file mode 100755 index 0000000..af145e8 --- /dev/null +++ b/ophiculus/ophiculus.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +import socket +import ssl +import sys +import urllib.parse + +# read target url from arguments +arg = sys.argv[1] +if '//' not in arg: + arg = '//' + arg + +url = urllib.parse.urlparse(arg, scheme='gemini') +if url.scheme != 'gemini': + sys.exit('Unable to handle scheme') + +port = url.port if url.port is not None else 1965 +context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) +context.options |= OP_NO_TLSv1 | OP_NO_TLSV1_1 | OP_NO_TLSv1_2 +# ignore certs for now, TODO actually handle cert TOFU +context.check_hostname = False +context.verify_mode = ssl.CERT_NONE + +# TODO AF_INET6 support +with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + with context.wrap_socket(sock, server_hostname=url.hostname) as ssock: + # connect to server [TLS handshake occurs after connect() by default] + ip = socket.gethostbyname(url.hostname) + ssock.connect((ip, port)) + ssock.sendall(urlparse.urlunsplit(url) + '\r\n') + + with ssock.makefile() as io: + header = io.readline() + print(header) + status, meta = header.split(' ', 1) + switch = status[0] + if switch == '1': + query = urllib.parse.quote(input('server requesting input: ' + meta)) + # TODO retry request + else if switch == '2': + # meta is mime type + # TODO handle text/gemini i guess? + if meta.startswith('text/'): + body = io.read() + for line in body.spiltlines(): + print(line) + else: + # TODO download file instead + sys.exit('didn\'t get a text file') + else if switch == '3': + print('temporary redirect to: ' + meta) + # TODO prompt to follow + else if switch == '4': + print('temporary failure: ' + meta) + else if switch == '5': + print('permanent failure: ' + meta) + else if switch == '6': + print('client certificate required: ' + meta) + # TODO prompt to retry + else: + print('unknown response: ' + meta) |