├── .gitignore ├── README.md ├── app.yaml ├── main.html ├── mirror.py ├── static ├── base.css ├── favicon.ico ├── favicon.png ├── lock.png ├── mirrorrr_logo.png ├── mirrorrr_screenshot.png ├── nolock.png └── robots.txt ├── transform_content.py └── transform_content_test.py /.gitignore: -------------------------------------------------------------------------------- 1 | .svn 2 | *.pyc 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Google App Engine app that Mirrors the content of URLs you supply. Rewrites the fetched page to mirror all content, including images, Flash, Javascript, CSS, and even favicons. You stay within the cache when you follow links. Useful for pulling load off of slashdotted servers. Also can be used to anonymize web access. 2 | 3 | Example live version: 4 | 5 | [https://mirrorrr.appspot.com](https://mirrorrr.appspot.com) 6 | 7 | Instructions on how to setup your own proxy: 8 | 9 | [http://www.hongkiat.com/blog/proxy-with-google-app-engine/](http://www.hongkiat.com/blog/proxy-with-google-app-engine/) 10 | 11 | For POST support and other features, see mirrorrr-plus: 12 | 13 | [https://code.google.com/p/mirrorrr-plus/](https://code.google.com/p/mirrorrr-plus/) 14 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | application: your-app-id-here 2 | version: first 3 | module: default 4 | runtime: python27 5 | api_version: 1 6 | threadsafe: yes 7 | 8 | inbound_services: 9 | - warmup 10 | 11 | instance_class: F1 12 | automatic_scaling: 13 | min_idle_instances: 1 14 | max_idle_instances: 1 15 | max_concurrent_requests: 40 16 | 17 | handlers: 18 | - url: /robots\.txt 19 | static_files: static/robots.txt 20 | upload: static/robots\.txt 21 | 22 | - url: /favicon\.ico 23 | static_files: static/favicon.ico 24 | upload: static/favicon\.ico 25 | secure: optional 26 | 27 | - url: /static 28 | static_dir: static 29 | secure: optional 30 | 31 | - url: /_ah/warmup 32 | script: mirror.app 33 | secure: optional 34 | 35 | - url: /.* 36 | script: mirror.app 37 | secure: optional 38 | -------------------------------------------------------------------------------- /main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | mirror - ɹoɹɹıɯ 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |

mıɾɾoɾ - ɹoɹɹıɯ

15 |
16 | 17 |
18 | 19 |
20 |
21 |
22 | http:// 23 | 24 |
25 |
26 |
27 | 28 |
29 | Fair use: All content belongs to the original copyright holders, respectively. 30 |
31 | 32 |
33 | 34 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /mirror.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2008-2014 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 re 22 | import time 23 | import urllib 24 | import wsgiref.handlers 25 | 26 | from google.appengine.api import memcache 27 | from google.appengine.api import urlfetch 28 | import webapp2 29 | from google.appengine.ext.webapp import template 30 | from google.appengine.runtime import apiproxy_errors 31 | 32 | import transform_content 33 | 34 | ############################################################################### 35 | 36 | DEBUG = False 37 | EXPIRATION_DELTA_SECONDS = 3600 38 | 39 | # DEBUG = True 40 | # EXPIRATION_DELTA_SECONDS = 10 41 | 42 | HTTP_PREFIX = "http://" 43 | 44 | IGNORE_HEADERS = frozenset([ 45 | "set-cookie", 46 | "expires", 47 | "cache-control", 48 | 49 | # Ignore hop-by-hop headers 50 | "connection", 51 | "keep-alive", 52 | "proxy-authenticate", 53 | "proxy-authorization", 54 | "te", 55 | "trailers", 56 | "transfer-encoding", 57 | "upgrade", 58 | ]) 59 | 60 | TRANSFORMED_CONTENT_TYPES = frozenset([ 61 | "text/html", 62 | "text/css", 63 | ]) 64 | 65 | MAX_CONTENT_SIZE = 10 ** 6 - 600 66 | 67 | ############################################################################### 68 | 69 | def get_url_key_name(url): 70 | url_hash = hashlib.sha256() 71 | url_hash.update(url) 72 | return "hash_" + url_hash.hexdigest() 73 | 74 | ############################################################################### 75 | 76 | class MirroredContent(object): 77 | def __init__(self, original_address, translated_address, 78 | status, headers, data, base_url): 79 | self.original_address = original_address 80 | self.translated_address = translated_address 81 | self.status = status 82 | self.headers = headers 83 | self.data = data 84 | self.base_url = base_url 85 | 86 | @staticmethod 87 | def get_by_key_name(key_name): 88 | return memcache.get(key_name) 89 | 90 | @staticmethod 91 | def fetch_and_store(key_name, base_url, translated_address, mirrored_url): 92 | """Fetch and cache a page. 93 | 94 | Args: 95 | key_name: Hash to use to store the cached page. 96 | base_url: The hostname of the page that's being mirrored. 97 | translated_address: The URL of the mirrored page on this site. 98 | mirrored_url: The URL of the original page. Hostname should match 99 | the base_url. 100 | 101 | Returns: 102 | A new MirroredContent object, if the page was successfully retrieved. 103 | None if any errors occurred or the content could not be retrieved. 104 | """ 105 | logging.debug("Fetching '%s'", mirrored_url) 106 | try: 107 | response = urlfetch.fetch(mirrored_url) 108 | except (urlfetch.Error, apiproxy_errors.Error): 109 | logging.exception("Could not fetch URL") 110 | return None 111 | 112 | adjusted_headers = {} 113 | for key, value in response.headers.iteritems(): 114 | adjusted_key = key.lower() 115 | if adjusted_key not in IGNORE_HEADERS: 116 | adjusted_headers[adjusted_key] = value 117 | 118 | content = response.content 119 | page_content_type = adjusted_headers.get("content-type", "") 120 | for content_type in TRANSFORMED_CONTENT_TYPES: 121 | # startswith() because there could be a 'charset=UTF-8' in the header. 122 | if page_content_type.startswith(content_type): 123 | content = transform_content.TransformContent(base_url, mirrored_url, 124 | content) 125 | break 126 | 127 | new_content = MirroredContent( 128 | base_url=base_url, 129 | original_address=mirrored_url, 130 | translated_address=translated_address, 131 | status=response.status_code, 132 | headers=adjusted_headers, 133 | data=content) 134 | 135 | # Do not memcache content over 1MB 136 | if len(content) < MAX_CONTENT_SIZE: 137 | if not memcache.add(key_name, new_content, time=EXPIRATION_DELTA_SECONDS): 138 | logging.error('memcache.add failed: key_name = "%s", ' 139 | 'original_url = "%s"', key_name, mirrored_url) 140 | else: 141 | logging.warning("Content is over 1MB; not memcached") 142 | 143 | return new_content 144 | 145 | ############################################################################### 146 | 147 | class WarmupHandler(webapp2.RequestHandler): 148 | def get(self): 149 | pass 150 | 151 | 152 | class BaseHandler(webapp2.RequestHandler): 153 | def get_relative_url(self): 154 | slash = self.request.url.find("/", len(self.request.scheme + "://")) 155 | if slash == -1: 156 | return "/" 157 | return self.request.url[slash:] 158 | 159 | def is_recursive_request(self): 160 | if "AppEngine-Google" in self.request.headers.get("User-Agent", ""): 161 | logging.warning("Ignoring recursive request by user-agent=%r; ignoring") 162 | self.error(404) 163 | return True 164 | return False 165 | 166 | 167 | class HomeHandler(BaseHandler): 168 | def get(self): 169 | if self.is_recursive_request(): 170 | return 171 | 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 | # Do this dictionary construction here, to decouple presentation from 182 | # how we store data. 183 | secure_url = None 184 | if self.request.scheme == "http": 185 | secure_url = "https://%s%s" % (self.request.host, self.request.path_qs) 186 | context = { 187 | "secure_url": secure_url, 188 | } 189 | self.response.out.write(template.render("main.html", context)) 190 | 191 | 192 | class MirrorHandler(BaseHandler): 193 | def get(self, base_url): 194 | if self.is_recursive_request(): 195 | return 196 | 197 | assert base_url 198 | 199 | # Log the user-agent and referrer, to see who is linking to us. 200 | logging.debug('User-Agent = "%s", Referrer = "%s"', 201 | self.request.user_agent, 202 | self.request.referer) 203 | logging.debug('Base_url = "%s", url = "%s"', base_url, self.request.url) 204 | 205 | translated_address = self.get_relative_url()[1:] # remove leading / 206 | mirrored_url = HTTP_PREFIX + translated_address 207 | 208 | # Use sha256 hash instead of mirrored url for the key name, since key 209 | # names can only be 500 bytes in length; URLs may be up to 2KB. 210 | key_name = get_url_key_name(mirrored_url) 211 | logging.info("Handling request for '%s' = '%s'", mirrored_url, key_name) 212 | 213 | content = MirroredContent.get_by_key_name(key_name) 214 | cache_miss = False 215 | if content is None: 216 | logging.debug("Cache miss") 217 | cache_miss = True 218 | content = MirroredContent.fetch_and_store(key_name, base_url, 219 | translated_address, 220 | mirrored_url) 221 | if content is None: 222 | return self.error(404) 223 | 224 | for key, value in content.headers.iteritems(): 225 | self.response.headers[key] = value 226 | if not DEBUG: 227 | self.response.headers["cache-control"] = \ 228 | "max-age=%d" % EXPIRATION_DELTA_SECONDS 229 | 230 | self.response.out.write(content.data) 231 | 232 | ############################################################################### 233 | 234 | app = webapp2.WSGIApplication([ 235 | (r"/", HomeHandler), 236 | (r"/main", HomeHandler), 237 | (r"/_ah/warmup", WarmupHandler), 238 | (r"/([^/]+).*", MirrorHandler), 239 | ], debug=DEBUG) 240 | -------------------------------------------------------------------------------- /static/base.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | body { 7 | background-color: #ffffff; 8 | font-family: sans-serif; 9 | margin: 0px; 10 | padding: 8px; 11 | color: #000000; 12 | } 13 | 14 | header { 15 | text-align: center; 16 | font-size: 20px; 17 | letter-spacing: 10px; 18 | margin-bottom: 40px; 19 | margin-top: 40px; 20 | } 21 | 22 | #form_wrapper { 23 | margin-top: 10px; 24 | width: 100%; 25 | } 26 | 27 | #input_wrapper { 28 | width: 500px; 29 | margin-left: auto; 30 | margin-right: auto; 31 | } 32 | 33 | #http_prefix { 34 | vertical-align: middle; 35 | } 36 | 37 | #go_button { 38 | width: 50px; 39 | vertical-align: middle; 40 | } 41 | 42 | #url_entry { 43 | width: 375px; 44 | border: 1px dashed black; 45 | padding: 3px; 46 | margin-left: 3px; 47 | margin-right: 10px; 48 | font-family: arial,helvetica,sans-serif; 49 | font-weight: normal; 50 | color: #959595; 51 | font-size: 14px; 52 | } 53 | 54 | #warning { 55 | font-size: 10px; 56 | margin-top: 20px; 57 | letter-spacing: 3px; 58 | color: #7f7f7f; 59 | text-align: center; 60 | } 61 | 62 | /* secure link */ 63 | .secure { 64 | text-align: center; 65 | margin-top: 50px; 66 | vertical-align: middle; 67 | font-size: 12px; 68 | } 69 | 70 | .secure img { 71 | margin-bottom: -0.3em; 72 | border: 0; 73 | } 74 | 75 | .secure a, .secure a:hover, .secure a:visited, .secure a:active { 76 | color: #000; 77 | text-decoration: none; 78 | } 79 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bslatkin/mirrorrr/9888e1676d67556ac248ceb2cb766a187e525dad/static/favicon.ico -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bslatkin/mirrorrr/9888e1676d67556ac248ceb2cb766a187e525dad/static/favicon.png -------------------------------------------------------------------------------- /static/lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bslatkin/mirrorrr/9888e1676d67556ac248ceb2cb766a187e525dad/static/lock.png -------------------------------------------------------------------------------- /static/mirrorrr_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bslatkin/mirrorrr/9888e1676d67556ac248ceb2cb766a187e525dad/static/mirrorrr_logo.png -------------------------------------------------------------------------------- /static/mirrorrr_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bslatkin/mirrorrr/9888e1676d67556ac248ceb2cb766a187e525dad/static/mirrorrr_screenshot.png -------------------------------------------------------------------------------- /static/nolock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bslatkin/mirrorrr/9888e1676d67556ac248ceb2cb766a187e525dad/static/nolock.png -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | Allow: /main 4 | -------------------------------------------------------------------------------- /transform_content.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2008-2014 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 os 19 | import re 20 | import urlparse 21 | 22 | ################################################################################ 23 | 24 | # URLs that have absolute addresses 25 | ABSOLUTE_URL_REGEX = r"(http(s?):)?//(?P[^\"'> \t\)]+)" 26 | 27 | # URLs that are relative to the base of the current hostname. 28 | BASE_RELATIVE_URL_REGEX = r"/(?!(/)|(http(s?)://)|(url\())(?P[^\"'> \t\)]*)" 29 | 30 | # URLs that have '../' or './' to start off their paths. 31 | TRAVERSAL_URL_REGEX = r"(?P\.(\.)?)/(?!(/)|(http(s?)://)|(url\())(?P[^\"'> \t\)]*)" 32 | 33 | # URLs that are in the same directory as the requested URL. 34 | SAME_DIR_URL_REGEX = r"(?!(/)|(http(s?)://)|(url\())(?P[^\"'> \t\)]+)" 35 | 36 | # URL matches the root directory. 37 | ROOT_DIR_URL_REGEX = r"(?!//(?!>))/(?P)(?=[ \t\n]*[\"'\)>/])" 38 | 39 | # Start of a tag using 'src' or 'href' 40 | TAG_START = r"(?i)\b(?Psrc|href|action|url|background)(?P[\t ]*=[\t ]*)(?P[\"']?)" 41 | 42 | # Start of a CSS import 43 | CSS_IMPORT_START = r"(?i)@import(?P[\t ]+)(?P[\"']?)" 44 | 45 | # CSS url() call 46 | CSS_URL_START = r"(?i)\burl\((?P[\"']?)" 47 | 48 | 49 | REPLACEMENT_REGEXES = [ 50 | (TAG_START + SAME_DIR_URL_REGEX, 51 | "\g\g\g%(accessed_dir)s\g"), 52 | 53 | (TAG_START + TRAVERSAL_URL_REGEX, 54 | "\g\g\g%(accessed_dir)s/\g/\g"), 55 | 56 | (TAG_START + BASE_RELATIVE_URL_REGEX, 57 | "\g\g\g/%(base)s/\g"), 58 | 59 | (TAG_START + ROOT_DIR_URL_REGEX, 60 | "\g\g\g/%(base)s/"), 61 | 62 | # Need this because HTML tags could end with '/>', which confuses the 63 | # tag-matching regex above, since that's the end-of-match signal. 64 | (TAG_START + ABSOLUTE_URL_REGEX, 65 | "\g\g\g/\g"), 66 | 67 | (CSS_IMPORT_START + SAME_DIR_URL_REGEX, 68 | "@import\g\g%(accessed_dir)s\g"), 69 | 70 | (CSS_IMPORT_START + TRAVERSAL_URL_REGEX, 71 | "@import\g\g%(accessed_dir)s/\g/\g"), 72 | 73 | (CSS_IMPORT_START + BASE_RELATIVE_URL_REGEX, 74 | "@import\g\g/%(base)s/\g"), 75 | 76 | (CSS_IMPORT_START + ABSOLUTE_URL_REGEX, 77 | "@import\g\g/\g"), 78 | 79 | (CSS_URL_START + SAME_DIR_URL_REGEX, 80 | "url(\g%(accessed_dir)s\g"), 81 | 82 | (CSS_URL_START + TRAVERSAL_URL_REGEX, 83 | "url(\g%(accessed_dir)s/\g/\g"), 84 | 85 | (CSS_URL_START + BASE_RELATIVE_URL_REGEX, 86 | "url(\g/%(base)s/\g"), 87 | 88 | (CSS_URL_START + ABSOLUTE_URL_REGEX, 89 | "url(\g/\g"), 90 | ] 91 | 92 | ################################################################################ 93 | 94 | def TransformContent(base_url, accessed_url, content): 95 | url_obj = urlparse.urlparse(accessed_url) 96 | accessed_dir = os.path.dirname(url_obj.path) 97 | if not accessed_dir.endswith("/"): 98 | accessed_dir += "/" 99 | 100 | for pattern, replacement in REPLACEMENT_REGEXES: 101 | fixed_replacement = replacement % { 102 | "base": base_url, 103 | "accessed_dir": accessed_dir, 104 | } 105 | content = re.sub(pattern, fixed_replacement, content) 106 | return content 107 | -------------------------------------------------------------------------------- /transform_content_test.py: -------------------------------------------------------------------------------- 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 logging 19 | import unittest 20 | 21 | import transform_content 22 | 23 | ################################################################################ 24 | 25 | class TransformTest(unittest.TestCase): 26 | 27 | def _RunTransformTest(self, base_url, accessed_url, original, expected): 28 | tag_tests = [ 29 | '', 30 | "", 31 | "", 32 | "", 33 | "", 35 | "", 36 | "", 37 | '', 38 | "", 39 | "", 40 | "", 41 | "", 43 | "", 44 | "", 45 | "", 46 | "", 47 | '', 48 | '
', 49 | "", 50 | "", 51 | "", 52 | "", 54 | "", 55 | "", 56 | "@import '%s';", 57 | "@import '%s'\nnext line here", 58 | "@import \t '%s';", 59 | "@import %s;", 60 | "@import %s", 61 | '@import "%s";', 62 | '@import "%s"\nnext line here', 63 | "@import url(%s)", 64 | "@import url('%s')", 65 | '@import url("%s")', 66 | "background: transparent url(%s) repeat-x left;", 67 | 'background: transparent url("%s") repeat-x left;', 68 | "background: transparent url('%s') repeat-x left;", 69 | '', 70 | ] 71 | for tag in tag_tests: 72 | test = tag % original 73 | correct = tag % expected 74 | result = transform_content.TransformContent(base_url, accessed_url, test) 75 | logging.info("Test with\n" 76 | "Accessed: %s\n" 77 | "Input : %s\n" 78 | "Received: %s\n" 79 | "Expected: %s", 80 | accessed_url, test, result, correct) 81 | if result != correct: 82 | logging.info("FAIL") 83 | self.assertEquals(correct, result) 84 | 85 | def testBaseTransform(self): 86 | self._RunTransformTest( 87 | "slashdot.org", 88 | "http://slashdot.org", 89 | "//images.slashdot.org/iestyles.css?T_2_5_0_204", 90 | "/images.slashdot.org/iestyles.css?T_2_5_0_204") 91 | 92 | def testAbsolute(self): 93 | self._RunTransformTest( 94 | "slashdot.org", 95 | "http://slashdot.org", 96 | "http://slashdot.org/slashdot_files/all-minified.js", 97 | "/slashdot.org/slashdot_files/all-minified.js") 98 | 99 | def testRelative(self): 100 | self._RunTransformTest( 101 | "slashdot.org", 102 | "http://slashdot.org", 103 | "images/foo.html", 104 | "/slashdot.org/images/foo.html") 105 | 106 | def testUpDirectory(self): 107 | self._RunTransformTest( 108 | "a248.e.akamai.net", 109 | "http://a248.e.akamai.net/foobar/is/the/path.html", 110 | "../layout/mh_phone-home.png", 111 | "/a248.e.akamai.net/foobar/is/the/../layout/mh_phone-home.png") 112 | 113 | def testSameDirectoryRelative(self): 114 | self._RunTransformTest( 115 | "a248.e.akamai.net", 116 | "http://a248.e.akamai.net/foobar/is/the/path.html", 117 | "./layout/mh_phone-home.png", 118 | "/a248.e.akamai.net/foobar/is/the/./layout/mh_phone-home.png") 119 | 120 | def testSameDirectory(self): 121 | self._RunTransformTest( 122 | "a248.e.akamai.net", 123 | "http://a248.e.akamai.net/foobar/is/the/path.html", 124 | "mh_phone-home.png", 125 | "/a248.e.akamai.net/foobar/is/the/mh_phone-home.png") 126 | 127 | def testSameDirectoryNoParent(self): 128 | self._RunTransformTest( 129 | "a248.e.akamai.net", 130 | "http://a248.e.akamai.net/path.html", 131 | "mh_phone-home.png", 132 | "/a248.e.akamai.net/mh_phone-home.png") 133 | 134 | def testSameDirectoryWithParent(self): 135 | self._RunTransformTest( 136 | "a248.e.akamai.net", 137 | ("http://a248.e.akamai.net/7/248/2041/1447/store.apple.com" 138 | "/rs1/css/aos-screen.css"), 139 | "aos-layout.css", 140 | ("/a248.e.akamai.net/7/248/2041/1447/store.apple.com" 141 | "/rs1/css/aos-layout.css")) 142 | 143 | def testRootDirectory(self): 144 | self._RunTransformTest( 145 | "a248.e.akamai.net", 146 | "http://a248.e.akamai.net/foobar/is/the/path.html", 147 | "/", 148 | "/a248.e.akamai.net/") 149 | 150 | def testSecureContent(self): 151 | self._RunTransformTest( 152 | "slashdot.org", 153 | "https://slashdot.org", 154 | "https://images.slashdot.org/iestyles.css?T_2_5_0_204", 155 | "/images.slashdot.org/iestyles.css?T_2_5_0_204") 156 | 157 | def testPartiallySecureContent(self): 158 | self._RunTransformTest( 159 | "slashdot.org", 160 | "http://slashdot.org", 161 | "https://images.slashdot.org/iestyles.css?T_2_5_0_204", 162 | "/images.slashdot.org/iestyles.css?T_2_5_0_204") 163 | 164 | ################################################################################ 165 | 166 | if __name__ == "__main__": 167 | logging.getLogger().setLevel(logging.DEBUG) 168 | unittest.main() 169 | --------------------------------------------------------------------------------