def validate(self): """Hook for late validation so we can use Response. """ resource_fs_path = uri_to_fs(self.app.site_root, self.app.fs_root, self.app.uri_root, self.request.path, raw=True) # Is the app still on the filesystem? if not os.path.isdir(self.app.fs_root): raise Response(404) if self.server.config.__: # Is the site's magic directory still on the filesystem? if self.server.config.__: if not os.path.isdir(self.server.config.__): raise Response( 500, "The site's magic directory has " + "disappeared.") # Protect the magic directory from direct access, but make sure we # can serve a default app.py from there. if resource_fs_path.startswith(self.server.config.__): app_py = os.path.join(self.server.config.__, 'app.py') if not resource_fs_path == app_py: raise Response(404)
def parse_line(self): """Parse and validate the Request-Line. """ # Tokenize the first line as a Request-Line. # ========================================== tokens = self.raw_line.strip().split() if len(tokens) == 3: method, uri, version = tokens elif len(tokens) == 2: method, uri = tokens version = 'HTTP/0.9' else: raise Response( 400, "The Request-Line `%s' " % self.raw_line + "appears to be malformed because it has " + "neither two nor three tokens.") # Validate the URI. # ================= # We build a mapping using the urlparse naming convention for the keys. # Then we do a little validation and cleanup. uri = urlparse.urlparse(uri) keys = ('scheme', 'netloc', 'path', 'parameters', 'query', 'fragment') _uri = {} for i in range(len(uri)): k = keys[i] v = uri[i] _uri[k] = v uri = _uri if not uri['path']: # this catches, e.g., '//foo' raise Response(400) if '%' in uri['path']: uri['path'] = urllib.unquote(uri['path']) # Validate the version. # ===================== # Consider raising Response(505) here ... for 0.9? 2.0? m = HTTP_VERSION.match(version) if not m: raise Response( 400, "The HTTP-Version `%s' appears to " % version + "be malformed because it does not match the " + "pattern `^HTTP/\d+\.\d+$'.") # Save a few absolutely basic things for application use. # ======================================================= self.method = method self.uri = uri self.path = uri['path']
def respond(self): """Execute one transaction. The call to Application.respond is complicated by the fact that in debugging mode we want to drop into the post-mortem debugger when there is an exception (other than Response, of course). Application.respond is expected to raise a Response or other exception. """ # Do some late validation so we can use Response. # =============================================== resource_fs_path = uri_to_fs(self.app.site_root, self.app.fs_root, self.app.uri_root, self.request.path, raw=True) # Is the app still on the filesystem? if not os.path.isdir(self.app.fs_root): raise Response(404) if self.app.__: # Is the app's magic directory still on the filesystem? if not os.path.isdir(self.app.__): raise Response( 500, "The application's magic directory has " + "disappeared.") # Protect the magic directory from direct access. if resource_fs_path.startswith(self.app.__): raise Response(404) # Get out of the way. # =================== if not self.server.debug_mode: self.app.respond(self.request) else: try: self.app.respond(self.request) except Response: raise except: log(90, traceback.format_exc()) pdb.post_mortem(sys.exc_info()[2]) raise # You know something? No soup for you! # ==================================== raise Response( 500, "%s.respond did not raise " % str(application) + "anything.")
def testBasic(self): request = self.make_request("/") expected = Response(200) expected.headers['Last-Modified'] = 'blah blah blah' expected.headers['Content-Type'] = 'text/html' try: self.app.respond(request) except Response, actual: pass
def testHeadersAreOptional(self): response = Response(200) response.headers = {} # the default self.task.deliver(response) expected = [ 'HTTP/1.0 200 OK', 'content-length: 0', 'content-type: application/octet-stream', 'server: stub server', '' ] actual = self.task.channel.getvalue().splitlines() self.assertEqual(expected, actual)
def testBodyNotWrittenFor304(self): response = Response(304) response.body = "Greetings, program!" self.task.deliver(response) expected = [ 'HTTP/1.0 304 Not modified', 'content-length: 19', 'content-type: text/plain', 'server: stub server', '' ] actual = self.task.channel.getvalue().splitlines() self.assertEqual(expected, actual)
def testContentTypeDefaultsToApplicationOctetStream(self): response = Response(200) response.body = "Greetings, program!" self.task.respond(response) expected = [ 'HTTP/1.0 200 OK', 'content-length: 19', 'content-type: application/octet-stream', 'server: stub server', '', 'Greetings, program!' ] actual = self.task.channel.getvalue().splitlines() self.assertEqual(expected, actual)
def testOtherwiseBodyIsWritten(self): response = Response(200) response.body = "Greetings, program!" self.task.deliver(response) expected = [ 'HTTP/1.0 200 OK', 'content-length: 19', 'content-type: application/octet-stream', 'server: stub server', '', 'Greetings, program!' ] actual = self.task.channel.getvalue().splitlines() self.assertEqual(expected, actual)
def testButOnlyForNonStrings(self): response = Response(537) response.body = 'foo' self.task.server.deploy_mode = False self.task.deliver(response) expected = [ 'HTTP/1.0 537 HTTPY App Dev', 'content-length: 3', 'content-type: text/plain', 'server: stub server', '', 'foo' ] actual = self.task.channel.getvalue().splitlines() self.assertEqual(expected, actual)
def testDevelopment_ModifiedSinceIsFalse(self): headers = {'If-Modified-Since': 'Fri, 31 Dec 9999 23:59:59 GMT'} request = self.make_request("/foo.html", headers) expected = Response(200) expected.headers['Last-Modified'] = 'blah blah blah' expected.headers['Content-Type'] = 'text/html' expected.body = 'Greetings, program!' try: self.txn.process(request) except Response, actual: pass
def testButReasonMessageCanBeOverriden(self): response = Response(505) response.body = "Just leave me alone, ok!" self.task.deliver(response) expected = [ 'HTTP/1.0 505 HTTP Version not supported', 'content-length: 24', 'content-type: text/plain', 'server: stub server', '', 'Just leave me alone, ok!' ] actual = self.task.channel.getvalue().splitlines() self.assertEqual(expected, actual)
def testIncludingNoneForExample(self): response = Response(537) response.body = None self.task.server.deploy_mode = False self.task.deliver(response) expected = [ 'HTTP/1.0 537 HTTPY App Dev', 'content-length: 4', 'content-type: text/plain', 'server: stub server', '', 'None' ] actual = self.task.channel.getvalue().splitlines() self.assertEqual(expected, actual)
def testJustLikeFor200(self): response = Response(200) response.headers['content-type'] = 'text/plain' response.body = '' self.task.deliver(response) expected = [ 'HTTP/1.0 200 OK', 'content-length: 0', 'content-type: text/plain', 'server: stub server', '' ] actual = self.task.channel.getvalue().splitlines() self.assertEqual(expected, actual)
def testAndEmptyValuesArePreserved(self): response = Response(537) response.body = {} self.task.server.deploy_mode = False self.task.deliver(response) expected = [ 'HTTP/1.0 537 HTTPY App Dev', 'content-length: 2', 'content-type: text/plain', 'server: stub server', '', '{}' ] actual = self.task.channel.getvalue().splitlines() self.assertEqual(expected, actual)
def testExceptForNonSuccessfulRequests_InWhichCaseItIsTextPlain(self): response = Response(301) response.headers['location'] = 'http://www.google.com/' response.body = "Greetings, program!" self.task.deliver(response) expected = [ 'HTTP/1.0 301 Moved Permanently', 'content-length: 19', 'content-type: text/plain', 'location: http://www.google.com/', 'server: stub server', '', 'Greetings, program!' ] actual = self.task.channel.getvalue().splitlines() self.assertEqual(expected, actual)
def testBasic(self): response = Response(200) response.headers['content-type'] = 'text/plain' response.body = "Greetings, program!" self.task.deliver(response) expected = [ 'HTTP/1.0 200 OK', 'content-length: 19', 'content-type: text/plain', 'server: stub server', '', 'Greetings, program!' ] actual = self.task.channel.getvalue().splitlines() self.assertEqual(expected, actual)
def testButYouCantOverrideContentLength(self): response = Response(200) response.headers['content-length'] = 50000 response.body = "Greetings, program!" self.task.deliver(response) expected = [ 'HTTP/1.0 200 OK', 'content-length: 19', 'content-type: application/octet-stream', 'server: stub server', '', 'Greetings, program!' ] actual = self.task.channel.getvalue().splitlines() self.assertEqual(expected, actual)
def testYouCanAddArbitraryHeaders_TheyWillBeLowerCasedButWillMakeIt(self): response = Response(200) response.headers['cheese'] = 'yummy' response.headers['FOO'] = 'Bar' response.body = "Greetings, program!" self.task.deliver(response) expected = [ 'HTTP/1.0 200 OK', 'cheese: yummy', 'content-length: 19', 'foo: Bar', 'content-type: application/octet-stream', 'server: stub server', '', 'Greetings, program!' ] actual = self.task.channel.getvalue().splitlines() self.assertEqual(expected, actual)
def testYouCanOverrideServerAndContentType(self): response = Response(200) response.headers['server'] = 'MY SERVER! MINE!' response.headers['content-type'] = 'cheese/yummy' response.body = "Greetings, program!" self.task.deliver(response) expected = [ 'HTTP/1.0 200 OK', 'content-length: 19', 'content-type: cheese/yummy', 'server: MY SERVER! MINE!', '', 'Greetings, program!' ] actual = self.task.channel.getvalue().splitlines() self.assertEqual(expected, actual)
def testBodyNotWrittenForHEADRequest(self): self.task.request = ZopeRequest() self.task.request.received("HEAD / HTTP/1.1\r\n\r\n") response = Response(200) response.body = "Greetings, program!" self.task.deliver(response) expected = [ 'HTTP/1.0 200 OK', 'content-length: 19', 'content-type: application/octet-stream', 'server: stub server', '' ] actual = self.task.channel.getvalue().splitlines() self.assertEqual(expected, actual)
def testBodyIsFormattedFor537(self): response = Response(537) response.body = ('#' * 20, ['#' * 70]) self.task.server.deploy_mode = False self.task.deliver(response) expected = [ 'HTTP/1.0 537 HTTPY App Dev', 'content-length: 101', 'content-type: text/plain', 'server: stub server', '', u"('####################',", u" ['######################################################################'])" ] actual = self.task.channel.getvalue().splitlines() self.assertEqual(expected, actual)
def testDevelopment_ModifiedSinceIsFalse(self): os.environ['HTTPY_MODE'] = 'development' self.app = DefaultApp.Application() headers = {'If-Modified-Since': 'Fri, 31 Dec 9999 23:59:59 GMT'} request = self.make_request("/foo.html", headers) expected = Response(200) expected.headers['Last-Modified'] = 'blah blah blah' expected.headers['Content-Type'] = 'text/html' expected.body = 'Greetings, program!' try: self.app.respond(request) except Response, actual: pass
def testDeployment_ModifiedSinceIsFalse(self): self.config.mode = 'deployment' self.txn = DefaultApp.Transaction(self.config) headers = {'If-Modified-Since': 'Fri, 31 Dec 9999 23:59:59 GMT'} request = self.make_request("/foo.html", headers) expected = Response(304) expected.headers['Last-Modified'] = 'blah blah blah' expected.headers['Content-Type'] = 'text/html' expected.body = '' # no body for 304 try: self.txn.process(request) except Response, actual: pass
def testDeployment_ModifiedSinceIsTrue(self): self.config.mode = 'deployment' self.txn = DefaultApp.Transaction(self.config) headers = {'If-Modified-Since': 'Fri, 01 Jan 1970 00:00:00 GMT'} request = self.make_request("/foo.html", headers) expected = Response(200) expected.headers['Last-Modified'] = 'blah blah blah' expected.headers['Content-Type'] = 'text/html' expected.body = 'Greetings, program!' try: self.txn.process(request) except Response, actual: pass
def respond(self, request): """Given a request, hand it off to a method handler. """ try: if request.path != '/': response = Response(301) response.headers['location'] = '/' raise response handler = getattr(self, request.method, None) if handler: handler(request) else: raise Response(501) # Not Implemented finally: pass
def process(self): """Execute one transaction. The call to Transaction.process is complicated by the fact that in development mode we want to drop into the post-mortem debugger when there is an exception (other than Response, of course). Transaction.process is expected to raise a Response or other exception. """ config = TransactionConfig(self.app, self.server_config) # Do some late validation so we can use Response. # =============================================== resource_fs_path = uri_to_fs(config, self.request.path, raw=True) # Is the app still on the filesystem? if not os.path.isdir(config.app_fs_root): raise Response(404) if config.__: # Is the app's magic directory still on the filesystem? if not os.path.isdir(config.__): response = Response(500) response.body = ("The application's magic directory has " + "disappeared.") raise response # Protect the magic directory from direct access. if resource_fs_path.startswith(config.__): raise Response(404) # Get out of the way. # =================== transaction = self.app.module.Transaction(config) if not self.dev_mode: transaction.process(self.request) else: try: transaction.process(self.request) except Response: raise except: log(90, traceback.format_exc()) pdb.post_mortem(sys.exc_info()[2]) raise # You know something? No soup for you! # ==================================== response = Response(500) response.body = "%s.process did not raise anything." % str(transaction) raise response
def translate(self, path): """Given a URI path, return a corresponding filesystem path. The URI path is taken to be rooted in our application root. If it points to a directory, look for a default resource. If it points to a file, make sure the file exists. This method can raise the following Responses: 301 Moved Permanently 400 Bad Request 403 Forbidden 400 Not Found """ full_path = os.path.join(self.config['root'], path.lstrip('/')) if os.path.isdir(full_path): if not path.endswith('/'): # redirect directory requests to trailing slash new_location = '%s/' % path response = Response(301) response.headers = {'Location': new_location} log(98, "Redirecting to trailing slash: %s" % path) raise response default = '' for name in self.config['defaults']: _path = os.path.join(full_path, name) if os.path.isfile(_path): default = _path break full_path = default if not default: log(94, "No default resource in %s" % path) raise Response(403) else: if not os.path.exists(full_path): log(94, "Not Found: %s" % path) raise Response(404) return full_path
def respond(self, request): # Preen the request. # ================== if request.method != 'GET': raise Response(501) if self.uri_root != '/' and request.path.startswith(self.uri_root): request.fullpath = request.path request.path = request.path[len(self.uri_root):] if request.path == '': raise Response(302, '', {'Location': self.uri_root + '/'}) if request.path == '/': self.serve_index(request) else: self.serve_doc(request)
def process(self, request): """Given an httpy.Request, raise an httpy.Response. """ path = self.translate(request.path) mtime = os.stat(path)[stat.ST_MTIME] content_length = os.stat(path)[stat.ST_SIZE] if self.config['mode'] == 'deployment': ims = self.request.message.get('If-Modified-Since') length_match = True if ims: length = ims.group(4) if length: try: length = int(length) if length != content_length: length_match = False except: pass ims_date = False if ims: ims_date = http_date.parse_http_date(ims.group(1)) if length_match and ims_date: if mtime <= ims_date: raise NotModified # Build a response and raise. # =========================== content_type = mimetypes.guess_type(path)[0] or 'text/plain' log(88, "content type for %s: %s" % (path, content_type)) response = Response(200) #response.headers['Last-Modified'] = mtime response.headers['content-type'] = content_type response.body = open(path, 'rb').read() raise response
def testStatusLineGetsWritten(self): response = Response(505) self.task.deliver(response) expected = [ 'HTTP/1.0 505 HTTP Version not supported', 'content-length: 23', 'content-type: text/plain', 'server: stub server', '', 'Cannot fulfill request.' ] actual = self.task.channel.getvalue().splitlines() self.assertEqual(expected, actual)