about summary refs log tree commit diff
path: root/ophiculus
diff options
context:
space:
mode:
authorStarfall <us@starfall.systems>2023-12-05 09:32:03 -0600
committerStarfall <us@starfall.systems>2023-12-05 09:35:14 -0600
commitcd5699fc227d13896ea88239fd8fe1e8396cca20 (patch)
tree47c6e13164ec16de44dc2bb215218f3bc6bfd901 /ophiculus
parentf69526d8b85d408124fc268ce4e8d06f9db82089 (diff)
ophiculus: abandoned Gemini client written in python
Diffstat (limited to 'ophiculus')
-rwxr-xr-xophiculus/README.md91
-rwxr-xr-xophiculus/ophiculus.py60
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)