gem

a fork of khuxkm's gemini to web proxy
Log | Files | Refs

index.cgi (9204B)


      1 #!/usr/bin/python3
      2 import cgi, os, json, socket, ssl, threading, time, sys, mimeparse, gem2html
      3 from html import escape
      4 from urllib.parse import urlparse, parse_qs, urlencode, uses_relative, uses_netloc, urlunparse, urljoin
      5 uses_relative.append("gemini")
      6 uses_netloc.append("gemini")
      7 
      8 class StartComparison(str):
      9     def __eq__(self,lhs):
     10         return lhs.startswith(self)
     11 
     12 class ResponseCodes:
     13     # use like: response.status==ResponseCodes.GENERIC_SUCCESS
     14     GENERIC_INPUT = StartComparison("1")
     15     GENERIC_SUCCESS = StartComparison("2")
     16     GENERIC_REDIRECT = StartComparison("3")
     17     GENERIC_TEMPFAIL = StartComparison("4")
     18     GENERIC_PERMFAIL = StartComparison("5")
     19     GENERIC_CERTFAIL = StartComparison("6")
     20 
     21     # the rest of these are just normal strings
     22     INPUT_NEEDED = "10"
     23     INPUT_NEEDED_SENSITIVE = "11"
     24 
     25     SUCCESS = "20"
     26 
     27     REDIRECT_TEMP = "30"
     28     REDIRECT_PERM = "31"
     29 
     30     TEMPFAIL_GENERIC = "40"
     31     TEMPFAIL_SERVER_UNAVAILABLE = "41"
     32     TEMPFAIL_CGI_ERROR = "42"
     33     TEMPFAIL_PROXY_ERROR = "43"
     34     TEMPFAIL_SLOW_DOWN = "44"
     35 
     36     PERMFAIL_GENERIC = "50"
     37     PERMFAIL_NOT_FOUND = "51"
     38     PERMFAIL_GONE = "52"
     39     PERMFAIL_PROXY_REFUSED = "53"
     40     PERMFAIL_BAD_REQUEST = "59"
     41 
     42     CERTFAIL_NEED_CERT = "60"
     43     CERTFAIL_BAD_CERT = "61"
     44     CERTFAIL_INVALID_CERT = "62"
     45 
     46 class ResponseObject:
     47     def __init__(self,status,meta,content):
     48         self.status=status
     49         self.meta=meta
     50         self.content=content
     51 
     52 def connect_factory(url,parsed=None):
     53     if parsed is None: parsed = urlparse(url)
     54     event = threading.Event()
     55     def _connect():
     56         ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
     57         ctx.minimum_version = ssl.TLSVersion.TLSv1_2
     58         ctx.check_hostname = False
     59         ctx.verify_mode = ssl.CERT_NONE
     60         try:
     61             sock = socket.create_connection((parsed.hostname,parsed.port or 1965),2)
     62         except socket.timeout:
     63             threading.current_thread().ret="Socket connection timed out"
     64             return
     65         sock.settimeout(5)
     66         ssock = ctx.wrap_socket(sock,server_hostname=parsed.hostname)
     67         ssock.sendall((url+"\r\n").encode("utf-8"))
     68         out = b''
     69         try:
     70             while (data:=ssock.recv(1024)) and not event.is_set():
     71                 out+=data
     72         except socket.timeout:
     73             threading.current_thread().ret="Read timed out, the page is probably too big."
     74             return
     75         ssock.shutdown(socket.SHUT_RDWR)
     76         ssock.close()
     77         header, data = out.split(b"\n",1)
     78         header = header.strip().decode("utf-8")
     79         status, meta = header.split(None,1)
     80         threading.current_thread().ret = ResponseObject(status,meta,data)
     81     return _connect, event
     82 
     83 BASE_URL = "https://xfnw.ttm.sh/gem/?"
     84 def link_callback(lurl, text):
     85     global url
     86     lurl = urljoin(url,lurl)
     87     parsed = urlparse(lurl)
     88     query = None
     89     if parsed.query:
     90         query = parsed.query
     91         parsed = parsed._replace(query=None)
     92         lurl = urlunparse(parsed)
     93     params = dict(addr=lurl)
     94     if query is not None:
     95         params["query"]=query
     96     return BASE_URL+urlencode(params), text
     97 
     98 qs = parse_qs(os.environ["QUERY_STRING"])
     99 
    100 url = next(iter(qs.get("addr",["gemini://tilde.team/~xfnw/start.gmi"])))
    101 parsed = urlparse(url)
    102 query = next(iter(qs.get("query",[None]))) or parsed.query
    103 if query:
    104     parsed = parsed._replace(query=query)
    105     url = urlunparse(parsed)
    106 
    107 if parsed.scheme and parsed.scheme!="gemini":
    108     print("Content-Type: text/html")
    109     print()
    110     print("""<!DOCTYPE html>
    111 <html>
    112 <head>
    113 <meta http-equiv="refresh" content="0;URL='{0}'">
    114 <title>Redirecting...</title>
    115 <meta charset="UTF-8">
    116 <meta name="viewport" content="width=device-width, initial-scale=1">
    117 <link rel="stylesheet" href="gem.css">
    118 </head>
    119 <body>
    120 <p>If the redirect doesn't work, <a href="{0}">click here.</a></p>
    121 </body>
    122 </html>""".format(escape(url)))
    123     sys.exit()
    124 
    125 connect_func, killswitch = connect_factory(url,parsed)
    126 connect_thread = threading.Thread(target=connect_func)
    127 # now here's the fun part
    128 # we'll join the thread with a timeout
    129 connect_thread.start()
    130 connect_thread.join(5)
    131 # now if the thread's still alive, we'll set the killswitch and join the thread again
    132 if connect_thread.is_alive():
    133     killswitch.set()
    134     connect_thread.join()
    135 # now we know for a fact the thread is done
    136 # get the return value as Thread.ret (done manually in the function definition)
    137 response = getattr(connect_thread,"ret")
    138 if type(response)!=ResponseObject:
    139     print("""Content-Type: text/html
    140 
    141 <!DOCTYPE html>
    142 <html>
    143 <head>
    144 <title>Error...</title>
    145 <meta charset="UTF-8">
    146 <meta name="viewport" content="width=device-width, initial-scale=1">
    147 <link rel="stylesheet" href="gem.css">
    148 </head>
    149 <body>
    150 <pre>""")
    151     print(response or "Unknown error occurred.")
    152     sys.exit()
    153 
    154 if response.status==ResponseCodes.GENERIC_INPUT:
    155     print("Content-Type: text/html")
    156     print()
    157     print("""<!DOCTYPE html>
    158         <html>
    159         <head>
    160           <meta charset="UTF-8">
    161             <meta name="viewport" content="width=device-width, initial-scale=1">
    162               <link rel="stylesheet" href="gem.css"></head>
    163               <title>The page you want to visit has requested input.</title>
    164               </head>
    165               <body>
    166 <p>The page you want to visit has requested input.</p>
    167 <form method='GET' action='./'>
    168 <label for='input_prompt'>{}</label><br>
    169 <input type='{}' name='query' id='input_prompt' autocomplete="off"/>
    170 <input type='hidden' name='addr' value='{}' />
    171 <button type='submit'>Submit</button>
    172 </form>
    173 <p>I take no responsibility if shoulder-surfers read your input.</p>""".format(escape(response.meta),"password" if response.status==ResponseCodes.INPUT_NEEDED_SENSITIVE else "text",escape(url)))
    174 elif response.status==ResponseCodes.GENERIC_SUCCESS:
    175     mime = response.meta
    176     content = response.content
    177     mimeparsed = mimeparse.parse_mime(mime)
    178     if mimeparsed[0][0]=="text": # mimeparsed is ([type, subtype],parameters)
    179         content = content.decode(mimeparsed[1].get("charset","UTF-8"))
    180         if mimeparsed[0][1]=="html":
    181             mime = mime.replace("text/html","text/plain")
    182         elif mimeparsed[0][1]=="gemini":
    183             mime = "text/html; charset=UTF-8"
    184             content = gem2html.gem2html(content,link_callback)
    185         print("Content-Type: "+mime)
    186         print("")
    187         print(content)
    188     else:
    189         print("Content-Type: "+mime)
    190         print()
    191         sys.stdout.flush()
    192         sys.stdout.buffer.write(content)
    193 elif response.status==ResponseCodes.GENERIC_REDIRECT:
    194     print("Content-Type: text/html")
    195     print()
    196     print("""<!DOCTYPE html>
    197 <html>
    198 <head>
    199 <title>Redirecting...</title>
    200 <meta charset="UTF-8">
    201 <meta name="viewport" content="width=device-width, initial-scale=1">
    202 <link rel="stylesheet" href="gem.css">
    203 </head>
    204 <body>
    205 """)
    206     print("<p>The current page ({}) wishes to redirect you to {}.</p>".format(escape(url),escape(response.meta)))
    207     tmp, tmp2 = link_callback(response.meta,"Click here to follow this redirect.")
    208     print("<p><a href={}>{}</a></p>".format(escape(tmp),escape(tmp2)))
    209 elif response.status==ResponseCodes.GENERIC_TEMPFAIL or response.status==ResponseCodes.GENERIC_PERMFAIL:
    210     print("""Content-Type: text/html
    211 
    212 <!DOCTYPE html>
    213 <html>
    214 <head>
    215 <title>Error...</title>
    216 <meta charset="UTF-8">
    217 <meta name="viewport" content="width=device-width, initial-scale=1">
    218 <link rel="stylesheet" href="gem.css">
    219 </head>
    220 <body>
    221 <pre>""")
    222     msg = "Unknown {} error".format("permanent" if response.status==ResponseCodes.GENERIC_PERMFAIL else "temporary")
    223     if response.status==ResponseCodes.TEMPFAIL_SERVER_UNAVAILABLE:
    224         msg = "Server unavailable"
    225     if response.status==ResponseCodes.TEMPFAIL_CGI_ERROR:
    226         msg = "CGI script error"
    227     if response.status==ResponseCodes.TEMPFAIL_PROXY_ERROR:
    228         msg = "Proxy error"
    229     if response.status==ResponseCodes.TEMPFAIL_SLOW_DOWN:
    230         msg = "Slow down"
    231     if response.status==ResponseCodes.PERMFAIL_NOT_FOUND:
    232         msg = "Not Found"
    233     if response.status==ResponseCodes.PERMFAIL_GONE:
    234         msg = "Gone"
    235     if response.status==ResponseCodes.PERMFAIL_PROXY_REFUSED:
    236         msg = "Proxy request refused"
    237     if response.status==ResponseCodes.PERMFAIL_BAD_REQUEST:
    238         msg = "Bad request"
    239     print(f"Error {response.status}: {msg}")
    240     print(f"Server says: {response.meta}")
    241 elif response.status==ResponseCodes.GENERIC_CERTFAIL:
    242     print("""Content-Type: text/html
    243 
    244 <!DOCTYPE html>
    245 <html>
    246 <head>
    247 <title>Cert Error...</title>
    248 <meta charset="UTF-8">
    249 <meta name="viewport" content="width=device-width, initial-scale=1">
    250 <link rel="stylesheet" href="gem.css">
    251 </head>
    252 <body>
    253 <pre>""")
    254     print("Page requires the use of client certificates, which are outside the scope of this proxy.")
    255 else:
    256     print("""Content-Type: text/html
    257 
    258 <!DOCTYPE html>
    259 <html>
    260 <head>
    261 <title>Unknown Error...</title>
    262 <meta charset="UTF-8">
    263 <meta name="viewport" content="width=device-width, initial-scale=1">
    264 <link rel="stylesheet" href="gem.css">
    265 </head>
    266 <body>
    267 <pre>""")
    268     print("Page returned status code {} which is unimplemented.".format(response.status))
    269     print("META = {!r}".format(response.meta))
    270     print("Response body = {!r}".format(response.content))
    271