1 #!/usr/bin/python 2 # Copyright (c) 2010 The Chromium Authors. All rights reserved. 3 # Use of this source code is governed by a BSD-style license that can be 4 # found in the LICENSE file. 5 6 import cgi 7 import logging 8 import re 9 import os 10 11 from google.appengine.ext import webapp 12 from google.appengine.ext.webapp.util import run_wsgi_app 13 from google.appengine.api import memcache 14 from google.appengine.api import urlfetch 15 16 # TODO(nickbaum): unit tests 17 18 19 # TODO(nickbaum): is this the right way to do constants? 20 class Channel(): 21 def __init__(self, name, tag): 22 self.name = name 23 self.tag = tag 24 25 # TODO(nickbaum): unit test this 26 def matchPath(self, path): 27 match = "/" + self.name + "/" 28 if path[0:len(match)] == match: 29 return true 30 else: 31 return false 32 33 Channel.DEV = Channel("dev", "2.0-dev") 34 Channel.BETA = Channel("beta", "1.1-beta") 35 Channel.STABLE = Channel("stable", "") 36 Channel.CHANNELS = [Channel.DEV, Channel.BETA, Channel.STABLE] 37 Channel.TRUNK = Channel("trunk", "") 38 Channel.DEFAULT = Channel.STABLE 39 40 41 DEFAULT_CACHE_TIME = 300 42 43 44 class MainPage(webapp.RequestHandler): 45 # get page from memcache, or else fetch it from src 46 def get(self): 47 path = os.path.realpath(os.path.join('/', self.request.path)) 48 # special path to invoke the unit tests 49 # TODO(nickbaum): is there a less ghetto way to invoke the unit test? 50 if path == "/test": 51 self.unitTest() 52 return 53 # if root, redirect to index.html 54 # TODO(nickbaum): this doesn't handle /chrome/extensions/trunk, etc 55 if (path == "/chrome/extensions") or (path == "chrome/extensions/"): 56 self.redirect("/chrome/extensions/index.html") 57 return 58 # else remove prefix 59 if(path[:18] == "/chrome/extensions"): 60 path = path[18:] 61 # TODO(nickbaum): there's a subtle bug here: if there are two instances of the app, 62 # their default caches will override each other. This is bad! 63 result = memcache.get(path) 64 if result is None: 65 logging.info("Cache miss: " + path) 66 url = self.getSrcUrl(path) 67 if (url[1] is not Channel.TRUNK) and (url[0] != "http://src.chromium.org/favicon.ico"): 68 branch = self.getBranch(url[1]) 69 url = url[0] % branch 70 else: 71 url = url[0] 72 logging.info("Path: " + self.request.path) 73 logging.info("Url: " + url) 74 try: 75 result = urlfetch.fetch(url + self.request.query_string) 76 if result.status_code != 200: 77 logging.error("urlfetch failed: " + url) 78 # TODO(nickbaum): what should we do when the urlfetch fails? 79 except: 80 logging.error("urlfetch failed: " + url) 81 # TODO(nickbaum): what should we do when the urlfetch fails? 82 try: 83 if not memcache.add(path, result, DEFAULT_CACHE_TIME): 84 logging.error("Memcache set failed.") 85 except: 86 logging.error("Memcache set failed.") 87 for key in result.headers: 88 self.response.headers[key] = result.headers[key] 89 self.response.out.write(result.content) 90 91 def head(self): 92 self.get() 93 94 # get the src url corresponding to the request 95 # returns a tuple of the url and the branch 96 # this function is the only part that is unit tested 97 def getSrcUrl(self, path): 98 # from the path they provided, figure out which channel they requested 99 # TODO(nickbaum) clean this logic up 100 # find the first subdirectory of the path 101 path = path.split('/', 2) 102 url = "http://src.chromium.org/viewvc/chrome/" 103 channel = None 104 # if there's no subdirectory, choose the default channel 105 # otherwise, figure out if the subdirectory corresponds to a channel 106 if len(path) == 2: 107 path.append("") 108 if path[1] == "": 109 channel = Channel.DEFAULT 110 if(Channel.DEFAULT == Channel.TRUNK): 111 url = url + "trunk/src/chrome/" 112 else: 113 url = url + "branches/%s/src/chrome/" 114 path = "" 115 elif path[1] == Channel.TRUNK.name: 116 url = url + "trunk/src/chrome/" 117 channel = Channel.TRUNK 118 path = path[2] 119 else: 120 # otherwise, run through the different channel options 121 for c in Channel.CHANNELS: 122 if(path[1] == c.name): 123 channel = c 124 url = url + "branches/%s/src/chrome/" 125 path = path[2] 126 break 127 # if the subdirectory doesn't correspond to a channel, use the default 128 if channel is None: 129 channel = Channel.DEFAULT 130 if(Channel.DEFAULT == Channel.TRUNK): 131 url = url + "trunk/src/chrome/" 132 else: 133 url = url + "branches/%s/src/chrome/" 134 if path[2] != "": 135 path = path[1] + "/" + path[2] 136 else: 137 path = path[1] 138 # special cases 139 # TODO(nickbaum): this is super cumbersome to maintain 140 if path == "third_party/jstemplate/jstemplate_compiled.js": 141 url = url + path 142 elif path == "api/extension_api.json": 143 url = url + "common/extensions/" + path 144 elif path == "favicon.ico": 145 url = "http://src.chromium.org/favicon.ico" 146 else: 147 if path == "": 148 path = "index.html" 149 url = url + "common/extensions/docs/" + path 150 return [url, channel] 151 152 # get the current version number for the channel requested (dev, beta or stable) 153 # TODO(nickbaum): move to Channel object 154 def getBranch(self, channel): 155 branch = memcache.get(channel.name) 156 if branch is None: 157 # query Omaha to figure out which version corresponds to this channel 158 postdata = """<?xml version="1.0" encoding="UTF-8"?> 159 <o:gupdate xmlns:o="http://www.google.com/update2/request" protocol="2.0" testsource="crxdocs"> 160 <o:app appid="{8A69D345-D564-463C-AFF1-A69D9E530F96}" version="0.0.0.0" lang=""> 161 <o:updatecheck tag="%s" installsource="ondemandcheckforupdates" /> 162 </o:app> 163 </o:gupdate> 164 """ % channel.tag 165 result = urlfetch.fetch(url="https://tools.google.com/service/update2", 166 payload=postdata, 167 method=urlfetch.POST, 168 headers={'Content-Type': 'application/x-www-form-urlencoded', 169 'X-USER-IP': '72.1.1.1'}) 170 if result.status_code != 200: 171 logging.error("urlfetch failed.") 172 # TODO(nickbaum): what should we do when the urlfetch fails? 173 # find branch in response 174 match = re.search(r'<updatecheck Version="\d+\.\d+\.(\d+)\.\d+"', result.content) 175 if match is None: 176 logging.error("Version number not found: " + result.content) 177 #TODO(nickbaum): should we fall back on trunk in this case? 178 branch = match.group(1) 179 # TODO(nickbaum): make cache time a constant 180 if not memcache.add(channel.name, branch, DEFAULT_CACHE_TIME): 181 logging.error("Memcache set failed.") 182 return branch 183 184 # TODO(nickbaum): is there a more elegant way to write this unit test? 185 # I deliberately kept it dumb to avoid errors sneaking in, but it's so verbose... 186 # TODO(nickbaum): should I break this up into multiple files? 187 def unitTest(self): 188 self.response.out.write("Testing TRUNK<br/>") 189 self.check("/trunk/", "http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/index.html", Channel.TRUNK) 190 self.check("/trunk/index.html", "http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/index.html", Channel.TRUNK) 191 self.check("/trunk/getstarted.html", "http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/getstarted.html", Channel.TRUNK) 192 self.check("/trunk/images/toolstrip.png", "http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/images/toolstrip.png", Channel.TRUNK) 193 194 self.response.out.write("<br/>Testing DEV<br/>") 195 self.check("/dev/", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/index.html", Channel.DEV) 196 self.check("/dev/index.html", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/index.html", Channel.DEV) 197 self.check("/dev/getstarted.html", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/getstarted.html", Channel.DEV) 198 self.check("/dev/images/toolstrip.png", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/images/toolstrip.png", Channel.DEV) 199 200 self.response.out.write("<br/>Testing BETA<br/>") 201 self.check("/beta/", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/index.html", Channel.BETA) 202 self.check("/beta/index.html", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/index.html", Channel.BETA) 203 self.check("/beta/getstarted.html", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/getstarted.html", Channel.BETA) 204 self.check("/beta/images/toolstrip.png", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/images/toolstrip.png", Channel.BETA) 205 206 self.response.out.write("<br/>Testing STABLE<br/>") 207 self.check("/stable/", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/index.html", Channel.STABLE) 208 self.check("/stable/index.html", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/index.html", Channel.STABLE) 209 self.check("/stable/getstarted.html", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/getstarted.html", Channel.STABLE) 210 self.check("/stable/images/toolstrip.png", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/images/toolstrip.png", Channel.STABLE) 211 212 self.response.out.write("<br/>Testing jstemplate_compiled.js<br/>") 213 self.check("/trunk/third_party/jstemplate/jstemplate_compiled.js", "http://src.chromium.org/viewvc/chrome/trunk/src/chrome/third_party/jstemplate/jstemplate_compiled.js", Channel.TRUNK) 214 self.check("/dev/third_party/jstemplate/jstemplate_compiled.js", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/third_party/jstemplate/jstemplate_compiled.js", Channel.DEV) 215 self.check("/beta/third_party/jstemplate/jstemplate_compiled.js", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/third_party/jstemplate/jstemplate_compiled.js", Channel.BETA) 216 self.check("/stable/third_party/jstemplate/jstemplate_compiled.js", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/third_party/jstemplate/jstemplate_compiled.js", Channel.STABLE) 217 218 self.response.out.write("<br/>Testing extension_api.json<br/>") 219 self.check("/trunk/api/extension_api.json", "http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/api/extension_api.json", Channel.TRUNK) 220 self.check("/dev/api/extension_api.json", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/api/extension_api.json", Channel.DEV) 221 self.check("/beta/api/extension_api.json", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/api/extension_api.json", Channel.BETA) 222 self.check("/stable/api/extension_api.json", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/api/extension_api.json", Channel.STABLE) 223 224 self.response.out.write("<br/>Testing favicon.ico<br/>") 225 self.check("/trunk/favicon.ico", "http://src.chromium.org/favicon.ico", Channel.TRUNK) 226 self.check("/dev/favicon.ico", "http://src.chromium.org/favicon.ico", Channel.DEV) 227 self.check("/beta/favicon.ico", "http://src.chromium.org/favicon.ico", Channel.BETA) 228 self.check("/stable/favicon.ico", "http://src.chromium.org/favicon.ico", Channel.STABLE) 229 230 self.response.out.write("<br/>Testing DEFAULT<br/>") 231 temp = Channel.DEFAULT 232 Channel.DEFAULT = Channel.DEV 233 self.check("/", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/index.html", Channel.DEV) 234 self.check("/index.html", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/index.html", Channel.DEV) 235 self.check("/getstarted.html", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/getstarted.html", Channel.DEV) 236 self.check("/images/toolstrip.png", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/images/toolstrip.png", Channel.DEV) 237 self.check("/third_party/jstemplate/jstemplate_compiled.js", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/third_party/jstemplate/jstemplate_compiled.js", Channel.DEV) 238 self.check("/api/extension_api.json", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/api/extension_api.json", Channel.DEV) 239 self.check("/css/ApiRefStyles.css", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/css/ApiRefStyles.css", Channel.DEV) 240 self.check("/favicon.ico", "http://src.chromium.org/favicon.ico", Channel.DEV) 241 242 self.response.out.write("<br/>Testing DEFAULT (trunk)<br/>") 243 Channel.DEFAULT = Channel.TRUNK 244 self.check("/", "http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/index.html", Channel.TRUNK) 245 self.check("/index.html", "http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/index.html", Channel.TRUNK) 246 self.check("/getstarted.html", "http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/getstarted.html", Channel.TRUNK) 247 self.check("/images/toolstrip.png", "http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/images/toolstrip.png", Channel.TRUNK) 248 self.check("/third_party/jstemplate/jstemplate_compiled.js", "http://src.chromium.org/viewvc/chrome/trunk/src/chrome/third_party/jstemplate/jstemplate_compiled.js", Channel.TRUNK) 249 self.check("/api/extension_api.json", "http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/api/extension_api.json", Channel.TRUNK) 250 self.check("/css/ApiRefStyles.css", "http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/css/ApiRefStyles.css", Channel.TRUNK) 251 self.check("/favicon.ico", "http://src.chromium.org/favicon.ico", Channel.TRUNK) 252 Channel.DEFAULT = temp 253 254 return 255 256 # utility function for my unit test 257 # checks that getSrcUrl(path) returns the expected values 258 # TODO(nickbaum): can this be replaced by assert or something similar? 259 def check(self, path, expectedUrl, expectedChannel): 260 actual = self.getSrcUrl(path) 261 if (actual[0] != expectedUrl): 262 self.response.out.write('<span style="color:#f00;">Failure:</span> path ' + path + " gave url " + actual[0] + "<br/>") 263 elif (actual[1] != expectedChannel): 264 self.response.out.write('<span style="color:#f00;">Failure:</span> path ' + path + " gave branch " + actual[1].name + "<br/>") 265 else: 266 self.response.out.write("Path " + path + ' <span style="color:#0f0;">OK</span><br/>') 267 return 268 269 270 application = webapp.WSGIApplication([ 271 ('/.*', MainPage), 272 ], debug=False) 273 274 275 def main(): 276 run_wsgi_app(application) 277 278 279 if __name__ == '__main__': 280 main() 281