From f77d40cc0fe38846c42f8cfeabdeb48326fcbea0 Mon Sep 17 00:00:00 2001 From: Harald Wolff Date: Tue, 8 Jan 2019 17:58:53 +0100 Subject: [PATCH] Initial Commit --- .gitignore | 10 +++ lnwsgi/__init__.py | 9 +++ lnwsgi/application.py | 91 +++++++++++++++++++++++ lnwsgi/authentication.py | 47 ++++++++++++ lnwsgi/exceptions.py | 25 +++++++ lnwsgi/request.py | 143 ++++++++++++++++++++++++++++++++++++ lnwsgi/reqvar.py | 95 ++++++++++++++++++++++++ lnwsgi/status.py | 14 ++++ lnwsgi/walkable/__init__.py | 3 + lnwsgi/walkable/walkable.py | 108 +++++++++++++++++++++++++++ 10 files changed, 545 insertions(+) create mode 100755 .gitignore create mode 100755 lnwsgi/__init__.py create mode 100755 lnwsgi/application.py create mode 100755 lnwsgi/authentication.py create mode 100755 lnwsgi/exceptions.py create mode 100755 lnwsgi/request.py create mode 100755 lnwsgi/reqvar.py create mode 100755 lnwsgi/status.py create mode 100755 lnwsgi/walkable/__init__.py create mode 100755 lnwsgi/walkable/walkable.py diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..0044a14 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.AppleDouble +.DS_Store + +*/.AppleDouble +*/.DS_Store +*.pyc + +**/*.pyc + +tmp diff --git a/lnwsgi/__init__.py b/lnwsgi/__init__.py new file mode 100755 index 0000000..9eaaabf --- /dev/null +++ b/lnwsgi/__init__.py @@ -0,0 +1,9 @@ +from lnwsgi.exceptions import * + +import lnwsgi.status +import lnwsgi.walkable +import lnwsgi.authentication + +from lnwsgi.request import WSGIRequest +from lnwsgi.application import WSGIApplication + diff --git a/lnwsgi/application.py b/lnwsgi/application.py new file mode 100755 index 0000000..4abfd93 --- /dev/null +++ b/lnwsgi/application.py @@ -0,0 +1,91 @@ +import sys +import traceback +import io + +import lnwsgi +from lnwsgi.exceptions import WebException + + +class WSGIApplication: + + def __init__(self, appName = "WSGIApplication"): + self.__root = lnwsgi.walkable.Walkable() + self.url = None + self.appName = appName + + def root(self): + return self.__root + + def setRoot(self, r): + self.__root = r + + def setApplicationURL(self, url): + self.url = url + + + def authenticate(self, request): + return lnwsgi.authentication.Identity("", []) + + + def handle(self, request): +# try: + path = list( filter( None, request.path.split("/") ) ) + + response = self.__root.walk( request, path ) + + if isinstance(response, str): + response = ( response.encode(), ) + + return response +# except: +# ei = sys.exc_info() +# request.setResponseHeader("content-type","text/html") +# return ("{0}: {1}".format(ei[0].__name__,ei[1]).encode(),) + + def handleException(self, request, exc_info = None): + if exc_info is None: + exc_info = sys.exc_info() + + m = io.StringIO() + + m.write("Exception: {0}\n".format(exc_info[0].__name__)) + m.write("Message: {0}\n".format(exc_info[1])) + m.write("Traceback: {0}\n".format( + "\n ".join( traceback.extract_tb(exc_info[2]).format() ) + )) + + if isinstance(exc_info[1], WebException ): + status = exc_info[1].status + else: + status = 500 + + if status == 401: + request.setResponseHeader("WWW-Authenticate","Basic realm=\"{0}\", charset=\"UTF-8\"".format(self.appName)) + + request.setResponseHeader("content-type","text/plain") + request.status(status) + + return (m.getvalue().encode(),) + + + def __call__(self, environ, start_response): + request = lnwsgi.WSGIRequest( self, environ ) + print("R: {0:16} {1}".format( str(request.identity()), request )) + + try: + request.identity().authorize( request ) + response = self.handle( request ) + except: + response = self.handleException( request, sys.exc_info() ) + + + start_response( + lnwsgi.status.getStatusMessage(request.status()), + request.listResponseHeaders() + ) + + return response + + + + diff --git a/lnwsgi/authentication.py b/lnwsgi/authentication.py new file mode 100755 index 0000000..9f61bc4 --- /dev/null +++ b/lnwsgi/authentication.py @@ -0,0 +1,47 @@ +import itertools + +from lnwsgi.exceptions import Unauthorized,Unauthenticated + +class Identity: + + def __init__(self, name, rights): + self.__name = name + self.__rights = dict( rights ) + + def name(self): + return self.__name + + def rights(self): + return dict(self.__rights) + + def authorizePath(self, request, rpath): + + print("authorizePath(): {0} => {1}".format(rpath, request.path)) + + p = itertools.zip_longest(rpath.split("/"),request.path.split("/")) + for pi in p: + if pi[1] is None: + return True + if pi[0] == pi[1]: + continue + if pi[0] == "*": + continue + if pi[0] == "**": + return True + + return False + return True + + def authorize(self, request): + for r in self.__rights: + if self.authorizePath( request, r ): + return self.__rights[r] + + if self.__name == "": + raise Unauthenticated( request ) + raise Unauthorized( request ) + + def __str__(self): + return self.__name + + diff --git a/lnwsgi/exceptions.py b/lnwsgi/exceptions.py new file mode 100755 index 0000000..d5dd443 --- /dev/null +++ b/lnwsgi/exceptions.py @@ -0,0 +1,25 @@ + +class WebException(Exception): + + def __init__(self, message, status = 500): + Exception.__init__( self, message ) + self.status = status + + +class UnsupportedMediaType(WebException): + pass + +class ParameterException(WebException): + + def __init__(self, name): + WebException.__init__(self, "value of parameter [{0}] is unsupported".format(name)) + +class Unauthenticated(WebException): + def __init__(self, request): + WebException.__init__(self, "unauthorized to access {0}".format(request.path), status = 401) + +class Unauthorized(WebException): + + def __init__(self, request): + WebException.__init__(self, "unauthorized to access {0}".format(request.path), status = 403) + \ No newline at end of file diff --git a/lnwsgi/request.py b/lnwsgi/request.py new file mode 100755 index 0000000..7af8638 --- /dev/null +++ b/lnwsgi/request.py @@ -0,0 +1,143 @@ +import sys +import io +import json + +from lnwsgi.reqvar import RequestParameters + +class WSGIRequest: + + def __init__(self,application,environ): + self.__application = application + self.__environ = environ + self.__scheme = self.__environ[ "wsgi.url_scheme" ].lower() + + self.method = self.__environ["REQUEST_METHOD"].upper() + + if "HTTP_HOST" in self.__environ: + self.__hostname = self.__environ[ "HTTP_HOST" ] + else: + self.__hostname = self.__environ[ "SERVER_NAME" ] + + self.__port = int(self.__environ[ "SERVER_PORT" ]) + self.__script = self.__environ[ "SCRIPT_NAME" ] + self.path = self.__environ[ "PATH_INFO" ] + + if "QUERY_STRING" in self.__environ: + self.__query = self.__environ[ "QUERY_STRING" ] + else: + self.__query = "" + + if "CONTENT_TYPE" in self.__environ: + self.content_type = self.__environ["CONTENT_TYPE"] + + if "wsgi.input" in self.__environ: + self.__content = self.__environ["wsgi.input"].read() + else: + self.__content = bytes() + + self.parameters = RequestParameters( self ) + + self.__status = 200 + self.__response_headers = {} + self.setResponseHeader("content-type","text/html") + + self.__walked = [] + self.__interpretAccepts() + + self.__identity = application.authenticate( self ) + + def identity(self): + return self.__identity + + def __interpretAccepts(self): + self.__accepts = [] + + if "HTTP_ACCEPTS" in self.__environ: + al = self.__environ["HTTP_ACCEPTS"].split(",") + for a in al: + t = a.split(";") + mtype = t[0].strip() + + self.__accepts.append( mtype.lower() ) + + def __contains__(self, name): + return name in self.__environ + + def __getitem__(self, name): + return self.__environ[name] + + def __iter__(self): + return self.__environ.__iter__() + + def getOrNone(self, name): + if name in self: + return self[name] + return None + + def accepts(self): + return list( self.__accepts ) + + def status(self, sc = None): + if not sc is None: + self.__status = sc + return self.__status + + def content(self): + return self.__content + + def json(self): + return json.loads(self.__content.decode()) + + def query(self): + return self.__query + + def pushWalkedObject(self, wo): + self.__walked.append( wo ) + + def walkedObjects(self): + return list( self.__walked ) + + + def self(self, path = None): + if self.__application.url is None: + url = "{0}://{1}".format(self.__scheme,self.__hostname) + else: + url = self.__application.url +# if not ((self.__scheme == "http" and self.__port == 80) or (self.__scheme == "https" and self.__port == 443)): +# url = "{0}:{1}".format(url, self.__port) + + if path is None: + path = self.path + + url = "{0}{1}{2}".format(url,self.__script,path) + + return url; + + def getResponseHeaders(self): + return dict( self.__response_headers ) + + def listResponseHeaders(self): + rh = [] + for n in self.__response_headers: + rh.append( (n, self.__response_headers[n] ) ) + return rh + + def setResponseHeader(self, name, value): + if value is None: + del self.__response_headers[ name.lower() ] + else: + self.__response_headers[ name.lower() ] = value + + def __str__(self): + s = io.StringIO() + s.write("{method:8} {path}".format(**self.__dict__)) + return s.getvalue() + + + + + + + + + diff --git a/lnwsgi/reqvar.py b/lnwsgi/reqvar.py new file mode 100755 index 0000000..31ad5d8 --- /dev/null +++ b/lnwsgi/reqvar.py @@ -0,0 +1,95 @@ +from urllib.parse import unquote_plus + +class RequestParameter: + + def __init__(self,name,value = None,headers = {}): + self.__name = name + self.__value = value + self.__headers = headers + + def name(self): + return self.__name + def value(self): + return self.__value + def headers(self): + return self.__headers + + def filename(self): + pp = self.__headers.parameters("CONTENT-DISPOSITION") + if (not pp is None) and ("filename" in pp.keys()): + return pp["filename"] + return None + + def save(self, filename): + f = open( filename, "wb" ) + f.write( self.__value ) + f.close() + + +class RequestParameters: + + def __init__(self, request): + self.__request = request + self.__values = {} + + self.__append_query() + self.__append_mime() + + def __getitem__(self,name): + return self.__values[name][0] + + def __contains__(self,name): + return name in self.__values + + def get(self,name): + try: + return self[name].value() + except: + return None + + def getOrDefault(self, name, default = None): + if name in self: + return self[name].value() + return default + + + def keys(self): + return self.__values.keys() + + def list(self,name): + if name in self.__values: + return list(self.__values[name]) + return [] + + def __append_var(self,var): + if not var.name() in self.__values.keys(): + self.__values[var.name()] = [] + self.__values[var.name()].append( var ) + + def __append_mime(self): + pass +# mime = self.__request.mime() +# +# if (mime.multipart()): +# for p in mime.parts(): +# n = p.name() +# if not n is None: +# self.__append_var(FormValue(n,p.body(),p.headers())) + + def __append_query(self): + qs = self.__request.query() + + pairs = qs.split("&") + for pair in pairs: + sp = pair.split("=") + if (len(sp)>0): + name = sp[0] + if (len(sp)>1): + value = sp[1] + else: + value= "" + + self.__append_var( RequestParameter( unquote_plus(name), unquote_plus(value)) ) + + + diff --git a/lnwsgi/status.py b/lnwsgi/status.py new file mode 100755 index 0000000..10e6904 --- /dev/null +++ b/lnwsgi/status.py @@ -0,0 +1,14 @@ + +status_messages = { + 200: "OK", + 401: "Unauthenticated", + 403: "Unauthorized", + 404: "NotFound", + 500: "Exception was raised while handling your request" +} + +def getStatusMessage(statuscode): + if statuscode in status_messages: + return "{0} {1}".format(statuscode,status_messages[statuscode]) + return "{0} unknown status".format(statuscode) + diff --git a/lnwsgi/walkable/__init__.py b/lnwsgi/walkable/__init__.py new file mode 100755 index 0000000..3c1f9ab --- /dev/null +++ b/lnwsgi/walkable/__init__.py @@ -0,0 +1,3 @@ + +from lnwsgi.walkable.walkable import Walkable,CachedWalkable,Callable,TypeAware + diff --git a/lnwsgi/walkable/walkable.py b/lnwsgi/walkable/walkable.py new file mode 100755 index 0000000..b5100d3 --- /dev/null +++ b/lnwsgi/walkable/walkable.py @@ -0,0 +1,108 @@ +import sys +import time + +from lnwsgi.exceptions import WebException + +class TypeAware: + + def media(self, mimeType, request): + raise UnsupportedMediaType( mimeType ) + + def __call__(self, request): + for accepted in request.accepts(): + try: + return self.media( accepted ) + except UnsupportedMediaType: + pass + + raise UnsupportedMediaType(", ".join(request.accepts())) + +class Walkable: + + def __init__(self): + pass + + def walk(self, request, path = []): + if len(path) == 0: + return self( request ) + + np = path.pop(0) + next = None + + try: + print("___ = {0} : {1}".format(str(self),np)) + next = getattr( self, np ) + except AttributeError: + if len(path) == 0 and request.method == "PUT": + return self.PUT( request, np ) + raise WebException("Not Found", status = 404) + + if not isinstance( next, Walkable ): + raise TypeError("Walkable needed") + + return next.walk( request, path ) + + def GET(self, request): + return ("No Implementation here.".encode(),) + + def PUT(self, request, name): + raise WebException("Object already exists!") + + POST = GET + PUT = GET + HEAD = GET + DELETE = None + + def __call__(self, request): + method = getattr(self, request.method) + return method( request ) + +class CachedWalkable(Walkable): + + def __init__(self): + Walkable.__init__(self) + + self.__cache_size = 16 # Maximum number of entries within cache + self.__cache = {} # key -> value table + self.__cache_names = [] # List of all cached names (youngest accessed #0) + + def _touchCache(self, name): + if name in self.__cache_names: + self.__cache_names.remove( name ) + self.__cache_names.insert(0, name) + if len(self.__cache_names) > self.__cache_size: + self._rmCachedObject( self.__cache_names[-1] ) + + def _addCachedObject(self, name, value): + self.__cache[ name ] = value + self._touchCache( name ) + + def _rmCachedObject(self, name): + del self.__cache[ name ] + self.__cache_names.remove( name ) + + + + def lookup(self,name): + raise AttributeError(name) + + def __getattr__(self,name): + if name in self.__cache_names: + self._touchCache( name ) + return self.__cache[ name ] + + o = self.lookup(name) + self._addCachedObject( name, o ) + return o + + +class Callable(Walkable): + + def __init__(self, m): + self.__method = m + + def __call__(self, s, request): + return self.__method( s, request ) + + +