!/usr/bin/env python3

gmitodo - Gemini Shared Task List CGI

Author: Simon Volpert simon@simonvolpert.com

Project page: gemini://simonvolpert.com/gmitodo/

This program is free software, released under the MIT license. See the LICENSE file for more information

import os

import sys

from pathlib import Path

import urllib.parse

Possible state directory locations in order of decreasing priority

state_locations = [

Path('/etc/gmitodo'),             # System-wide config directory

Path.home() / '.config/gmitodo',  # User config directory

Path.cwd(),                       # Current working directory

Path('/tmp/gmitodo')              # Transient directory

]

CONFIG_TEMPLATE = '''# gmitodo configuration file

Set this option to "yes" to require authentication to view the task list.

auth_view=no

Set this option to "yes" to require authentication to modify the task list.

auth_edit=no

Clients which are allowed to access the sections requiring authentication

above. This option can be set to either an IP address or a certificate SHA

hash, as seen by the server, and can appear more than once. IP addresses

have higher priority than certificate hashes. To find out the correct

certificate hash, examine the server logs after navigating to the app's page

with an unauthorized client.

allow=

'''

Read persistent state

def read_state(filename):

for location in state_locations:

	try:

		state_file = location / filename

		data = [x for x in state_file.read_text().split('\n') if x != '']

		return data

	except OSError:

		continue

Write persistent state

def write_state(filename, data):

for i, line in enumerate(data):

	if ' ' in line:

		data[i] = urllib.parse.quote_plus(line)

for location in state_locations:

	try:

		location.mkdir(parents=True, exist_ok=True)

		state_file = location / filename

		state_file.write_text('\n'.join(data) + '\n')

		return

	except OSError:

		continue

print('42 Unable to write state\r')

raise SystemExit

Check authorization

def check_auth():

if remote_addr not in allowed_users:

	if cert_hash == '':

		print('60 Client certificate required\r')

		raise SystemExit

	elif cert_hash not in allowed_users:

		sys.stderr.write('certificate hash: ' + cert_hash)

		print('61 Certificate not authorized\r')

		raise SystemExit

Collect request parameters

server_name = os.environ['SERVER_NAME'] if 'SERVER_NAME' in os.environ else ''

path_info = os.environ['PATH_INFO'] if 'PATH_INFO' in os.environ else ''

query_string = os.environ['QUERY_STRING'].replace('/', '%2F') if 'QUERY_STRING' in os.environ else ''

remote_addr = os.environ['REMOTE_ADDR'] if 'REMOTE_ADDR' in os.environ else ''

cert_hash = os.environ['TLS_CLIENT_HASH'].lower() if 'TLS_CLIENT_HASH' in os.environ else ''

Find and read the configuration file

_data = read_state('todo.conf')

If no config file is found, write out defaults

if _data is None:

write_state('todo.conf', [CONFIG_TEMPLATE])

_data = CONFIG_TEMPLATE.split('\n')

Scan the config file for relevant options

auth_view = False

auth_edit = False

allowed_users = []

for line in _data:

if line.startswith('auth_view='):

	auth_view = True if line[10:].lower() in ['yes', '1', 'true'] else False

elif line.startswith('auth_edit='):

	auth_edit = True if line[10:].lower() in ['yes', '1', 'true'] else False

elif line.startswith('allow='):

	_param = line[6:].lower()

	if _param != '':

		allowed_users.append(_param)

List entries

if path_info == '':

if auth_view:

	check_auth()

print('20 text/gemini\r')

print('=> /cgi-bin/todo/add + Add New')

print('=> /cgi-bin/todo/multiadd + Add Multiple')

print('____________________\r')

print()

for line in read_state('todo.list'):

	if ' ' in line:

		line = urllib.parse.quote_plus(line)

	unquoted = urllib.parse.unquote_plus(line)

	print('=> /cgi-bin/todo/rm?{} √ {}'.format(line, unquoted))

Add new entry

elif path_info == '/add' or path_info == '/multiadd':

if auth_edit:

	check_auth()

if query_string == '':

	print('10 Enter new entry\r')

else:

	state = read_state('todo.list')

	# Split input at newlines and add each as separate entries

	if '\n' in query_string or ' ' in query_string:

		query_string = urllib.parse.quote_plus(query_string)

	for line in query_string.split('%0A'):

		line = line.strip('+')

		if line != '' and line not in state:

			state.append(line)

			write_state('todo.list', state)

	if path_info == '/multiadd':

		print('30 gemini://{}/cgi-bin/todo/multiadd\r'.format(server_name))

	print('30 gemini://{}/cgi-bin/todo\r'.format(server_name))

Remove existing entry

elif path_info == '/rm':

if auth_edit:

	check_auth()

if query_string == '':

	print('59 Missing parameter\r')

else:

	state = read_state('todo.list')

	if query_string in state:

		state.remove(query_string)

		write_state('todo.list', state)

	print('30 gemini://{}/cgi-bin/todo\r'.format(server_name))

Deny all other requests

else:

print('50 Not found\r')

Proxy Information
Original URL
gemini://simonvolpert.com/gmitodo/todo
Status Code
Success (20)
Meta
application/octet-stream
Capsule Response Time
199.697581 milliseconds
Gemini-to-HTML Time
2.987503 milliseconds

This content has been proxied by September (3851b).