#!/usr/bin/env python #-*- encoding: utf-8 -*- # # kakache.py # Kakache Web Server # # Copyright (c) 2008 Pierre "delroth" Bourdon # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import re import cgi import sys from ConfigParser import ConfigParser from SocketServer import ThreadingTCPServer, StreamRequestHandler from subprocess import Popen, PIPE class InvalidRequestException(Exception): """ Raised when the incoming request can't be parsed. """ class KKHTTPRequest(object): """ Used to represent an incoming request received by the server. """ def __init__(self, source): self.source = source self.rawdata = '' self.method = '' self.path = '' self.version = '' self.headers = {} self.indata = False self.postdata = '' def feed(self, more): """ Feeds the parser with additionnal data. """ return self.refresh(more) def refresh(self, additionnal): toparse = [s.strip() for s in additionnal.split('\n')] for line in toparse: if not self.rawdata: try: method, path, version = line.split() method, version = [e.upper() for e in [method, version]] except Exception: raise InvalidRequestException, "malformed first line" if not method in ['GET', 'POST']: raise InvalidRequestException, "bad method used" if not path.startswith('/'): raise InvalidRequestException, "malformed path" if not version.startswith('HTTP/'): raise InvalidRequestException, "bad HTTP version" self.method, self.path, self.version = method, path, version elif line != '': if self.indata and self.method == 'GET': raise InvalidRequestException, "too much data for a GET" elif self.indata: self.postdata += line + '\n' try: header, value = [s.strip() for s in line.split(':', 1)] except Exception: raise InvalidRequestException, "malformed header" self.headers[header.lower()] = value else: if not "host" in self.headers: raise InvalidRequestException, "no host specified" if self.method == 'GET': return True elif self.method == 'POST' and not self.indata: if not "content-length" in self.headers: raise InvalidRequestException, "no content length" elif not "content-type" in self.headers: raise InvalidRequestException, "no content type" self.indata = True elif self.method == 'POST' and self.indata: return True self.rawdata += line + '\n' return False # More data should arrive. class KKURLMapper(object): def __init__(self, mapping={}): self.mapping = {} for rule in mapping: self.add_rule(rule, mapping[rule]) def add_rule(self, source, destination): reg = re.compile(source) self.mapping[reg] = destination def pathto(self, destination): for rule in self.mapping: match = rule.match(destination) if not match: continue try: return self.mapping[rule] % match.groups() except: return self.mapping[rule] % match.groupdict() return '' class KKHTTPResponse(object): def __init__(self, status='', headers={}, body=''): self.status, self.headers, self.body = status, headers, body def text(self): s = self.status s += '\r\n' s += '\r\n'.join(['%s: %s' % (key, self.headers[key]) for key in self.headers]) s += '\r\n' s += self.body s += '\r\n' return s class KKHTTP404(KKHTTPResponse): def __init__(self): super(KKHTTP404, self).__init__('HTTP/1.1 404 not found', {'Content-Type': 'text/html'}, '

Not found

') class KKHTTP500(KKHTTPResponse): def __init__(self, exception): body = '

Exception : %s

' % cgi.escape(repr(exception)) print "Exception has been raised : %r" % exception super(KKHTTP500, self).__init__('HTTP/1.1 500 internal server error', {'Content-Type': 'text/html'}, body) class KKHTTP200(KKHTTPResponse): def __init__(self, body): super(KKHTTP200, self).__init__('HTTP/1.1 200 OK', {}, body) class KKHTTPRequestHandler(StreamRequestHandler): def handle(self): httpreq = KKHTTPRequest(self.client_address) while not httpreq.feed(self.rfile.readline().strip()): continue try: self.wfile.write(self.make_resp(httpreq).text()) except Exception, ex: self.wfile.write(KKHTTP500(ex).text()) def make_resp(self, req): to_serve = self.server.find_file(req) if not to_serve: return KKHTTP404() try: return KKHTTP200(to_serve.read()) except Exception, ex: return KKHTTP500(ex) class KKHTTPServer(ThreadingTCPServer): allow_reuse_address = True def __init__(self): return def read_config(self, config): text = file(config).read() globs, locs = globals(), locals() globs['__name__'] = '__kakacheconfig__' exec text in globs, locs self.mappers = locs['mappers'] self.is_cgi = locs['is_cgi'] self.software = locs['server_version'] self.address = locs['host'] self.port = locs['port'] ThreadingTCPServer.__init__(self, (self.address, self.port), KKHTTPRequestHandler) def mapper(self, host): return self.mappers[host] def find_file(self, req): mapper = None for regexp in self.mappers: if re.match(regexp, req.headers['host']): mapper = self.mappers[regexp] break if not mapper: raise ValueError, "no route to host" path = mapper.pathto(req.path) path = self.filter(path) if not path: raise ValueError, "no map to URI" if self.is_cgi(path): return KKCGIHandler(self, req, path) else: return file(path) def filter(self, path): return path.replace('../', '') class KKCGIHandler(object): def __init__(self, server, request, path): self.server = server self.request = request self.path = path self.analyze_request() self.build_environ() def analyze_request(self): self.script_uri = self.request.path.split('?')[0] try: self.script_args = self.request.path.split('?', 1)[1] except IndexError: self.script_args = '' if self.request.method == 'POST': self.postdata = self.request.postdata self.postdatalen = self.request.headers['content-length'] self.postdatatype = self.request.headers['content-type'] def build_environ(self): env = {} env['SERVER_SOFTWARE'] = self.server.software env['SERVER_NAME'] = self.request.headers['host'] env['GATEWAY_INTERFACE'] = 'CGI/1.0' env['SERVER_PROTOCOL'] = self.request.version env['SERVER_PORT'] = str(self.request.source[1]) env['REQUEST_METHOD'] = self.request.method env['SCRIPT_NAME'] = self.script_uri env['QUERY_STRING'] = self.script_args env['REMOTE_ADDR'] = self.request.source[0] if self.request.method == 'POST': env['CONTENT_TYPE'] = self.postdatatype env['CONTENT_LENGTH'] = self.postdatalen for header in self.request.headers: headerstr = header.upper().replace('-', '_') env['HTTP_' + headerstr] = self.request.headers[header] self.environ = env def read(self): args = [self.path] for arg in self.script_args.split('&'): if not '=' in arg: args.append(arg) p = Popen(args, bufsize=1024, stdin=PIPE, stdout=PIPE, close_fds=True, env=self.environ) stdin, stdout = p.stdin, p.stdout if self.request.method == 'POST': stdin.write(self.postdata) stdin.flush() stdin.close() while p.poll(): continue return p.stdout.read() # Default config if __name__ == '__kakacheconfig__': mappers = { '^.*$': KKURLMapper( {r'^.*$': '/home/delroth/Desktop/kakache.py'} ), } is_cgi = lambda filename: filename.endswith('.cgi') server_version = 'Kakache 1.0' host = '0.0.0.0' port = 5899 if __name__ == '__main__': try: config = sys.argv[1] except IndexError: config = __file__ server = KKHTTPServer() try: server.read_config(config) server.serve_forever() except KeyboardInterrupt: server.server_close() finally: server.server_close()