Advertisement
Guest User

mirror.py

a guest
Sep 19th, 2014
277
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 9.13 KB | None | 0 0
  1. #!/usr/bin/env python
  2. # Copyright 2008 Brett Slatkin
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15.  
  16. __author__ = "Brett Slatkin (bslatkin@gmail.com)"
  17.  
  18. import datetime
  19. import hashlib
  20. import logging
  21. import pickle
  22. import re
  23. import time
  24. import urllib
  25. import wsgiref.handlers
  26.  
  27. from google.appengine.api import memcache
  28. from google.appengine.api import urlfetch
  29. from google.appengine.ext import db
  30. import webapp2
  31. from google.appengine.ext.webapp import template
  32. from google.appengine.runtime import apiproxy_errors
  33.  
  34. import transform_content
  35.  
  36. ################################################################################
  37.  
  38. DEBUG = False
  39. EXPIRATION_DELTA_SECONDS = 3600
  40. EXPIRATION_RECENT_URLS_SECONDS = 90
  41.  
  42. ## DEBUG = True
  43. ## EXPIRATION_DELTA_SECONDS = 10
  44. ## EXPIRATION_RECENT_URLS_SECONDS = 1
  45.  
  46. HTTP_PREFIX = "http://"
  47. HTTPS_PREFIX = "http://"
  48.  
  49. IGNORE_HEADERS = frozenset([
  50. 'set-cookie',
  51. 'expires',
  52. 'cache-control',
  53.  
  54. # Ignore hop-by-hop headers
  55. 'connection',
  56. 'keep-alive',
  57. 'proxy-authenticate',
  58. 'proxy-authorization',
  59. 'te',
  60. 'trailers',
  61. 'transfer-encoding',
  62. 'upgrade',
  63. ])
  64.  
  65. TRANSFORMED_CONTENT_TYPES = frozenset([
  66. "text/html",
  67. "text/css",
  68. ])
  69.  
  70.  
  71. MAX_CONTENT_SIZE = 10 ** 6
  72.  
  73. MAX_URL_DISPLAY_LENGTH = 50
  74.  
  75. ################################################################################
  76.  
  77. def get_url_key_name(url):
  78. url_hash = hashlib.sha256()
  79. url_hash.update(url)
  80. return "hash_" + url_hash.hexdigest()
  81.  
  82. ################################################################################
  83.  
  84. class EntryPoint(db.Model):
  85. translated_address = db.TextProperty(required=True)
  86. last_updated = db.DateTimeProperty(auto_now=True)
  87. display_address = db.TextProperty()
  88.  
  89.  
  90. class MirroredContent(object):
  91. def __init__(self, original_address, translated_address,
  92. status, headers, data, base_url):
  93. self.original_address = original_address
  94. self.translated_address = translated_address
  95. self.status = status
  96. self.headers = headers
  97. self.data = data
  98. self.base_url = base_url
  99.  
  100. @staticmethod
  101. def get_by_key_name(key_name):
  102. return memcache.get(key_name)
  103.  
  104. @staticmethod
  105. def fetch_and_store(key_name, base_url, translated_address, mirrored_url):
  106. """Fetch and cache a page.
  107.  
  108. Args:
  109. key_name: Hash to use to store the cached page.
  110. base_url: The hostname of the page that's being mirrored.
  111. translated_address: The URL of the mirrored page on this site.
  112. mirrored_url: The URL of the original page. Hostname should match
  113. the base_url.
  114.  
  115. Returns:
  116. A new MirroredContent object, if the page was successfully retrieved.
  117. None if any errors occurred or the content could not be retrieved.
  118. """
  119.  
  120. logging.debug("Fetching '%s'", mirrored_url)
  121. try:
  122. response = urlfetch.fetch(mirrored_url)
  123. except (urlfetch.Error, apiproxy_errors.Error):
  124. logging.exception("Could not fetch URL")
  125. return None
  126.  
  127. adjusted_headers = {}
  128. for key, value in response.headers.iteritems():
  129. adjusted_key = key.lower()
  130. if adjusted_key not in IGNORE_HEADERS:
  131. adjusted_headers[adjusted_key] = value
  132.  
  133. content = response.content
  134. page_content_type = adjusted_headers.get("content-type", "")
  135. for content_type in TRANSFORMED_CONTENT_TYPES:
  136. # Startswith() because there could be a 'charset=UTF-8' in the header.
  137. if page_content_type.startswith(content_type):
  138. content = transform_content.TransformContent(base_url, mirrored_url,
  139. content)
  140. break
  141.  
  142. # If the transformed content is over 1MB, truncate it (yikes!)
  143. if len(content) > MAX_CONTENT_SIZE:
  144. logging.warning('Content is over 1MB; truncating')
  145. content = content[:MAX_CONTENT_SIZE]
  146.  
  147. new_content = MirroredContent(
  148. base_url=base_url,
  149. original_address=mirrored_url,
  150. translated_address=translated_address,
  151. status=response.status_code,
  152. headers=adjusted_headers,
  153. data=content)
  154. if not memcache.add(key_name, new_content, time=EXPIRATION_DELTA_SECONDS):
  155. logging.error('memcache.add failed: key_name = "%s", '
  156. 'original_url = "%s"', key_name, mirrored_url)
  157.  
  158. return new_content
  159.  
  160. ################################################################################
  161.  
  162. class BaseHandler(webapp2.RequestHandler):
  163. def get_relative_url(self):
  164. slash = self.request.url.find("/", len(self.request.scheme + "://"))
  165. if slash == -1:
  166. return "/"
  167. return self.request.url[slash:]
  168.  
  169.  
  170. class HomeHandler(BaseHandler):
  171. def get(self):
  172. # Handle the input form to redirect the user to a relative url
  173. form_url = self.request.get("url")
  174. if form_url:
  175. # Accept URLs that still have a leading 'http://'
  176. inputted_url = urllib.unquote(form_url)
  177. if inputted_url.startswith(HTTP_PREFIX):
  178. inputted_url = inputted_url[len(HTTP_PREFIX):]
  179. return self.redirect("/" + inputted_url)
  180.  
  181. latest_urls = memcache.get('latest_urls')
  182. if latest_urls is None:
  183. latest_urls = EntryPoint.gql("ORDER BY last_updated DESC").fetch(25)
  184.  
  185. # Generate a display address that truncates the URL, adds an ellipsis.
  186. # This is never actually saved in the Datastore.
  187. for entry_point in latest_urls:
  188. entry_point.display_address = \
  189. entry_point.translated_address[:MAX_URL_DISPLAY_LENGTH]
  190. if len(entry_point.display_address) == MAX_URL_DISPLAY_LENGTH:
  191. entry_point.display_address += '...'
  192.  
  193. if not memcache.add('latest_urls', latest_urls,
  194. time=EXPIRATION_RECENT_URLS_SECONDS):
  195. logging.error('memcache.add failed: latest_urls')
  196.  
  197. # Do this dictionary construction here, to decouple presentation from
  198. # how we store data.
  199. secure_url = None
  200. if self.request.scheme == "http":
  201. secure_url = "https://mirrorrr.appspot.com"
  202. context = {
  203. "latest_urls": latest_urls,
  204. "secure_url": secure_url,
  205. }
  206. self.response.out.write(template.render("main.html", context))
  207.  
  208.  
  209. class MirrorHandler(BaseHandler):
  210. def get(self, base_url):
  211. assert base_url
  212.  
  213. # Log the user-agent and referrer, to see who is linking to us.
  214. logging.debug('User-Agent = "%s", Referrer = "%s"',
  215. self.request.user_agent,
  216. self.request.referer)
  217. logging.debug('Base_url = "%s", url = "%s"', base_url, self.request.url)
  218.  
  219. translated_address = self.get_relative_url()[1:] # remove leading /
  220. mirrored_url = HTTP_PREFIX + translated_address
  221.  
  222. # Use sha256 hash instead of mirrored url for the key name, since key
  223. # names can only be 500 bytes in length; URLs may be up to 2KB.
  224. key_name = get_url_key_name(mirrored_url)
  225. logging.info("Handling request for '%s' = '%s'", mirrored_url, key_name)
  226.  
  227. content = MirroredContent.get_by_key_name(key_name)
  228. cache_miss = False
  229. if content is None:
  230. logging.debug("Cache miss")
  231. cache_miss = True
  232. content = MirroredContent.fetch_and_store(key_name, base_url,
  233. translated_address,
  234. mirrored_url)
  235. if content is None:
  236. return self.error(404)
  237.  
  238. # Store the entry point down here, once we know the request is good and
  239. # there has been a cache miss (i.e., the page expired). If the referrer
  240. # wasn't local, or it was '/', then this is an entry point.
  241. if (cache_miss and
  242. 'Googlebot' not in self.request.user_agent and
  243. 'Slurp' not in self.request.user_agent and
  244. (not self.request.referer.startswith(self.request.host_url) or
  245. self.request.referer == self.request.host_url + "/")):
  246. # Ignore favicons as entry points; they're a common browser fetch on
  247. # every request for a new site that we need to special case them here.
  248. if not self.request.url.endswith("favicon.ico"):
  249. logging.info("Inserting new entry point")
  250. entry_point = EntryPoint(
  251. key_name=key_name,
  252. translated_address=translated_address)
  253. try:
  254. entry_point.put()
  255. except (db.Error, apiproxy_errors.Error):
  256. logging.exception("Could not insert EntryPoint")
  257.  
  258. for key, value in content.headers.iteritems():
  259. self.response.headers[key] = value
  260. if not DEBUG:
  261. self.response.headers['cache-control'] = \
  262. 'max-age=%d' % EXPIRATION_DELTA_SECONDS
  263.  
  264. self.response.out.write(content.data)
  265.  
  266. app = webapp2.WSGIApplication([
  267. (r"/", HomeHandler),
  268. (r"/main", HomeHandler),
  269. (r"/([^/]+).*", MirrorHandler)
  270. ], debug=DEBUG)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement