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)
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 = []
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)