#!/usr/bin/env python3 ##==========================================================================## ## ## ## --= BOOSTER --= ## ## Gemlog Admin Interface ## ## ## ##==========================================================================## # Copyright (c) 2021-2022 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 json import os import subprocess import sys import urllib.parse pjoin = os.path.join def append_slash(p): if not p.endswith('/'): return p + '/' return p def urlenc(q): return urllib.parse.quote(q) 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 report_error(code, msg): print(code, msg + '\r') sys.exit(0) 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()) #print(json.dumps(CONFIG, indent=2)) FILES_ROOT = append_slash(CONFIG['files']['root']) AUTHORIZED = CONFIG['authorized'] # cert/pubkey fingerprint(s) req_identity = os.getenv('REMOTE_IDENT').split(';') if req_identity[0] not in AUTHORIZED and req_identity[1] not in AUTHORIZED: report_error(61, 'Not authorized') req_mime = os.getenv('CONTENT_TYPE') req_token = os.getenv('TITAN_TOKEN') path = os.getenv('SCRIPT_NAME') + os.getenv('PATH_INFO') data = sys.stdin.buffer.read() #print(f'Path : {path}') #print(f'Token : {req_token}') #print(f'MIME : {req_mime}') #print(f'Data : {len(data)} bytes') file_path = None msg_path = None if (path.startswith('.') or os.path.basename(path).startswith('.') or '..' in path): report_error(61, 'Not authorized') # Are we allowed to edit this path? msg_prefix = '' for file_group in CONFIG['files']: group = CONFIG['files'][file_group] if type(group) != dict: continue if 'subdir' in group: auth_prefix = '/' + append_slash(group['subdir']) msg_path = path[len(auth_prefix):] elif 'file' in group: auth_prefix = '/' + group['file'] msg_path = os.path.basename(path) if path.startswith(auth_prefix): file_path = os.path.normpath(FILES_ROOT + path) if not file_path.startswith(FILES_ROOT) or \ os.path.isdir(file_path): report_error(61, 'Not authorized') msg_prefix = f'{file_group}: ' break if not file_path: report_error(61, "Unauthorized location") # Process the request. is_new = not os.path.exists(file_path) view_url = '%s%s' % (CONFIG['site_url'], path) response = '# Booster log\n' if not is_new and req_token == 'DELETE': response += f'* deleting file: {file_path}\n' os.remove(file_path) git_msg = msg_prefix + 'Removed ' + msg_path else: if is_new: response += f'* creating a new file: {file_path}\n' git_msg = msg_prefix + 'Added ' + msg_path else: response += f'* updating file: {file_path}\n' git_msg = msg_prefix + 'Updated ' + msg_path # write the data response += f'* writing %d bytes\n' % len(data) dst = open(file_path, 'wb') dst.write(data) dst.close() # Update Gemlog indices. GEMLOG_SUBDIR = CONFIG['files']['Gemlog']['subdir'] os.chdir(pjoin(FILES_ROOT, GEMLOG_SUBDIR)) 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([CONFIG['git'], 'add', file_path]) response += run_command([CONFIG['git'], '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(pjoin(CONFIG['site_url'], GEMLOG_SUBDIR)) print('20 text/gemini; charset=utf-8\r\n' + response) gemini://git.skyjake.fi/booster/capsule/booster.py

-- Leo's gemini proxy

-- Connecting to git.skyjake.fi:1965...

-- Connected

-- Sending request

-- Meta line: 20 text/plain;charset=utf-8

-- Response ended

-- Page fetched on Sat May 4 04:22:18 2024