waybackproxy.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. #!/usr/bin/env python
  2. import base64, re, socket, socketserver, sys, threading, urllib.request, urllib.error, urllib.parse, urllib.parse
  3. from config import *
  4. class ThreadingTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
  5. """TCPServer with ThreadingMixIn added."""
  6. pass
  7. class Handler(socketserver.BaseRequestHandler):
  8. """Main request handler."""
  9. def handle(self):
  10. """Handle a request."""
  11. global DATE
  12. # readline is pretty convenient
  13. f = self.request.makefile()
  14. # read request line
  15. reqline = line = f.readline()
  16. split = line.rstrip('\r\n').split(' ')
  17. http_version = len(split) > 2 and split[2] or 'HTTP/0.9'
  18. if split[0] != 'GET':
  19. # only GET is implemented
  20. return self.error_page(http_version, 501, 'Not Implemented')
  21. # parse the URL
  22. request_url = split[1]
  23. parsed = urllib.parse.urlparse(request_url)
  24. # make a path
  25. path = parsed.path
  26. if parsed.query != '': path += '?' + parsed.query
  27. if path == '': path == '/'
  28. # get the hostname for later
  29. host = parsed.netloc.split(':')
  30. hostname = host[0]
  31. # read out the headers, saving the PAC file host
  32. pac_host = '" + location.host + ":' + str(LISTEN_PORT) # may not actually work
  33. while line.rstrip('\r\n') != '':
  34. line = f.readline()
  35. if line[:6].lower() == 'host: ':
  36. pac_host = line[6:].rstrip('\r\n')
  37. if ':' not in pac_host: # who would run this on port 80 anyway?
  38. pac_host += ':80'
  39. elif line[:21].lower() == 'x-waybackproxy-date: ':
  40. # API for a personal project of mine
  41. new_date = line[21:].rstrip('\r\n')
  42. if DATE != new_date:
  43. DATE = new_date
  44. print('[-] Header requested date', DATE)
  45. try:
  46. if path == '/proxy.pac':
  47. # PAC file to bypass QUICK_IMAGES requests
  48. pac = http_version.encode('ascii', 'ignore') + b''' 200 OK\r\n'''
  49. pac += b'''Content-Type: application/x-ns-proxy-autoconfig\r\n'''
  50. pac += b'''\r\n'''
  51. pac += b'''function FindProxyForURL(url, host)\r\n'''
  52. pac += b'''{\r\n'''
  53. pac += b''' if (shExpMatch(url, "http://web.archive.org/web/*"))\r\n'''
  54. pac += b''' {\r\n'''
  55. pac += b''' return "DIRECT";\r\n'''
  56. pac += b''' }\r\n'''
  57. pac += b''' return "PROXY ''' + pac_host.encode('ascii', 'ignore') + b'''";\r\n'''
  58. pac += b'''}\r\n'''
  59. self.request.sendall(pac)
  60. return
  61. elif hostname == 'web.archive.org':
  62. if path[:5] != '/web/':
  63. # launch settings
  64. return self.handle_settings(parsed.query)
  65. else:
  66. # pass-through requests to web.archive.org
  67. # required for QUICK_IMAGES
  68. _print('[>] [QI] {0}'.format('/'.join(request_url.split('/')[5:])))
  69. conn = urllib.request.urlopen(request_url)
  70. elif GEOCITIES_FIX and hostname == 'www.geocities.com':
  71. # apply GEOCITIES_FIX and pass it through
  72. split = request_url.split('/')
  73. hostname = split[2] = 'www.oocities.org'
  74. request_url = '/'.join(split)
  75. _print('[>] {0}'.format(request_url))
  76. conn = urllib.request.urlopen(request_url)
  77. else:
  78. # get from Wayback
  79. _print('[>] {0}'.format(request_url))
  80. conn = urllib.request.urlopen('http://web.archive.org/web/{0}/{1}'.format(DATE, request_url))
  81. except urllib.error.HTTPError as e:
  82. # an error has been found
  83. _print('[!] {0} {1}'.format(e.code, e.reason))
  84. return self.error_page(http_version, e.code, e.reason)
  85. # get content type
  86. content_type = conn.info().get('Content-Type')
  87. if not CONTENT_TYPE_ENCODING and content_type.find(';') > -1: content_type = content_type[:content_type.find(';')]
  88. # send headers
  89. self.request.sendall(http_version.encode('ascii', 'ignore') + b' 200 OK\r\nContent-Type: ' + content_type.encode('ascii', 'ignore') + b'\r\n\r\n')
  90. # set the mode: [0]wayback [1]oocities
  91. mode = 0
  92. if GEOCITIES_FIX and hostname in ['www.oocities.org', 'www.oocities.com']: mode = 1
  93. if content_type[:9] == 'text/html' in content_type: # HTML
  94. toolbar = mode == 1 # oocities header starts without warning
  95. redirect_page = False
  96. for line in conn:
  97. line = line.rstrip(b'\r\n')
  98. if mode == 0:
  99. if toolbar:
  100. for delimiter in (b'<\!-- END WAYBACK TOOLBAR INSERT -->', b'<\!-- End Wayback Rewrite JS Include -->'):
  101. if re.search(delimiter, line):
  102. # toolbar is done - resume relaying on the next line
  103. toolbar = False
  104. line = re.sub(delimiter, b'', line)
  105. break
  106. if toolbar: continue
  107. elif redirect_page:
  108. # this is a really bad way to deal with Wayback's 302
  109. # pages, but necessary with the way this proxy works
  110. match = re.search(b'<p class="impatient"><a href="/web/(?:[^/]+)/([^"]+)">Impatient\\?</a></p>', line)
  111. if match:
  112. line = b'<title>WaybackProxy Redirect</title><meta http-equiv="refresh" content="0;url='
  113. line += match.group(1)
  114. line += b'"></head><body>If you are not redirected, <a href="'
  115. line += match.group(1)
  116. line += b'">click here</a>.</body></html>'
  117. self.request.sendall(line)
  118. break
  119. continue
  120. if b'<base ' in line.lower():
  121. # fix base
  122. line = re.sub(b'(?:http://web\.archive\.org)?/web/([0-9]+)/', b'', line)
  123. elif line == b'\t\t<title>Internet Archive Wayback Machine</title>':
  124. # redirect 302s - see the redirect_page code above
  125. redirect_page = True
  126. continue
  127. else:
  128. for delimiter in (
  129. b'<\!-- BEGIN WAYBACK TOOLBAR INSERT -->',
  130. b'<script src="//archive\.org/([^"]+)" type="text/javascript"></script>'
  131. ):
  132. if re.search(delimiter, line):
  133. # remove the toolbar - stop relaying from now on
  134. toolbar = True
  135. line = re.sub(delimiter, b'', line)
  136. break
  137. if QUICK_IMAGES:
  138. # QUICK_IMAGES works by intercepting asset URLs (those
  139. # with a date code ending in im_, js_...) and letting the
  140. # proxy pass them through. This may reduce load time
  141. # because Wayback doesn't have to hunt down the closest
  142. # copy of that asset to DATE, as those URLs have specific
  143. # date codes. The only side effect is tainting the HTML
  144. # with web.archive.org URLs.
  145. line = re.sub(b'(?:http://web.archive.org)?/web/([0-9]+)([a-z]+_)/',
  146. b'http://web.archive.org/web/\\1\\2/', line)
  147. line = re.sub(b'(?:http://web.archive.org)?/web/([0-9]+)/', b'', line)
  148. else:
  149. line = re.sub(b'(?:http://web.archive.org)?/web/([^/]+)/', b'', line)
  150. elif mode == 1:
  151. # remove the geocities/oocities-added code, which is
  152. # conveniently wrapped around comments
  153. if toolbar:
  154. if line in (
  155. b'<!-- text above generated by server. PLEASE REMOVE -->',
  156. b'<!-- preceding code added by server. PLEASE REMOVE -->'
  157. ):
  158. toolbar = False
  159. continue
  160. elif line == b'<!-- following code added by server. PLEASE REMOVE -->' \
  161. or line[:54] == b'<!-- text below generated by server. PLEASE REMOVE -->':
  162. toolbar = True
  163. continue
  164. # taint? what taint?
  165. line = line.replace(b'http://oocities.com', b'http://geocities.com')
  166. line = line.replace(b'http://www.oocities.com', b'http://www.geocities.com')
  167. self.request.sendall(line)
  168. self.request.sendall(b'\r\n')
  169. else: # other data
  170. while True:
  171. data = conn.read(1024)
  172. if not data: break
  173. self.request.sendall(data)
  174. self.request.close()
  175. def error_page(self, http_version, code, reason):
  176. """Generate an error page."""
  177. # make error page
  178. errorpage = '<html><head><title>{0} {1}</title></head><body><h1>{1}</h1><p>'.format(code, reason)
  179. # add code information
  180. if code == 404: # page not archived
  181. errorpage += 'This page may not be archived by the Wayback Machine.'
  182. elif code == 403: # not crawled due to robots.txt
  183. errorpage += 'This page was not archived due to a robots.txt block.'
  184. elif code == 501: # method not implemented
  185. errorpage += 'WaybackProxy only implements the GET method.'
  186. else: # another error
  187. errorpage += 'Unknown error. The Wayback Machine may be experiencing technical difficulties.'
  188. errorpage += '</p><hr><i>'
  189. errorpage += self.signature()
  190. errorpage += '</i></body></html>'
  191. # send error page and stop
  192. self.request.sendall('{0} {1} {2}\r\nContent-Type: text/html\r\nContent-Length: {3}\r\n\r\n{4}'.format(http_version, code, reason, len(errorpage), errorpage).encode('utf8', 'ignore'))
  193. self.request.close()
  194. def handle_settings(self, query):
  195. """Generate the settings page."""
  196. global DATE, GEOCITIES_FIX, QUICK_IMAGES, CONTENT_TYPE_ENCODING
  197. if query != '': # handle any parameters that may have been sent
  198. parsed = urllib.parse.parse_qs(query)
  199. if 'date' in parsed: DATE = parsed['date'][0]
  200. GEOCITIES_FIX = 'gcFix' in parsed
  201. QUICK_IMAGES = 'quickImages' in parsed
  202. CONTENT_TYPE_ENCODING = 'ctEncoding' in parsed
  203. # send the page and stop
  204. settingspage = 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n'
  205. settingspage += '<html><head><title>WaybackProxy Settings</title></head><body><p><b>'
  206. settingspage += self.signature()
  207. settingspage += '</b></p><form method="get" action="/"><p>Date to get pages from: <input type="text" name="date" size="8" value="'
  208. settingspage += DATE
  209. settingspage += '"><br><input type="checkbox" name="gcFix"'
  210. if GEOCITIES_FIX: settingspage += ' checked'
  211. settingspage += '> Geocities Fix<br><input type="checkbox" name="quickImages"'
  212. if QUICK_IMAGES: settingspage += ' checked'
  213. settingspage += '> Quick images<br><input type="checkbox" name="ctEncoding"'
  214. if CONTENT_TYPE_ENCODING: settingspage += ' checked'
  215. settingspage += '> Encoding in Content-Type</p><p><input type="submit" value="Save"></p></form></body></html>'
  216. self.request.send(settingspage.encode('utf8', 'ignore'))
  217. self.request.close()
  218. def signature(self):
  219. """Return the server signature."""
  220. return 'WaybackProxy on {0}'.format(socket.gethostname())
  221. print_lock = threading.Lock()
  222. def _print(s, linebreak=True):
  223. print_lock.acquire()
  224. sys.stdout.write(linebreak and (s + '\n') or s)
  225. sys.stdout.flush()
  226. print_lock.release()
  227. def main():
  228. """Starts the server."""
  229. server = ThreadingTCPServer(('', LISTEN_PORT), Handler)
  230. _print('[-] Now listening on port {0}'.format(LISTEN_PORT))
  231. try:
  232. server.serve_forever()
  233. except KeyboardInterrupt: # Ctrl+C to stop
  234. pass
  235. if __name__ == '__main__':
  236. main()