#!/usr/bin/python # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Composite deployment and integration with WSGI from wsgiref.simple_server import make_server from wsgiref.handlers import CGIHandler from wsgiref.util import request_uri from wsgiref.util import FileWrapper from os import environ import os.path import hashlib from sys import stderr, argv from util import * from scdl import * from atomutil import * from jsonutil import * # Cache the deployed components between requests comps = None # Return the path of an HTTP request def requestPath(e): return e.get("PATH_INFO", "") # Return the method of an HTTP request def requestMethod(e): return e.get("REQUEST_METHOD", "") # Return the method of an HTTP request def requestContentType(e): return e.get("CONTENT_TYPE", "") # Return the request body input stream def requestBody(e): i = e.get("wsgi.input", None) if i == None: return () l = int(e.get("CONTENT_LENGTH", "0")) return (i.read(l),) def requestIfNoneMatch(e): return e.get("HTTP_IF_NONE_MATCH", ""); # Hash a list of strings into an MD5 signature def md5update(md, s): if isNil(s): return md.hexdigest() md.update(car(s)) return md5update(md, cdr(s)) def md5(s): return md5update(hashlib.md5(), s) # Return an HTTP success result def result(e, r, st, h = (), b = None): if st == 201: r("201 Created", list(h)) return () if st == 200: if b == None: r("200 OK", list(h)) return () # Handle etags to minimize bandwidth usage md = md5(b) if (md == requestIfNoneMatch(e)): r("304 Not Modified", list((("Etag", md), ("Expires", "Tue, 01 Jan 1980 00:00:00 GMT")))) return () r("200 OK", list(h + (("Etag", md), ("Expires", "Tue, 01 Jan 1980 00:00:00 GMT")))) return b if st == 301: r("301 Moved Permanently", list(h)) return failure(e, r, 500) # Return an HTTP failure result def failure(e, r, st): s = "404 Not Found" if st == 404 else str(st) + " " + "Internal Server Error" r(s, list((("Content-type", "text/html"),))) return (""+ s + "

" + s[4:] + "

",) # Return a static file def fileresult(e, r, ct, f): # Read the file, return a 404 if not found p = "htdocs" + f if not os.path.exists(p): return failure(e, r, 404) c = tuple(FileWrapper(open("htdocs" + f))) # Handle etags to minimize bandwidth usage md = md5(c) r("200 OK", list((("Content-type", ct),("Etag", md)))) return c # Converts the args received in a POST to a list of key value pairs def postArgs(a): if isNil(a): return ((),) l = car(a); return cons(l, postArgs(cdr(a))) # Return the URL used to sign out def signout(ruri): try: from google.appengine.api import users return users.create_logout_url(ruri) except: return None # Return the URL used to sign in def signin(ruri): try: from google.appengine.api import users return users.create_login_url(ruri) except: return None # WSGI application function def application(e, r): m = requestMethod(e) fpath = requestPath(e) # Serve static files if m == "GET": if fpath.endswith(".html"): return fileresult(e, r, "text/html", fpath) if fpath.endswith(".css"): return fileresult(e, r, "text/css", fpath) if fpath.endswith(".js"): return fileresult(e, r, "application/x-javascript", fpath) if fpath.endswith(".png"): return fileresult(e, r, "image/png", fpath) if fpath == "/": return result(e, r, 301, (("Location", "/index.html"),)) # Debug hook if fpath == "/debug": return result(e, r, 200, (("Content-type", "text/plain"),), ("Debug",)) # Sign in and out if fpath == "/login": redir = signin("/") if redir: return result(e, r, 301, (("Location", redir),)) if fpath == "/logout": redir = signout(signin("/")) if redir: return result(e, r, 301, (("Location", redir),)) # Find the requested component path = tokens(fpath) uc = uriToComponent(path, comps) uri = car(uc) if uri == None: return failure(e, r, 404) comp = cadr(uc) # Call the requested component function id = path[len(uri):] if m == "GET": v = comp("get", id) # Write a simple value as a JSON value if not isList(v): return result(e, r, 200, (("Content-type", "application/json"),), writeJSON(valuesToElements((("'value", v),)))) # Write an empty list as a JSON empty value if not isList(v): return result(e, r, 200, (("Content-type", "application/json"),), writeJSON(())) # Write content-type / content-list pair if isString(car(v)) and not isNil(cdr(v)) and isList(cadr(v)): return result(e, r, 200, (("Content-type", car(v)),), cadr(v)) # Convert list of values to element values ve = valuesToElements(v) # Write an assoc result as a JSON value if isList(car(ve)) and not isNil(car(ve)): el = car(ve) if isSymbol(car(el)) and car(el) == element and not isNil(cdr(el)) and isSymbol(cadr(el)): if cadr(el) == "'feed": return result(e, r, 200, (("Content-type", "application/atom+xml"),), writeATOMFeed(ve)) if cadr(el) == "'entry": return result(e, r, 200, (("Content-type", "application/atom+xml"),), writeATOMEntry(ve)) # Write a JSON value return result(e, r, 200, (("Content-type", "application/json"),), writeJSON(ve)) if m == "POST": ct = requestContentType(e) # Handle a JSON-RPC function call if contains(ct, "application/json-rpc") or contains(ct, "text/plain") or contains(ct, "application/x-www-form-urlencoded"): print >> stderr, "Handling JSON-RPC request" json = elementsToValues(readJSON(requestBody(e))) args = postArgs(json) jid = cadr(assoc("'id", args)) func = funcName(cadr(assoc("'method", args))) params = cadr(assoc("'params", args)) v = comp(func, *params) return result(e, r, 200, (("Content-type", "application/json-rpc"),), jsonResult(jid, v)) # Handle an ATOM entry POST if contains(ct, "application/atom+xml"): ae = elementsToValues(readATOMEntry(requestBody(e))) v = comp("post", id, ae) if isNil(v): return failure(e, r, 500) return result(e, r, 201, (("Location", request_uri(e) + "/" + "/".join(v)),)) return failure(e, r, 500) if m == "PUT": # Handle an ATOM entry PUT ae = elementsToValues(readATOMEntry(requestBody(e))) v = comp("put", id, ae) if v == False: return failure(e, r, 404) return result(e, r, 200) if m == "PATCH": # Handle an ATOM entry PATCH ae = elementsToValues(readATOMEntry(requestBody(e))) v = comp("patch", id, ae) if v == False: return failure(e, r, 404) return result(e, r, 200) if m == "DELETE": v = comp("delete", id) if v == False: return failure(e, r, 404) return result(e, r, 200) return failure(e, r, 500) # Return the WSGI server type def serverType(e): return e.get("SERVER_SOFTWARE", "") def main(): # Read the deployed composite and evaluate the configured components global comps if comps == None: domain = "domain.composite" if os.path.exists("domain.composite") else "domain-test.composite" comps = evalComponents(components(parse(domain))) # Handle the WSGI request with the WSGI runtime st = serverType(environ) if contains(st, "App Engine") or contains(st, "Development"): from google.appengine.ext.webapp.util import run_wsgi_app run_wsgi_app(application) elif st != "": CGIHandler().run(application) else: make_server("", int(argv[1]), application).serve_forever() # Run the WSGI application if __name__ == "__main__": main()