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