| #!/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 isNull(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 ("<html><head><title>"+ s + "</title></head><body><h1>" + s[4:] + "</h1></body></html>",) | |
| # 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 isNull(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 isNull(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 isNull(car(ve)): | |
| el = car(ve) | |
| if isSymbol(car(el)) and car(el) == element and not isNull(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 isNull(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() | |