-- Leo's gemini proxy

-- Connecting to gemini.dimakrasner.com:1965...

-- Connected

-- Sending request

-- Meta line: 20 text/gemini

import ussl

import socket

import network

import _thread

import machine

import urequests

import time

import zlib

import _thread

import uselect

import time

from conf import *


from guppy import *

import random


keyfile = open("key.der", "rb").read()

certfile = open("cert.der", "rb").read()


py_status = b'20 text/x-script.python\r\n'

gmi_status = b'20 text/gemini\r\n'


def get_path(req, index):

url = req[:-2]

print(url)

try:

_, _, _, path = url.split(b'/', 3)

except ValueError:

_, _, _, path = (url + b'/').split(b'/', 3)


path = path.strip(b'/')


if len(path) == 0:

return index


return path.decode('utf-8')


def update_public_ip():

print('Updating public address')

res = urequests.get(f"https://www.duckdns.org/update?domains={DOMAINS}&token={TOKEN}")

res.close()


class Request:

def __init__(self, ident, s, poller):

self.id = ident

self.start = time.ticks_ms()

self.deadline = time.ticks_add(self.start, 20000)

self.s = s

self.poller = poller

self.ssl = None

self.req = bytearray(64)

self.req_off = 0

self.status = None

self.status_off = 0

self.chunk = None

self.chunk_off = 0

self.f = None


poller.register(s, uselect.POLLIN)


def closenotify(self):

self.ssl.closenotify()


def close(self):

if self.ssl:

self.poller.unregister(self.ssl)

self.ssl.close()


if self.s:

self.poller.unregister(self.s)

self.s.close()


if self.f:

self.f.close()


time.sleep(10)


wlan = network.WLAN(network.STA_IF)

wlan.active(True)

wlan.connect(SSID, PASSPHRASE)


while wlan.ifconfig()[0] == '0.0.0.0':

print('Waiting for connection')

time.sleep(1)


s = socket.socket()

s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

s.setblocking(False)

s.bind(('0.0.0.0', PORT))

s.listen(5)

led = machine.Pin('LED', machine.Pin.OUT)


guppy

us = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

us.bind(('0.0.0.0', 6775))

print(us)


print(f"Local address: {wlan.ifconfig()[0]}")


try:

update_public_ip()

except Exception as e:

print(e)


last_ip_update = time.ticks_ms()

next_ip_update = time.ticks_add(last_ip_update, 21600000)


poller = uselect.poll()

poller.register(s, uselect.POLLIN)


next_id = 0

requests = []


guppy

usessions = {}

poller.register(us, uselect.POLLIN)


print('Ready')


while True:

poller.register(s, uselect.POLLIN)


now = time.ticks_ms()

res = poller.poll(100)


if not res and now >= next_ip_update:

try:

update_public_ip()

last_ip_update = now

next_ip_update = time.ticks_add(now, 21600000)

except Exception as e:

print(e)


led.value(1 if res or requests else 0)


new_conn = False

events = {}


for t in res:

strm = t[0]

event = t[1]


guppy

if strm == us:

continue


if strm is s:

new_conn = True

else:

reqs = [req for req in requests if req.s is strm or req.ssl is strm]

if len(reqs) > 1:

raise Exception("Invalid state")

req = reqs[0]


if not req:

print(f"Unknown stream: {strm}")

continue


events[req] = event


for i in range(2):

for req in requests:

try:

if i == 0:

if now >= req.deadline:

raise Exception(f"{req.id} has timed out")

continue


event = events.get(req)

if event is None:

continue


if event == uselect.POLLHUP or event == uselect.POLLHUP:

raise Exception(f"EOF from {req.id}")


if event == uselect.POLLOUT:

if req.status_off < len(req.status):

print(f"Sending status line to {req.id}")


sent = req.ssl.write(req.status)

if sent is None:

continue

if sent <= 0:

raise Exception(f"Failed to send status line to {req.id}")


req.status_off += sent


if not req.f:

try:

req.closenotify()

finally:

raise Exception(f"Done sending status line to {req.id}")

else:

if not req.chunk or req.chunk_off >= len(req.chunk):

req.chunk = req.f.read(1024)

if not req.chunk:

try:

req.closenotify()

finally:

