Package gavo :: Package web :: Module root
[frames] | no frames]

Source Code for Module gavo.web.root

  1  """ 
  2  The root resource of the data center. 
  3  """ 
  4   
  5  #c Copyright 2008-2019, the GAVO project 
  6  #c 
  7  #c This program is free software, covered by the GNU GPL.  See the 
  8  #c COPYING file in the source distribution. 
  9   
 10   
 11  from __future__ import print_function 
 12   
 13  import os 
 14  import re 
 15  import time 
 16   
 17  from twisted.python import threadable 
 18  threadable.init() 
 19   
 20  from nevow import appserver 
 21  from nevow import compression 
 22  from nevow import inevow 
 23  from nevow import rend 
 24  from nevow import tags as T 
 25  from nevow import static 
 26   
 27  from gavo import base 
 28  from gavo import svcs 
 29  from gavo import utils 
 30  from gavo.imp import formal 
 31  from gavo.web import caching 
 32  from gavo.web import common 
 33  from gavo.web import grend 
 34  from gavo.web import ifpages 
 35  from gavo.web import jsonquery 
 36  from gavo.web import metarender 
 37  from gavo.web import weberrors 
 38   
 39  from gavo.svcs import (UnknownURI, WebRedirect) 
40 41 -def _escape(s):
42 """helps formatDefaultLog. 43 """ 44 if isinstance(s, unicode): 45 s = s.encode('ascii', 'ignore') 46 return "'%s'"%(s.replace('"', '\\"'))
47
48 49 -def formatDefaultLog(timestamp, request):
50 """returns a log line for request in DaCHS' default format. 51 52 It doesn't include IP addresses, referrers or user agents, which means you 53 should be fine as far as processing personal data is concerned. 54 55 The format itself should be compatible with "combined" logs. 56 """ 57 line = ( 58 u'%(ip)s - - %(timestamp)s "%(method)s %(uri)s %(protocol)s" ' 59 u'%(code)d %(length)s "%(referrer)s" "%(agent)s"' % dict( 60 ip="-", 61 timestamp=timestamp, 62 method=_escape(request.method), 63 uri=_escape(request.uri), 64 protocol=_escape(request.clientproto), 65 code=request.code, 66 length=request.sentLength or u"-", 67 referrer="-", 68 agent="-", 69 )) 70 return line
71
72 73 -def getLogFormatter():
74 """returns a log formatter for this site. 75 76 Right now, this just interprets [web]logger. 77 """ 78 logFormat = base.getConfig("web", "logformat") 79 if logFormat=="default": 80 return formatDefaultLog 81 elif logFormat=="combined": 82 from twisted.web import http 83 return http.combinedLogFormatter 84 else: 85 raise NotImplemented("No logger %s"%logFormat)
86
87 88 -def makeDynamicPage(pageClass):
89 """returns a resource that returns a "dynamic" resource of pageClass. 90 91 pageClass must be a rend.Page subclass that is constructed with a 92 request context (like ifpages.LoginPage). We want such things 93 when the pages have some internal state (since you're not supposed 94 to keep such things in the context any more, which I personally agree 95 with). 96 97 The dynamic pages are directly constructed, their locateChild methods 98 are not called (do we want to change this)? 99 """ 100 class DynPage(rend.Page): 101 def renderHTTP(self, ctx): 102 return pageClass(ctx)
103 return DynPage() 104
105 106 @utils.memoized 107 -def makeFaviconPNG():
108 """returns a "small" version of the logo. 109 110 This is used mainly for SAMP logos at them moment. 111 """ 112 from gavo.utils import imgtools 113 imgPath = os.path.join( 114 base.getConfig("webDir"), "nv_static", "logo_medium.png") 115 if not os.path.exists(imgPath): 116 imgPath = base.getPathForDistFile("web/img/logo_medium.png") 117 return static.Data( 118 imgtools.getScaledPNG(imgPath, 62), 119 type="image/png")
120
121 122 -def _authorizeCORS(request):
123 """adds cross-origin authorisation headers if appropriate. 124 125 This evaluates the [web]corsOrigins config item. 126 """ 127 origin = request.getHeader("Origin") 128 if not origin: 129 return 130 pat = base.getConfig("web", "corsoriginpat") 131 if pat and re.match(pat, origin): 132 request.setHeader("Access-Control-Allow-Origin", origin)
133 134 135 # A cache for RD-specific page caches. Each of these maps segments 136 # (tuples) to a finished caching.CachedPage. The argument is the id of the 137 # RD responsible for generating that data. This ensures that pre-computed 138 # data is cleared when the RD is reloaded. 139 base.caches.makeCache("getPageCache", lambda rdId: {})
140 141 142 -class ArchiveService(rend.Page):
143 """The root resource on the data center. 144 145 It does the main dispatching based on four mechanisms: 146 147 0. redirects -- one-segments fragments that redirect somewhere else. 148 This is for "bad" shortcuts corresponding to input directory name 149 exclusively (since it's so messy). These will not match if 150 path has more than one segment. 151 1. statics -- first segment leads to a resource that gets passed any 152 additional segments. 153 2. mappings -- first segment is replaced by something else, processing 154 continues. 155 3. resource based -- consisting of an RD id, a service id, a renderer and 156 possibly further segments. 157 158 The first three mechanisms only look at the first segment to determine 159 any action (except that redirect is skipped if len(segments)>1). 160 161 The statics and mappings are configured on the class level. 162 """ 163 timestampStarted = time.time() 164 statics = {} 165 mappings = {} 166 redirects = {} 167
168 - def __init__(self):
169 rend.Page.__init__(self) 170 self.maintFile = os.path.join(base.getConfig("stateDir"), "MAINT") 171 self.rootSegments = tuple(s for s in 172 base.getConfig("web", "nevowRoot").split("/") if s) 173 self.rootLen = len(self.rootSegments)
174 175 @classmethod
176 - def addRedirect(cls, key, destination):
177 cls.redirects[key.strip("/")] = destination
178 179 @classmethod
180 - def addStatic(cls, key, resource):
181 cls.statics[key] = resource
182 183 @classmethod
184 - def addMapping(cls, key, segments):
185 cls.mappings[key] = segments
186 187 @classmethod
188 - def _addVanityRedirect(cls, src, dest, options):
189 """a helper for parseVanityMap. 190 """ 191 if '!redirect' in options: 192 if "://" in dest: 193 cls.addRedirect(src, dest) 194 else: 195 cls.addRedirect(src, base.makeSitePath(dest)) 196 else: 197 cls.addMapping(src, dest.split("/"))
198 199 @classmethod
200 - def installVanityMap(cls):
201 """builds the redirects prescribed by the system-wide vanity map. 202 """ 203 for src, (dest, options) in svcs.getVanityMap().shortToLong.iteritems(): 204 cls._addVanityRedirect(src, dest, options)
205
206 - def renderHTTP(self, ctx):
207 # this is only ever executed on the root URL. For consistency 208 # (e.g., caching), we route this through locateChild though 209 # we know we're going to return RootPage. locateChild must 210 # thus *never* return self, (). 211 return self.locateChild(ctx, self.rootSegments)
212
213 - def _processCache(self, ctx, service, rendC, segments):
214 """shortcuts if ctx's request can be cached with rendC. 215 216 This function returns a cached item if a page is in the cache and 217 request allows caching, None otherwise. For cacheable requests, 218 it instruments the request such that the page is actually cached. 219 220 Cacheable pages also cause request's lastModified to be set. 221 222 Requests with arguments or a user info are never cacheable. 223 """ 224 request = inevow.IRequest(ctx) 225 if request.method!="GET" or request.args or request.getUser(): 226 return None 227 228 if not rendC.isCacheable(segments, request): 229 return None 230 231 request.setLastModified( 232 max(self.timestampStarted, service.rd.timestampUpdated)) 233 234 cache = base.caches.getPageCache(service.rd.sourceId) 235 segments = tuple(segments) 236 if segments in cache: 237 return compression.CompressingResourceWrapper(cache[segments]) 238 239 caching.instrumentRequestForCaching(request, 240 caching.enterIntoCacheAs(segments, cache)) 241 return None
242
243 - def _locateResourceBasedChild(self, ctx, segments):
244 """returns a standard, resource-based service renderer. 245 246 Their URIs look like <rd id>/<service id>{/<anything>}. 247 248 This works by successively trying to use parts of the query path 249 of increasing length as RD ids. If one matches, the next 250 segment is the service id, and the following one the renderer. 251 252 The remaining segments are returned unconsumed. 253 254 If no RD matches, an UnknwownURI exception is raised. 255 """ 256 for srvInd in range(1, len(segments)): 257 try: 258 rd = base.caches.getRD("/".join(segments[:srvInd])) 259 except base.RDNotFound: 260 continue 261 else: 262 break 263 else: 264 raise UnknownURI("No matching RD") 265 try: 266 subId, rendName = segments[srvInd], segments[srvInd+1] 267 except IndexError: 268 # a URL requesting a default renderer 269 subId, rendName = segments[srvInd], None 270 271 service = rd.getService(subId) 272 if service is None: 273 if rd.hasProperty("superseded-url"): 274 return weberrors.NotFoundPageWithFancyMessage([ 275 "This resource is stale and has been superseded; you should really" 276 " not be directed here. Anyway, you might find what you" 277 " were looking for at this ", 278 T.a(href=rd.getProperty("superseded-url"))["new location"], 279 "."]), () 280 raise UnknownURI("No such service: %s"%subId, rd=rd) 281 282 if not rendName: 283 rendName = service.defaultRenderer 284 if rendName is None: 285 raise UnknownURI("No renderer given and service has no default") 286 try: 287 rendC = svcs.getRenderer(rendName) 288 except Exception as exc: 289 exc.rd = rd 290 raise 291 cached = self._processCache(ctx, service, rendC, segments) 292 if cached: 293 return cached, () 294 else: 295 return rendC(ctx, service), segments[srvInd+2:]
296 297
298 - def locateChild(self, ctx, segments):
299 if False: 300 from gavo.helpers import testtricks 301 testtricks.memdebug() 302 303 request = inevow.IRequest(ctx) 304 if "x-forwarded-host" in request.received_headers: 305 # we need the externally visible host in our request even 306 # when we're behind a reverse proxy. This is a dumb attempt 307 # at fixing such situations. 308 request.setHost(request.getHeader("x-forwarded-host"), 80) 309 if "origin" in request.received_headers: 310 _authorizeCORS(request) 311 312 if segments[0]!='static' and os.path.exists(self.maintFile): 313 return ifpages.ServiceUnavailable(), () 314 if self.rootSegments: 315 if segments[:self.rootLen]!=self.rootSegments: 316 raise UnknownURI("Misconfiguration: Saw a URL outside of the server's" 317 " scope") 318 segments = segments[self.rootLen:] 319 320 curPath = "/".join(segments) 321 # allow // to stand for __system__ like in RDs 322 if curPath.startswith ("/"): 323 segments = ("__system__",)+segments[1:] 324 curPath = "/".join(segments).strip("/") 325 326 curPath = curPath.strip("/") 327 if curPath=="": 328 segments = ("__system__", "services", "root", "fixed") 329 if curPath in self.redirects: 330 raise WebRedirect(self.redirects[curPath]) 331 332 if segments[0] in self.statics: 333 return self.statics[segments[0]], segments[1:] 334 335 if segments[0] in self.mappings: 336 segments = self.mappings[segments[0]]+list(segments[1:]) 337 338 try: 339 res = self._locateResourceBasedChild(ctx, segments) 340 return res 341 except grend.RDBlocked: 342 return static.File(svcs.getTemplatePath("blocked.html")), ()
343 344 345 ArchiveService.addStatic("login", makeDynamicPage(ifpages.LoginPage)) 346 ArchiveService.addStatic("static", ifpages.StaticServer()) 347 ArchiveService.addStatic("robots.txt", makeDynamicPage(ifpages.RobotsTxt)) 348 ArchiveService.addStatic("clientcount", ifpages.CurReaders()) 349 350 # make these self-registering? Or write them out somewhere? 351 ArchiveService.addStatic("getRR", metarender.ResourceRecordMaker()) 352 353 ArchiveService.addStatic('formal.css', formal.defaultCSS) 354 ArchiveService.addStatic('formal.js', formal.formsJS) 355 356 # .well-known right now is only used by ACME 357 ArchiveService.addStatic(".well-known", ifpages.WellKnown()) 358 359 # Let's see how many more of such JSON things we want to have; for now, 360 # since it's just one, let's do it manually 361 ArchiveService.addStatic('_portaljs', jsonquery.PortalPage()) 362 363 if base.getConfig("web", "enabletests"): 364 from gavo.web import webtests 365 ArchiveService.addStatic("test", webtests.Tests()) 366 367 ArchiveService.addStatic("favicon.png", 368 makeFaviconPNG()) 369 if (base.getConfig("web", "favicon") 370 and os.path.exists(base.getConfig("web", "favicon"))): 371 ArchiveService.addStatic("favicon.ico", 372 static.File(base.getConfig("web", "favicon"))) 373 374 ArchiveService.installVanityMap() 375 376 root = ArchiveService() 377 378 site = appserver.NevowSite(root, 379 timeout=300, 380 logFormatter=getLogFormatter()) 381 site.remember(weberrors.DCExceptionHandler()) 382 site.requestFactory = common.Request 383