2025-01-12 21:36:58 -08:00
#!/usr/bin/python3
2025-01-15 21:34:38 -08:00
import asyncio , traceback , socket , ssl , zipfile , uuid , io , os
2025-01-14 19:14:42 -08:00
from OpenSSL import crypto
2025-01-13 20:28:54 -08:00
from urllib . parse import urlparse , quote
2025-01-14 19:33:01 -08:00
from flask import Flask , request , redirect , send_file , Response , make_response
2025-01-12 21:36:58 -08:00
from hypercorn . config import Config
from hypercorn . asyncio import serve
2025-01-14 19:14:42 -08:00
from pathlib import Path
homefolder = str ( Path . home ( ) )
2025-01-12 21:36:58 -08:00
app = Flask ( __name__ )
2025-01-14 19:23:13 -08:00
@app.route ( " /robots.txt " )
def robots ( ) :
2025-01-15 15:14:57 -08:00
return Response ( " User-agent: * \n Disallow: /gem \n Disallow: /loadcert \n Disallow: /gencert.zip \n Disallow: /certload " , mimetype = " text/plain " )
2025-01-12 21:36:58 -08:00
@app.route ( " / " )
def root ( ) :
2025-01-13 21:31:01 -08:00
return send_file ( " home.html " )
2025-01-13 15:53:40 -08:00
@app.route ( " /external.png " )
def external ( ) :
return send_file ( " external.png " )
@app.route ( " /cross-server.png " )
2025-01-13 15:55:48 -08:00
def crosserver ( ) :
return send_file ( " cross-server.png " )
2025-01-14 20:02:56 -08:00
@app.route ( " /loadcert " )
def loadcert ( ) :
return send_file ( " loadcert.html " )
def allowed_file ( filename ) :
return ' . ' in filename and filename . rsplit ( ' . ' , 1 ) [ 1 ] . lower ( ) == " pem "
@app.route ( " /certload " , methods = [ ' GET ' , ' POST ' ] )
2025-01-14 20:38:09 -08:00
def loadcert_backend ( ) :
2025-01-14 20:02:56 -08:00
if request . method == ' POST ' :
if ' cert ' not in request . files :
return " Invalid request. (cert is missing!) "
if ' privkey ' not in request . files :
return " Invalid request. (privkey is missing!) "
2025-01-15 21:52:09 -08:00
cert = request . files [ ' cert ' ]
2025-01-15 21:51:42 -08:00
privkey = request . files [ ' privkey ' ]
certt = cert . read ( )
privkeyy = privkey . read ( )
2025-01-14 20:02:56 -08:00
if cert . filename == ' ' or privkey . filename == ' ' :
return " Please upload a certificate and private key. "
if allowed_file ( cert . filename ) and allowed_file ( privkey . filename ) :
2025-01-15 21:31:58 -08:00
for i in os . listdir ( homefolder + " /certs/ " ) :
thisname = " - " . join ( i . split ( " - " ) [ : - 1 ] )
2025-01-15 21:51:42 -08:00
if ( open ( homefolder + " /certs/ " + thisname + " -privkey.pem " ) . read ( ) == privkeyy ) and ( open ( homefolder + " /certs/ " + thisname + " -chain.pem " ) . read ( ) == certt ) :
2025-01-15 21:31:58 -08:00
resp = make_response ( ' <meta name= " viewport " content= " width=device-width, initial-scale=1.0 " ><link rel= " stylesheet " href= " /style.css " >The certificate seems to already exist, loading the used file.<br><br><p><a href= " / " class=go style= " color: white; " >Go home</a></p> ' )
resp . set_cookie ( ' certname ' , thisname )
return resp
2025-01-14 22:02:45 -08:00
random_name = str ( uuid . uuid4 ( ) )
2025-01-15 21:51:42 -08:00
open ( homefolder + " /certs/ " + random_name + " -chain.pem " , ' w ' ) . write ( certt )
open ( homefolder + " /certs/ " + random_name + " -privkey.pem " , ' w ' ) . write ( privkeyy )
2025-01-15 21:31:58 -08:00
resp = make_response ( ' <meta name= " viewport " content= " width=device-width, initial-scale=1.0 " ><link rel= " stylesheet " href= " /style.css " >Success!<br><br><p><a href= " / " class=go style= " color: white; " >Go home</a></p> ' )
2025-01-14 20:36:26 -08:00
resp . set_cookie ( ' certname ' , random_name )
return resp
2025-01-14 20:02:56 -08:00
else :
return " Both files must be a .pem file, you might want to generate a certificate via the home page. "
else :
return " Cannot go to /certload with GET, perhaps you ' re looking for /loadcert "
2025-01-14 19:18:23 -08:00
@app.route ( " /gencert.zip " )
def gencert ( ) :
2025-01-14 22:02:45 -08:00
random_name = str ( uuid . uuid4 ( ) )
2025-01-14 19:14:42 -08:00
k = crypto . PKey ( )
2025-01-14 20:47:47 -08:00
k . generate_key ( crypto . TYPE_RSA , 2048 )
2025-01-14 19:14:42 -08:00
cert = crypto . X509 ( )
2025-01-14 19:28:00 -08:00
cert . get_subject ( ) . C = " US "
2025-01-14 19:14:42 -08:00
cert . get_subject ( ) . ST = " Earth "
cert . get_subject ( ) . L = " Earth "
2025-01-14 19:20:35 -08:00
cert . get_subject ( ) . O = random_name
cert . get_subject ( ) . OU = random_name
2025-01-15 14:19:36 -08:00
cert . get_subject ( ) . CN = " Gem2Browser user " + random_name
2025-01-14 19:14:42 -08:00
cert . set_serial_number ( 1000 )
cert . gmtime_adj_notBefore ( 0 )
cert . gmtime_adj_notAfter ( 10 * 365 * 24 * 60 * 60 )
cert . set_issuer ( cert . get_subject ( ) )
cert . set_pubkey ( k )
cert . sign ( k , ' sha1 ' )
#open(homefolder + "/" + random_name + "-privkey.pem", "wb").write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k))
#open(homefolder + "/" + random_name + "-cert.pem", "wb").write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
zip_buffer = io . BytesIO ( )
with zipfile . ZipFile ( zip_buffer , " a " , zipfile . ZIP_DEFLATED , False ) as zip_file :
for file_name , data in [ ( ' cert.pem ' , io . BytesIO ( crypto . dump_certificate ( crypto . FILETYPE_PEM , cert ) ) ) ,
( ' privkey.pem ' , io . BytesIO ( crypto . dump_privatekey ( crypto . FILETYPE_PEM , k ) ) ) ] :
zip_file . writestr ( file_name , data . getvalue ( ) )
2025-01-14 20:02:56 -08:00
open ( homefolder + " /certs/ " + random_name + " -chain.pem " , " wb " ) . write ( crypto . dump_certificate ( crypto . FILETYPE_PEM , cert ) )
open ( homefolder + " /certs/ " + random_name + " -privkey.pem " , " wb " ) . write ( crypto . dump_privatekey ( crypto . FILETYPE_PEM , k ) )
2025-01-14 19:34:33 -08:00
resp = make_response ( zip_buffer . getvalue ( ) )
resp . set_cookie ( ' certname ' , random_name )
resp . mimetype = " application/zip "
2025-01-14 19:32:36 -08:00
return resp
2025-01-13 15:55:48 -08:00
@app.route ( " /style.css " )
2025-01-13 16:00:57 -08:00
def style ( ) :
2025-01-13 16:13:58 -08:00
return send_file ( " style.css " )
2025-01-15 15:16:12 -08:00
@app.route ( " /favicon.ico " )
def favicon ( ) :
return send_file ( " gem2browser.png " )
2025-01-14 16:59:44 -08:00
@app.route ( " /logo.png " )
2025-01-14 17:00:51 -08:00
def logo ( ) :
2025-01-14 16:59:44 -08:00
return send_file ( " gem2browser.png " )
2025-01-12 21:51:59 -08:00
@app.route ( " /gem " )
def relay ( ) :
2025-01-14 19:21:11 -08:00
print ( request . headers . get ( ' User-Agent ' ) )
2025-01-12 21:51:59 -08:00
url = request . args . get ( ' gemini ' )
2025-01-13 20:27:09 -08:00
queries = request . args . get ( ' query ' )
2025-01-14 20:36:26 -08:00
certfile = request . cookies . get ( ' certname ' )
2025-01-12 21:51:59 -08:00
if url == None :
return redirect ( " / " )
2025-01-12 22:00:24 -08:00
code = " <h1>Something went wrong...</h1> \n "
2025-01-12 21:36:58 -08:00
title = " Something went wrong... "
2025-01-13 22:11:26 -08:00
print ( " GET " + " gemini:// " + url + ( " ? " + queries if queries != None else " " ) )
2025-01-12 21:36:58 -08:00
try :
2025-01-12 22:31:01 -08:00
gsocket = socket . socket ( socket . AF_INET , socket . SOCK_STREAM )
2025-01-13 20:27:09 -08:00
fulladdr = " gemini:// " + url + ( " ? " + queries if queries != None else " " )
2025-01-14 20:59:13 -08:00
contx = ssl . create_default_context ( )
2025-01-14 20:59:41 -08:00
contx . check_hostname = False
2025-01-14 20:59:13 -08:00
contx . verify_mode = ssl . CERT_NONE
2025-01-14 20:36:26 -08:00
if certfile :
contx . load_cert_chain ( certfile = homefolder + " /certs/ " + certfile + " -chain.pem " , keyfile = homefolder + " /certs/ " + certfile + " -privkey.pem " ) ;
2025-01-14 21:19:51 -08:00
gemsocket = contx . wrap_socket ( gsocket , server_hostname = urlparse ( fulladdr ) . hostname )
2025-01-12 22:37:07 -08:00
gemsocket . connect ( ( urlparse ( fulladdr ) . hostname , 1965 ) )
2025-01-13 20:27:09 -08:00
gemsocket . send ( bytes ( fulladdr + " \r \n " , " UTF-8 " ) )
2025-01-12 22:31:01 -08:00
received = " "
2025-01-13 21:52:44 -08:00
mimetype = " "
gemraw = bytearray ( )
num = 0
2025-01-13 21:57:21 -08:00
ok = False
2025-01-13 21:58:28 -08:00
binary = False
2025-01-12 22:31:01 -08:00
while True :
2025-01-14 15:39:13 -08:00
gemresponse = gemsocket . recv ( 2048 )
2025-01-14 15:25:31 -08:00
if len ( gemresponse ) != 0 :
2025-01-14 15:39:13 -08:00
for i in gemresponse :
gemraw . append ( i )
2025-01-14 15:25:31 -08:00
else :
break
2025-01-14 15:39:13 -08:00
found = False
gemcontent_binary = bytearray ( )
mtype = bytearray ( )
2025-01-14 15:42:03 -08:00
for i in bytes ( gemraw ) :
2025-01-13 16:37:23 -08:00
try :
2025-01-14 15:39:13 -08:00
if not found :
2025-01-14 15:43:18 -08:00
mtype . append ( i )
2025-01-14 15:42:03 -08:00
else :
2025-01-14 15:43:18 -08:00
gemcontent_binary . append ( i )
2025-01-14 15:39:13 -08:00
if " \n " in bytes ( mtype ) . decode ( ) and not found :
2025-01-14 15:51:03 -08:00
found = True
2025-01-14 15:39:13 -08:00
ok = bytes ( mtype ) . decode ( ) . split ( " " ) [ 0 ] [ 0 ] == " 2 "
2025-01-14 15:45:21 -08:00
mimetype = bytes ( mtype ) . decode ( ) . replace ( " \r " , " " ) . split ( " \n " ) [ 0 ] . split ( " " ) [ 1 ] . split ( " ; " ) [ 0 ]
2025-01-13 16:37:23 -08:00
except :
2025-01-14 15:45:21 -08:00
pass
2025-01-14 15:39:13 -08:00
try :
2025-01-14 15:49:41 -08:00
received = gemraw . decode ( ) . strip ( )
2025-01-14 15:39:13 -08:00
except :
binary = True
print ( traceback . format_exc ( ) )
2025-01-13 21:58:28 -08:00
if binary :
2025-01-14 15:39:13 -08:00
return Response ( bytes ( gemcontent_binary ) , mimetype = " text/gemini " if mimetype == " " else mimetype )
2025-01-12 22:31:01 -08:00
received = received . replace ( " \r " , " " )
firstline = True
redirected = False
gemtext = True
code = " "
2025-01-13 20:12:31 -08:00
escaped = False
2025-01-12 22:31:01 -08:00
for i in received . split ( " \n " ) :
2025-01-13 18:35:32 -08:00
i = i . replace ( " < " , " < " )
2025-01-12 22:31:01 -08:00
if firstline :
if i . split ( " " ) [ 0 ] [ 0 ] == " 3 " :
2025-01-13 22:04:59 -08:00
if i . split ( " " ) [ 1 ] [ 0 ] == " / " :
2025-01-13 22:05:33 -08:00
return redirect ( " /gem?gemini= " + urlparse ( fulladdr ) . hostname + quote ( i . split ( " " ) [ 1 ] , safe = ' ' ) )
2025-01-13 20:28:54 -08:00
return redirect ( " /gem?gemini= " + quote ( i . split ( " " ) [ 1 ] [ 9 : ] , safe = ' ' ) )
2025-01-13 20:27:09 -08:00
elif i . split ( " " ) [ 0 ] [ 0 ] == " 1 " :
2025-01-15 21:31:58 -08:00
return f ' <!DOCTYPE html> \n <html><head><meta charset= " UTF-8 " ><meta name= " viewport " content= " width=device-width, initial-scale=1.0 " ><link rel= " stylesheet " href= " /style.css " ><title>Input required</title></head><body><h1>Input required</h1><p>The specified Gemini server wants more data: <pre> { i } </pre></p><form action= " /gem " ><input hidden class= " input " value= " { url } " type= " text " name= " gemini " ><input class= " input " type= " { " password " if i . split ( " " ) [ 0 ] [ 1 ] == " 1 " else " text " } " name= " query " autocomplete= " on " ><br><input type= " submit " class= " go " value= " Go! " ><br><br></form></body></html> '
2025-01-14 19:39:42 -08:00
elif i . split ( " " ) [ 0 ] [ 0 ] == " 6 " :
2025-01-15 21:31:58 -08:00
return f ' <!DOCTYPE html> \n <html><head><meta charset= " UTF-8 " ><meta name= " viewport " content= " width=device-width, initial-scale=1.0 " ><link rel= " stylesheet " href= " /style.css " ><title>Certificate requested</title></head><body><h1>Certificate requested</h1><p>The specified Gemini server wants a client certificate, or the certificate is invalid. <pre> { i } </pre></p><br><br><p>You can load a certificate in <a href= " / " >home page.</a></p></body></html> '
2025-01-12 22:31:01 -08:00
elif i . split ( " " ) [ 0 ] [ 0 ] != " 2 " :
2025-01-15 21:31:58 -08:00
return f ' <!DOCTYPE html> \n <html><head><meta charset= " UTF-8 " ><meta name= " viewport " content= " width=device-width, initial-scale=1.0 " ><link rel= " stylesheet " href= " /style.css " ><title>Something went wrong...</title></head><body><h1>Something went wrong...</h1><p>The specified Gemini server returned a status of: { i } </p></body></html> '
2025-01-12 22:31:01 -08:00
else :
firstline = False
2025-01-13 16:28:55 -08:00
if i . split ( " " ) [ 1 ] . split ( " ; " ) [ 0 ] != " text/gemini " :
2025-01-13 16:28:25 -08:00
print ( " Unrecognised type: " + i . split ( " " ) [ 1 ] )
2025-01-13 16:27:15 -08:00
return Response ( " " . join ( received . split ( " \n " ) [ 1 : ] ) , mimetype = i . split ( " " ) [ 1 ] )
2025-01-12 22:31:01 -08:00
else :
2025-01-13 20:11:59 -08:00
if escaped :
if i [ 0 : 3 ] == " ``` " :
code + = " </pre> \n "
escaped = False
2025-01-13 15:53:40 -08:00
else :
2025-01-13 20:11:59 -08:00
code + = i + " \n "
2025-01-12 22:31:01 -08:00
else :
2025-01-13 20:11:59 -08:00
if i [ 0 : 2 ] == " # " :
if title == " Something went wrong... " :
title = i [ 2 : ]
temp = i [ 2 : ]
code + = f " <h1> { temp } </h1> \n "
elif i [ 0 : 3 ] == " ## " :
temp = i [ 3 : ]
code + = f " <h2> { temp } </h2> \n "
elif i [ 0 : 3 ] == " ``` " :
code + = " <pre> \n "
escaped = True
elif i [ 0 : 4 ] == " ### " :
temp = i [ 4 : ]
code + = f " <h3> { temp } </h3> \n "
elif i [ 0 : 2 ] == " * " :
temp = i [ 2 : ]
code + = f " <ul><li> { temp } </li></ul> \n "
elif i [ 0 : 2 ] == " => " :
temp = " " . join ( i [ 2 : ] . strip ( ) . replace ( " " , " " ) . split ( " " ) )
goto = temp . split ( " " ) [ 0 ]
prse = urlparse ( goto )
extra = " "
2025-01-14 22:08:22 -08:00
qury = " "
2025-01-13 20:11:59 -08:00
extracomment = goto
if prse . netloc == " " and prse . scheme == " " :
isdir = url [ len ( url ) - 1 ] == " / "
isabs = goto [ 0 ] == " / " or goto [ 0 : 2 ] == " // "
2025-01-14 22:02:45 -08:00
isquery = goto [ 0 ] == " ? "
2025-01-13 20:11:59 -08:00
dubleslash = goto [ 0 : 2 ] == " // "
2025-01-15 15:39:57 -08:00
if url == urlparse ( fulladdr ) . hostname :
2025-01-15 15:39:15 -08:00
isquery = False
isabs = False
isdir = True
2025-01-15 15:40:32 -08:00
goto = f " / { goto } " if goto [ 0 ] != " / " else goto
2025-01-14 22:05:00 -08:00
if isquery :
2025-01-14 22:02:45 -08:00
tempurl = url
2025-01-14 22:09:20 -08:00
qury = goto [ 1 : ]
2025-01-14 22:02:45 -08:00
elif isabs :
2025-01-13 20:15:40 -08:00
tempurl = urlparse ( fulladdr ) . hostname + ( goto [ 1 : ] if dubleslash else goto )
2025-01-13 20:11:59 -08:00
elif isdir :
tempurl = url + goto
else :
tempurl = " / " . join ( url . split ( " / " ) [ : - 1 ] ) + " / " + goto
2025-01-13 22:09:09 -08:00
tempurl = quote ( tempurl , safe = ' ' )
2025-01-13 20:11:59 -08:00
goto = f " /gem?gemini= { tempurl } "
elif prse . scheme != " gemini " :
extra = " <img height= \" 19 \" src= \" /external.png \" > "
extracomment = f " This link points to an address that isn ' t gemini ( { prse . scheme } ) "
else :
if prse . hostname != urlparse ( fulladdr ) . hostname :
extra = " <img height= \" 19 \" src= \" /cross-server.png \" > "
extracomment = f " This link points to an address that isn ' t from the server you ' re currently connecting to ( { prse . hostname } ) "
goto = goto . replace ( " gemini:// " , " " )
2025-01-13 22:09:09 -08:00
goto = quote ( goto , safe = ' ' )
2025-01-13 20:11:59 -08:00
goto = f " /gem?gemini= { goto } "
if temp . split ( " " ) == 1 :
comment = goto
else :
comment = " " . join ( temp . split ( " " ) [ 1 : ] )
2025-01-14 22:08:22 -08:00
if qury != " " :
qury = " &query= " + quote ( qury , safe = ' ' )
code + = f " <p title= \" { extracomment } \" ><a href= \" { goto } { qury } \" > { comment } { extra } </a></p> \n "
2025-01-13 20:11:59 -08:00
elif i [ 0 : 2 ] == " > " :
code + = f " <p class= ' greentext ' > { i } </p> \n "
else :
code + = f " <p> { i } </p> \n "
2025-01-12 22:31:01 -08:00
if title == " Something went wrong... " :
title = " gemini:// " + url
2025-01-12 21:36:58 -08:00
except :
2025-01-13 16:24:58 -08:00
code + = " <pre> " + traceback . format_exc ( ) + " </pre> "
2025-01-15 21:31:58 -08:00
return f ' <!DOCTYPE html> \n <html><head><meta charset= " UTF-8 " ><meta name= " viewport " content= " width=device-width, initial-scale=1.0 " ><link rel= " stylesheet " href= " /style.css " ><title> { title } </title></head><body> { code } </body></html> '
2025-01-12 21:36:58 -08:00
# Run the Hypercorn ASGI server
2025-01-12 21:49:36 -08:00
conf = Config ( )
conf . bind = " 0.0.0.0:2009 "
asyncio . run ( serve ( app , conf ) )