raise Exception(f"Done sending response to {req.id}")

req.chunk_off = 0


print(f"Sending {len(req.chunk) - req.chunk_off} to {req.id}")

sent = req.ssl.write(req.chunk[req.chunk_off:])

if sent is None:

continue


if sent <= 0:

raise Exception(f"Failed to send response to {req.id}")


req.chunk_off += sent


continue


if req.ssl is None:

print(f"Handshake on {req.id}")

req.ssl = ussl.wrap_socket(req.s, server_side=True, key=keyfile, cert=certfile, do_handshake=False)

poller.unregister(req.s)

poller.register(req.ssl, uselect.POLLIN)

continue


b = req.ssl.read(1)

if b is None:

continue

if len(b) == 0:

raise Exception(f"EOF from {req.id}")


req.req[req.req_off] = b[0]

req.req_off += 1


if req.req_off == 9 and req.req[:req.req_off] != b'gemini://':

raise Exception(f"Invalid scheme from {req.id}: {req.req[:req.req_off]}")


b'gemini://a.b\r\n'

if req.req_off < 14:

continue


eof = req.req[:req.req_off].endswith(b'\r\n')


if not eof and req.req_off >= len(req.req):

raise Exception(f"Invalid request from {req.id}: {req.req[:req.req_off]}")


if not eof:

continue


path = get_path(req.req[:req.req_off], 'guppy-index.gmi' if req.req[:req.req_off].startswith(b'gemini://guppy.000090000.xyz') else 'index.gmi')


if '/' in path or path == 'key.der' or path == 'cert.der' or path == 'conf.py' or path == 'boot.py':

print(f"Forbidden path from {req.id}: {path}")

req.resp = b'40 Forbidden\r\n'

else:

print(f"Serving {path} to {req.id}")

try:

req.f = open(path, "rb")

req.status = py_status if path == b'main.py' else gmi_status

except Exception as e:

print(e)

req.status = b'41 Internal server error\r\n'


poller.modify(req.ssl, uselect.POLLOUT)

except Exception as e:

print(e)

print(f"Closing {req.id}")

req.close()

if len(requests) == 5:

print('Allowing new connections')

poller.register(s, uselect.POLLIN)

requests.remove(req)


if new_conn:

try:

print('Accepting new connection')

c, _ = s.accept()

except Exception as e:

print(e)

continue


try:

c.setblocking(False)

except Exception as e:

print(e)

c.close()

continue


next_id += 1

req = Request(next_id, c, poller)

requests.append(req)

print(f"Accepted {req.id}, have {len(requests)} connections")


if len(requests) == 5:

print('Reached the maximum number of concurrent requests')

poller.unregister(s)


guppy

ufinished = []


if [strm for (strm, event) in res if strm == us and event == uselect.POLLIN]:

pkt, src = us.recvfrom(2048)

try:

if not pkt.endswith(b'\r\n'):

raise Exception("Invalid packet")


session = usessions.get(src)

if session:

seq = int(pkt[:len(pkt) - 2])

if session.ack(seq):

print(f"Session {src} has ended successfully")

ufinished.append(src)

else:

if len(usessions) > 32:

raise Exception("Too many usessions")


if not pkt.startswith(b'guppy://') and not pkt.endswith(b'\r\n'):

raise Exception("Invalid request")


path = get_path(pkt, 'guppy-index.gmi' if pkt.startswith(b'guppy://guppy.000090000.xyz') else 'index.gmi')

if '/' in path or path == 'key.der' or path == 'cert.der' or path == 'conf.py' or path == 'boot.py':

raise Exception(f"Forbidden path from {src}: {path}")


mime_type = "text/gemini"

if path.endswith(b'.py'):

mime_type = "text/x-script.python"

elif path.endswith(b'.c'):

mime_type = "text/x-c"

usessions[src] = Session(us, src, mime_type, open(path, "rb"))

except Exception as e:

print("Unhandled exception", e)


for src, session in usessions.items():

try:

session.send()

except SessionTimeoutException:

print(f"Session {src} has timed out")

ufinished.append(src)

except Exception as e:

print("Unhandled exception", e)


for src in ufinished:

usessions[src].close()

usessions.pop(src)

-- Response ended

-- Page fetched on Thu May 9 20:47:42 2024