Gemlog Booster [main]

Initial commit

=> 27b00bc437c8b54318c0cf2f5fc9444acb684d68

diff --git a/booster.py b/booster.py
new file mode 100755
index 0000000..9164cf5
--- /dev/null
+++ b/booster.py
@@ -0,0 +1,192 @@
+#!/usr/bin/env python3
+
+##==========================================================================##
+##                                                                          ##
+##                             --= BOOSTER --=                              ##
+##                         Gemlog Admin Interface                           ##
+##                                                                          ##
+##==========================================================================##
+
+# Copyright (c) 2021 Jaakko Keränen 
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+#    this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import os, sys, json, subprocess
+import socket, ssl, OpenSSL.crypto, hashlib
+from urllib.parse import urlparse
+pjoin = os.path.join
+
+#----------------------------------------------------------------------------#
+#                               CONFIGURATION                                #
+#----------------------------------------------------------------------------#
+
+HOME = os.getenv('HOME')
+CFG_PATH = pjoin(HOME, '.booster/config.json')
+if not os.path.exists(CFG_PATH):
+    print(f'ERROR: Configuration file {CFG_PATH} not found.')
+    sys.exit(1)
+CONFIG = json.loads(open(CFG_PATH, 'rt').read())
+if not CONFIG['root'].endswith('/'):
+    CONFIG['root'] = CONFIG['root'] + '/'
+print(json.dumps(CONFIG, indent=2))
+
+#----------------------------------------------------------------------------#
+#                             REQUEST HANDLING                               #
+#----------------------------------------------------------------------------#
+
+def report_error(stream, code, msg):
+    stream.sendall(f'{code} {msg}\r\n'.encode('utf-8'))
+    
+def run_command(args):
+    try:
+        out = subprocess.check_output(args).decode('utf-8').strip()
+    except subprocess.CalledProcessError as er:
+        out = b''
+        if er.stdout: out += er.stdout + b'\n'
+        if er.stderr: out += er.stderr + b'\n'
+        out = out.decode('utf-8')
+    if len(out):
+        return '```\n' + out + '\n```\n'
+    return ''
+    
+def urlenc(q):
+    q = q.replace(' ', '%20')
+    q = q.replace(':', '%3A')
+    q = q.replace('/', '%2F')
+    return q    
+
+def handle_client(stream):    
+    data = bytes()
+    incoming = stream.recv(1024)
+    #der = stream.getpeercert(binary_form=True)
+    #print('DER:', der)
+    #cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_ASN1, der)
+    #print(cert)
+    #pkey = cert.get_pubkey()
+    #m = hashlib.sha256()
+    #m.update(OpenSSL.crypto.dump_publickey(OpenSSL.crypto.FILETYPE_ASN1, pkey))
+    #m.update(der)
+    #print(m.hexdigest())
+    # empty data means the client is finished with us
+    expected_size = -1
+    req_token = None
+    req_mime = 'application/octet-stream'
+    while incoming:
+        data += incoming
+        # Need to check if there's a titan:// request here.
+        if expected_size < 0:
+            if len(data) >= 6 and data[:6] != b'titan:':
+                stream.sendall(b'59 Only Titan requests are supported\r\n')
+                break
+            crlf_pos = data.find(b'\r\n')
+            if crlf_pos >= 0:
+                # Parse the request.
+                request = data[:crlf_pos].decode('utf-8')
+                print(request)
+                parms = request.split(';')
+                req_url = parms[0]
+                for parm in parms:
+                    if parm.startswith('size='):
+                        expected_size = int(parm[5:])
+                    elif parm.startswith('token='):
+                        req_token = parm[6:]
+                    elif parm.startswith('mime='):
+                        req_mime = parm[5:]
+                data = data[crlf_pos + 2:]
+        if len(data) >= expected_size:
+            break
+        incoming = stream.recv(1024)
+    # process the request
+    print(f'URL   : {req_url}')
+    print(f'Token : {req_token}')
+    print(f'MIME  : {req_mime}')
+    print(f'Data  : {len(data)} bytes')
+    parts = urlparse(req_url)
+    path = parts.path
+    if not path.startswith(CONFIG['root']):
+        report_error(stream, 51, "invalid path")
+        return
+    path = path[len(CONFIG['root']):]
+    if path.startswith('.'):
+        report_error(stream, 61, "access is not authorized")
+        return
+    file_path = pjoin(CONFIG['file_root'], path)
+    view_url = '%s/%s%s' % (CONFIG['site_url'], CONFIG['root'], path)
+    response = '# Booster log\n'
+    is_new = not os.path.exists(file_path)
+    GIT = CONFIG['git']
+    if not is_new and req_token == 'DELETE':
+        response += f'* deleting file: {file_path}\n'
+        os.remove(file_path)
+        git_msg = GIT['message_prefix'] + 'Removed ' + path
+    else:
+        if is_new:
+            response += f'* creating a new file: {file_path}\n'
+            git_msg = GIT['message_prefix'] + 'Added ' + path
+        else:
+            response += f'* updating file: {file_path}\n'
+            git_msg = GIT['message_prefix'] + 'Updated ' + path
+        # write the data
+        response += f'* writing %d bytes\n' % len(data)
+        dst = open(file_path, 'wb')
+        dst.write(data)
+        dst.close()
+    # update indices
+    os.chdir(CONFIG['file_root'])
+    response += f'* updating indices\n'
+    response += run_command([CONFIG['python'], ".makeindex.py"])
+    response += '* committing changes to Git repository\n'
+    if is_new:
+        response += run_command([GIT['exec'], 'add', file_path])
+    response += run_command([GIT['exec'], 'commit', '-a', '-m', git_msg])
+    response += "\n"
+    response += f"=> {view_url} View the page\n"
+    response += f"=> gemini://warmedal.se/~antenna/submit?%s Notify Antenna" %\
+        urlenc('%s/%s' % (CONFIG['site_url'], CONFIG['root']))
+    # compose a response
+    stream.sendall(('20 text/gemini; charset=utf-8\r\n%s' %
+                     response).encode('utf-8'))
+
+context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
+context.load_cert_chain(certfile=CONFIG['cert'], keyfile=CONFIG['key'])
+context.load_verify_locations(cafile=CONFIG['authorized'])
+
+sv_sock = socket.socket()
+sv_sock.bind(('localhost', 1966))
+sv_sock.listen(5)
+
+while True:
+    print('\n-=-=- Listening for requests -=-=-\n')
+    cl_sock, from_addr = sv_sock.accept()
+    context.verify_mode = ssl.CERT_REQUIRED
+    stream = context.wrap_socket(cl_sock, server_side=True)
+    try:
+        handle_client(stream)
+    except Exception as ex:
+        import traceback
+        traceback.print_exc()
+        print('\n-=-=- We have a problem -=-=-\n')
+        print(ex)        
+    finally:
+        stream.shutdown(socket.SHUT_RDWR)
+        stream.close()
+        print("Goodbye", from_addr)
Proxy Information
Original URL
gemini://git.skyjake.fi/booster/main/cdiff/27b00bc437c8b54318c0cf2f5fc9444acb684d68
Status Code
Success (20)
Meta
text/gemini; charset=utf-8
Capsule Response Time
55.973394 milliseconds
Gemini-to-HTML Time
0.859215 milliseconds

This content has been proxied by September (ba2dc).