Source code for jasy.http.Server

#
# Jasy - Web Tooling Framework
# Copyright 2010-2012 Zynga Inc.
# Copyright 2013-2014 Sebastian Werner
#

import os
import logging
import base64
import json
import requests
import cherrypy
import locale
from collections import namedtuple

import jasy.core.Cache as Cache
import jasy.core.Console as Console

from jasy.core.Types import CaseInsensitiveDict
from jasy.core.Util import getKey
from jasy import __version__ as jasyVersion

Result = namedtuple('Result', ['headers', 'content', 'status_code'])

# Disable logging HTTP request being created
logging.getLogger("requests").setLevel(logging.WARNING)


#
# UTILITIES
#

[docs]def enableCrossDomain(): # See also: https://developer.mozilla.org/En/HTTP_Access_Control # Allow requests from all locations cherrypy.response.headers["Access-Control-Allow-Origin"] = "*" # Allow all methods supported by urlfetch cherrypy.response.headers["Access-Control-Allow-Methods"] = "GET, POST, HEAD, PUT, DELETE" # Allow cache-control and our custom headers cherrypy.response.headers["Access-Control-Allow-Headers"] = "Cache-Control, X-Proxy-Authorization, X-Requested-With" # Cache allowence for cross domain for 7 days cherrypy.response.headers["Access-Control-Max-Age"] = "604800"
[docs]def findIndex(path): all = ["index.html", "index.php"] for candidate in all: rel = os.path.join(path, candidate) if os.path.exists(rel): return candidate return None
[docs]def noBodyProcess(): cherrypy.request.process_request_body = False
cherrypy.tools.noBodyProcess = cherrypy.Tool('before_request_body', noBodyProcess) # # ROUTERS #
[docs]class Proxy(object): def __init__(self, id, config): self.id = id self.config = config self.host = getKey(config, "host") self.auth = getKey(config, "auth") self.enableDebug = getKey(config, "debug", False) self.enableMirror = getKey(config, "mirror", False) self.enableOffline = getKey(config, "offline", False) if self.enableMirror: self.mirror = Cache.Cache(os.getcwd(), ".jasy/mirror-%s" % self.id, hashkeys=True) Console.info('Proxy "%s" => "%s" [debug:%s|mirror:%s|offline:%s]', self.id, self.host, self.enableDebug, self.enableMirror, self.enableOffline) # These headers will be blocked between header copies __blockHeaders = CaseInsensitiveDict.fromkeys([ "content-encoding", "content-length", "connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "transfer-encoding", "remote-addr", "host" ]) @cherrypy.expose @cherrypy.tools.noBodyProcess()
[docs] def default(self, *args, **query): """ This method returns the content of existing files on the file system. Query string might be used for cache busting and are otherwise ignored. """ url = self.config["host"] + "/".join(args) result = None body = None # Try using offline mirror if feasible if self.enableMirror and cherrypy.request.method == "GET": mirrorId = "%s[%s]" % (url, json.dumps(query, separators=(',', ':'), sort_keys=True)) result = self.mirror.read(mirrorId) if result is not None and self.enableDebug: Console.info("Mirrored: %s" % url) if cherrypy.request.method in ("POST", "PUT"): body = cherrypy.request.body.fp.read() # Check if we're in forced offline mode if self.enableOffline and result is None: Console.info("Offline: %s" % url) raise cherrypy.NotFound(url) # Load URL from remote server if result is None: # Prepare headers headers = CaseInsensitiveDict() for name in cherrypy.request.headers: if not name in self.__blockHeaders: headers[name] = cherrypy.request.headers[name] # Load URL from remote host try: if self.enableDebug: Console.info("Requesting: %s [%s]", url, cherrypy.request.method) # Apply headers for basic HTTP authentification if "X-Proxy-Authorization" in headers: headers["Authorization"] = headers["X-Proxy-Authorization"] del headers["X-Proxy-Authorization"] # Add headers for different authentification approaches if self.auth: # Basic Auth if self.auth["method"] == "basic": headers["Authorization"] = b"Basic " + base64.b64encode(("%s:%s" % (self.auth["user"], self.auth["password"])).encode("ascii")) # We disable verification of SSL certificates to be more tolerant on test servers result = requests.request(cherrypy.request.method, url, params=query, headers=headers, data=body, verify=False) except Exception as err: if self.enableDebug: Console.info("Request failed: %s", err) raise cherrypy.HTTPError(403) # Storing result into mirror if self.enableMirror and cherrypy.request.method == "GET" and result.status_code == 200: # Wrap result into mirrorable entry resultCopy = Result(result.headers, result.content, result.status_code) self.mirror.store(mirrorId, resultCopy) # Copy response headers to our reponse for name in result.headers: if not name.lower() in self.__blockHeaders: cherrypy.response.headers[name] = result.headers[name] # Set the proxyed reply status to the response status cherrypy.response.status = result.status_code # Append special header to all responses cherrypy.response.headers["X-Jasy-Version"] = jasyVersion # Enable cross domain access to this server enableCrossDomain() return result.content
[docs]class Static(object): def __init__(self, id, config, mimeTypes=None): self.id = id self.config = config self.mimeTypes = mimeTypes self.root = getKey(config, "root", ".") self.enableDebug = getKey(config, "debug", False) Console.info('Static "%s" => "%s" [debug:%s]', self.id, self.root, self.enableDebug) @cherrypy.expose
[docs] def default(self, *args, **query): """ This method returns the content of existing files on the file system. Query string might be used for cache busting and are otherwise ignored. """ # Append special header to all responses cherrypy.response.headers["X-Jasy-Version"] = jasyVersion # Enable cross domain access to this server enableCrossDomain() # When it's a file name in the local folder... load it if args: path = os.path.join(*args) else: path = "index.html" path = os.path.join(self.root, path) # Check for existance first if os.path.isfile(path): if self.enableDebug: Console.info("Serving file: %s", path) # Default content type to autodetection by Python mimetype API contentType = None # Support overriding by extensions extension = os.path.splitext(path)[1] if extension: extension = extension.lower()[1:] if extension in self.mimeTypes: contentType = self.mimeTypes[extension] + "; charset=" + locale.getpreferredencoding() return cherrypy.lib.static.serve_file(os.path.abspath(path), content_type=contentType) # Otherwise return a classic 404 else: if self.enableDebug: Console.warn("File at location %s not found at %s!", path, os.path.abspath(path)) raise cherrypy.NotFound(path)
# # ADDITIONAL MIME TYPES # additionalContentTypes = { "js": "application/javascript", "jsonp": "application/javascript", "json": "application/json", "oga": "audio/ogg", "ogg": "audio/ogg", "m4a": "audio/mp4", "f4a": "audio/mp4", "f4b": "audio/mp4", "ogv": "video/ogg", "mp4": "video/mp4", "m4v": "video/mp4", "f4v": "video/mp4", "f4p": "video/mp4", "webm": "video/webm", "flv": "video/x-flv", "svg": "image/svg+xml", "svgz": "image/svg+xml", "eot": "application/vnd.ms-fontobject", "ttf": "application/x-font-ttf", "ttc": "application/x-font-ttf", "otf": "font/opentype", "woff": "application/font-woff", "ico": "image/x-icon", "webp": "image/webp", "appcache": "text/cache-manifest", "manifest": "text/cache-manifest", "htc": "text/x-component", "rss": "application/xml", "atom": "application/xml", "xml": "application/xml", "rdf": "application/xml", "crx": "application/x-chrome-extension", "oex": "application/x-opera-extension", "xpi": "application/x-xpinstall", "safariextz": "application/octet-stream", "webapp": "application/x-web-app-manifest+json", "vcf": "text/x-vcard", "swf": "application/x-shockwave-flash", "vtt": "text/vtt" } # # START #
[docs]class Server: """Starts the built-in HTTP server inside the project's root directory""" def __init__(self, port=8080, host="127.0.0.1", mimeTypes=None): Console.info("Initializing server...") Console.indent() # Shared configuration (global/app) self.__config = { "global" : { "environment" : "production", "log.screen" : False, "server.socket_port": port, "server.socket_host": host, "engine.autoreload.on" : False, "tools.encode.on" : True, "tools.encode.encoding" : "utf-8" }, "/" : { "log.screen" : False } } self.__port = port # Build dict of content types to override native mimetype detection combinedTypes = {} combinedTypes.update(additionalContentTypes) if mimeTypes: combinedTypes.update(mimeTypes) # Update global config cherrypy.config.update(self.__config) # Somehow this screen disabling does not work # This hack to disable all access/error logging works def empty(*param, **args): pass def inspect(*param, **args): if args["severity"] > 20: Console.error("Critical error occoured:") Console.error(param[0]) cherrypy.log.access = empty cherrypy.log.error = inspect cherrypy.log.screen = False # Basic routing self.__root = Static("/", {}, mimeTypes=combinedTypes) Console.outdent()
[docs] def setRoutes(self, routes): """ Adds the given routes to the server configuration. Routes can be used to add special top level entries to the different features of the integrated webserver either mirroring a remote server or delivering a local directory. The parameters is a dict where every key is the name of the route and the value is the configuration of that route. """ Console.info("Adding routes...") Console.indent() for key in routes: entry = routes[key] if "host" in entry: node = Proxy(key, entry) else: node = Static(key, entry, mimeTypes=self.__root.mimeTypes) setattr(self.__root, key, node) Console.outdent()
[docs] def start(self): """ Starts the web server and blocks execution. Note: This stops further execution of the current task or method. """ app = cherrypy.tree.mount(self.__root, "", self.__config) cherrypy.process.plugins.PIDFile(cherrypy.engine, ".jasy/server-%s" % self.__port).subscribe() cherrypy.engine.start() Console.info("Started HTTP server at port %s... [PID=%s]", self.__port, os.getpid()) Console.indent() cherrypy.engine.block() Console.outdent() Console.info("Stopped HTTP server at port %s.", self.__port)