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