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 serve_static(self, fs_path, ims): """Given a filesystem path to a static resource, serve it. This is factored out for easier reuse. """ # Get basic info from the filesystem and start building a response. # ================================================================= mtime = os.stat(fs_path)[stat.ST_MTIME] content_type = mimetypes.guess_type(fs_path)[0] or 'text/plain' response = Response(200) # Support 304s, but only in deployment mode. # ========================================== if self.deploy_mode: if ims: mod_since = rfc822.parsedate(ims) last_modified = time.gmtime(mtime) if last_modified[:6] <= mod_since[:6]: response.code = 304 # Finish building the response and raise it. # ======================================== response.headers['Last-Modified'] = rfc822.formatdate(mtime) response.headers['Content-Type'] = content_type if response.code != 304: response.body = file(fs_path, 'rb').read() raise response
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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 respond(self, request): if request.method != 'POST': raise Response(501) else: response = Response(200) try: response.body = self.serve_xmlrpc(request) except: # Bug in the module. logger.error("Error serving request.") logger.debug(traceback.format_exc()) raise Response(500) else: # Valid XMLRPC response. response.headers = {'Content-Type': 'text/xml'} raise response
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 serve_index(self, request): """Serve a top-level package/module index. """ data = {} data['title'] = "doc537 — a Python documentation server" data['crumbs'] = data['title'] data['nav'] = '' data['callable'] = "<h1>index</h1>" data['content'] = self.getindex() data['version'] = __version__ data['date'] = time.strftime('%B %d, %Y') data['base'] = self.uri_root response = Response(200) response.headers = {'Content-Type': 'text/html'} response.body = TEMPLATE % data raise response
def GET(self, request): # Parse the querystring. # ====================== # We only want the first value for each argument. query = {} for k, v in parse_qs(request.uri['query'], True).items(): query[k] = v[0] # Implement GET with no query. # ============================ # "The simplest form of a query is a plain HTTP GET. It returns all # the RDF statements in the model at that URL." # -- http://w3.org/Submission/2003/SUBM-rdf-netapi-20031002/#http-query if query == {}: response = Response(200) response.headers['Content-Type'] = self.content_type response.body = self.store.serialize() raise response # Implement GET with a query. # =========================== # Query implementations should take the query less lang as keyword # arguments. else: if 'lang' not in query: raise Response(400, "You must specify a query language.") lang = urllib.unquote(query['lang']) del query['lang'] query_processor = self.get_query_processor(lang) if query_processor: query_processor(**query) else: raise Response(400, "Sorry, we don't support the " + "`%s' query language." % lang)
def TriplePattern(self, subject='', predicate='', object='', literal=''): """Given a triple pattern, return an RDF/XML graph. """ # Validate and parse incoming data. # ================================= if object and literal: raise Response(400, "Only one of parameters 'object' or " + "'literal' may be used in a single request.") def _parse(a): if a in ('', '*'): # wildcard return None else: # URI return rdflib.URIRef(urllib.unquote(a)) s = _parse(subject) p = _parse(predicate) if literal: o = rdflib.Literal(literal) else: o = _parse(object) # Build outgoing graph and send it along. # ======================================= out = rdflib.Graph("Memory") for statement in self.store.triples((s,p,o)): out.add(statement) response = Response(200) response.headers['Content-Type'] = self.content_type response.body = out.serialize() raise response
def testBut537OnlyAvailableInDevMode(self): response = Response(537) response.body = ('#' * 20, ['#' * 70]) self.task.dev_mode = False self.assertRaises(Exception, self.task.respond, response)
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: response = Response(400) response.body = ( "The Request-Line `%s' appears to be " % self.raw_line + "malformed because it has neither two nor " + "three tokens.") raise response # Validate the method. # ==================== if method not in ('GET', 'HEAD', 'POST'): response = Response(501) response.body = ("This server does not implement the " + "'%s' method." % method) raise response # 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: response = Response(400) response.body = ("The HTTP-Version `%s' appears to " % version + "be malformed because it does not match the " + "pattern `^HTTP/\d+\.\d+$'.") raise response # Save a few absolutely basic things for application use. # ======================================================= self.method = method self.uri = uri self.path = uri['path']
def testAnd537NotAvailableInDeploymentMode(self): response = Response(537) response.body = ('#' * 20, ['#' * 70]) self.task.server.deploy_mode = True self.assertRaises(StandardError, self.task.respond, response)
def set_line(self, stream): """Given an HTTP stream, parse and store the Request-Line. The HTTP stream must be positioned at the start of a request. The Request-Line is resolved into method, uri, and version, which are validated. Some helpful values are derived from these tokens, and all this information is stored on self. """ # Read and tokenize the Request-Line. # =================================== raw_line = stream.readline() while raw_line == '\r\n': # Skip initial blank lines to account for IE's buggy POST # implementation. raw_line = stream.readline() line = raw_line.rstrip('/n').rstrip('/r') tokens = line.split() if len(tokens) == 3: method, uri, version_string = tokens elif len(tokens) == 2: method, uri = tokens version_string = 'HTTP/0.9' else: response = Response(400) response.body = ("The Request-Line `%s' appears to be " % line + "malformed because it has neither two nor " + "three tokens.") raise response # Validate the method. # ==================== if method not in ('GET', 'HEAD', 'POST'): response = Response(501) response.body = ("This server does not implement the " + "`%s' method." % method) raise response # Parse the URI into a dictionary. # ================================ # We use the urlparse naming convention for uri's keys. uri = urlparse.urlsplit(uri) keys = ('scheme', 'netloc', 'path', 'query', 'fragment') _uri = {} for i in range(len(uri)): k = keys[i] v = uri[i] _uri[k] = v uri = _uri # Validate and possibly unencode the path. # ======================================== 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 an HTTPVersionNotSupported here ... for 0.9? 2.0? m = HTTP_VERSION.match(version_string) if not m: response = Response(400) response.body = ("The HTTP-Version `%s' appears to " % version + "be malformed because it does not match the " + "pattern `^HTTP/\d+\.\d+$'.") raise response version = tuple([int(i) for i in m.groups()]) # Store the most important info on self. # ====================================== # raw line self.raw.append(raw_line) self.raw_line = line # tokens self.method = method self.uri = uri self.version_string = version_string # derivatives self.path = uri['path'] self.querystring = uri['query'] self.version = version