Started setting up Python packaging properly
Getty Ritter
9 years ago
| 1 | # aloysius | |
| 2 | ||
| 3 | **EARLY AND EXPERIMENTAL** | |
| 4 | ||
| 5 | Aloysius is the HTTP server interface I want to use. It's very | |
| 6 | slow at present, and still quite early, but it's at least a | |
| 7 | proof-of-concept of something I think should exist. | |
| 8 | ||
| 9 | ## Basic Use | |
| 10 | ||
| 11 | The Aloysius server does nothing but pass HTTP requests and | |
| 12 | responses between other servers: it is, in effect, a mechanism | |
| 13 | for establishing reverse proxies. | |
| 14 | ||
| 15 | The server is invoked with a single optional argument, and it | |
| 16 | continues running in the foreground until it is killed with | |
| 17 | standard Unix signals. The argument is a directory, and if that | |
| 18 | directory exists, it switches to that directory before | |
| 19 | continuing. It then reads configuration from that directory and | |
| 20 | will continuously forward requests based on that configuration. | |
| 21 | ||
| 22 | The configuration directory contains zero or more | |
| 23 | subdirectories, each of which describes a given request filter | |
| 24 | and forwarding mechanism. The subdirectory may contain several | |
| 25 | specifically named files, whose contents specify a forwarding | |
| 26 | system: | |
| 27 | ||
| 28 | ~~~ | |
| 29 | path: which request paths to match; defaults to "*" | |
| 30 | domain: which request subdomains to match; defaults to "*" | |
| 31 | mode: how to forward the request; defaults to "http" | |
| 32 | host: which host to forward to; defaults to "localhost" | |
| 33 | port: which port to forward to; defaults to "80" | |
| 34 | conf: which path to forward to; defaults to "/dev/null" | |
| 35 | resp: which HTTP response to issue; defaults to 303 | |
| 36 | ~~~ | |
| 37 | ||
| 38 | These are interpreted as follows: | |
| 39 | ||
| 40 | - The `path` and `domain` fields tell us which requests to forward: | |
| 41 | both of them default to accepting anything, and both of them | |
| 42 | allow their values to have the wildcard character `*`. | |
| 43 | ||
| 44 | - The `mode` field tells us _how_ to forward requests. There are | |
| 45 | three possible forwarding modes: | |
| 46 | - If the mode is `http`, then Aloysius will forward the HTTP | |
| 47 | request to the server listening on the host `host` and the | |
| 48 | port `port`. | |
| 49 | - If the mode is `aloys`, then Aloysius will recursively check | |
| 50 | the configuration directory at `conf`. | |
| 51 | - If the mode is `redir`, then Aloysius will respond with an | |
| 52 | HTTP response code as indicated in `resp` and redirect to | |
| 53 | the host as indicated in `host`. | |
| 54 | ||
| 55 | ## Example Setups | |
| 56 | ||
| 57 | Because configuration is specified as a directory, rather than as | |
| 58 | a single file, we can use properties of the Unix file system as a | |
| 59 | simple ACL-like mechanism. For example, a system administrator | |
| 60 | can set up a user-owned configuration directory for each user, | |
| 61 | and then use a global configuration directory to forward requests | |
| 62 | to that user on a per-subdomain basis: | |
| 63 | ||
| 64 | ~~~ | |
| 65 | $ mkdir -p /var/run/aloys | |
| 66 | $ for U in $USERS | |
| 67 | > do | |
| 68 | > # find the user's home directory | |
| 69 | > HOMEDIR=`cat /etc/passwd | grep ${U} | cut -d ':' -f 6` | |
| 70 | > | |
| 71 | > # add a configuration directory to each user | |
| 72 | > mkdir -p ${HOMEDIR}/aloys | |
| 73 | > chown ${U} ${HOMEDIR}/aloys | |
| 74 | > | |
| 75 | > # add a new forwarding rule for each user | |
| 76 | > mkdir -p /var/run/aloys/${U}-local | |
| 77 | > # make ${U}.example.com forward to the user's aloys configuration | |
| 78 | > echo "${U}.example.com" >/var/run/aloys/user-${U}/domain | |
| 79 | > echo "aloys" >/var/run/aloys/user-${U}/mode | |
| 80 | > echo "${HOMEDIR}/aloys" >/var/run/aloys/user-${U}/conf | |
| 81 | > done | |
| 82 | $ aloysius /var/run/aloys | |
| 83 | ~~~ | |
| 84 | ||
| 85 | Now, if a given user wants to set up a local HTTP server that | |
| 86 | produces dynamic content, they can add the appropriate forwarding | |
| 87 | configuration to their own directory, but they cannot modify | |
| 88 | other users' configurations or the global configuration. | |
| 89 | ||
| 90 | Even if you're running a single server, but want to have multiple | |
| 91 | services on it, this can be a convenient way to set up reverse | |
| 92 | proxy servers without needing root access. |
| 1 | # aloysius | |
| 2 | ||
| 3 | **EARLY AND EXPERIMENTAL** | |
| 4 | ||
| 5 | Aloysius is the HTTP server interface I want to use. It's very | |
| 6 | slow at present, and still quite early, but it's at least a | |
| 7 | proof-of-concept of something I think should exist. | |
| 8 | ||
| 9 | ## Basic Use | |
| 10 | ||
| 11 | The Aloysius server does nothing but pass HTTP requests and | |
| 12 | responses between other servers: it is, in effect, a mechanism | |
| 13 | for establishing reverse proxies. | |
| 14 | ||
| 15 | The server is invoked with a single optional argument, and it | |
| 16 | continues running in the foreground until it is killed with | |
| 17 | standard Unix signals. The argument is a directory, and if that | |
| 18 | directory exists, it switches to that directory before | |
| 19 | continuing. It then reads configuration from that directory and | |
| 20 | will continuously forward requests based on that configuration. | |
| 21 | ||
| 22 | The configuration directory contains zero or more | |
| 23 | subdirectories, each of which describes a given request filter | |
| 24 | and forwarding mechanism. The subdirectory may contain several | |
| 25 | specifically named files, whose contents specify a forwarding | |
| 26 | system: | |
| 27 | ||
| 28 | ~~~ | |
| 29 | path: which request paths to match; defaults to "*" | |
| 30 | domain: which request subdomains to match; defaults to "*" | |
| 31 | mode: how to forward the request; defaults to "http" | |
| 32 | host: which host to forward to; defaults to "localhost" | |
| 33 | port: which port to forward to; defaults to "80" | |
| 34 | conf: which path to forward to; defaults to "/dev/null" | |
| 35 | resp: which HTTP response to issue; defaults to 303 | |
| 36 | ~~~ | |
| 37 | ||
| 38 | These are interpreted as follows: | |
| 39 | ||
| 40 | - The `path` and `domain` fields tell us which requests to forward: | |
| 41 | both of them default to accepting anything, and both of them | |
| 42 | allow their values to have the wildcard character `*`. | |
| 43 | ||
| 44 | - The `mode` field tells us _how_ to forward requests. There are | |
| 45 | three possible forwarding modes: | |
| 46 | - If the mode is `http`, then Aloysius will forward the HTTP | |
| 47 | request to the server listening on the host `host` and the | |
| 48 | port `port`. | |
| 49 | - If the mode is `aloys`, then Aloysius will recursively check | |
| 50 | the configuration directory at `conf`. | |
| 51 | - If the mode is `redir`, then Aloysius will respond with an | |
| 52 | HTTP response code as indicated in `resp` and redirect to | |
| 53 | the host as indicated in `host`. | |
| 54 | ||
| 55 | ## Example Setups | |
| 56 | ||
| 57 | Because configuration is specified as a directory, rather than as | |
| 58 | a single file, we can use properties of the Unix file system as a | |
| 59 | simple ACL-like mechanism. For example, a system administrator | |
| 60 | can set up a user-owned configuration directory for each user, | |
| 61 | and then use a global configuration directory to forward requests | |
| 62 | to that user on a per-subdomain basis: | |
| 63 | ||
| 64 | ~~~ | |
| 65 | $ mkdir -p /var/run/aloys | |
| 66 | $ for U in $USERS | |
| 67 | > do | |
| 68 | > # find the user's home directory | |
| 69 | > HOMEDIR=`cat /etc/passwd | grep ${U} | cut -d ':' -f 6` | |
| 70 | > | |
| 71 | > # add a configuration directory to each user | |
| 72 | > mkdir -p ${HOMEDIR}/aloys | |
| 73 | > chown ${U} ${HOMEDIR}/aloys | |
| 74 | > | |
| 75 | > # add a new forwarding rule for each user | |
| 76 | > mkdir -p /var/run/aloys/${U}-local | |
| 77 | > # make ${U}.example.com forward to the user's aloys configuration | |
| 78 | > echo "${U}.example.com" >/var/run/aloys/user-${U}/domain | |
| 79 | > echo "aloys" >/var/run/aloys/user-${U}/mode | |
| 80 | > echo "${HOMEDIR}/aloys" >/var/run/aloys/user-${U}/conf | |
| 81 | > done | |
| 82 | $ aloysius /var/run/aloys | |
| 83 | ~~~ | |
| 84 | ||
| 85 | Now, if a given user wants to set up a local HTTP server that | |
| 86 | produces dynamic content, they can add the appropriate forwarding | |
| 87 | configuration to their own directory, but they cannot modify | |
| 88 | other users' configurations or the global configuration. | |
| 89 | ||
| 90 | Even if you're running a single server, but want to have multiple | |
| 91 | services on it, this can be a convenient way to set up reverse | |
| 92 | proxy servers without needing root access. |
| 1 | from twisted.web.client import Agent | |
| 2 | from twisted.web.server import Site | |
| 3 | from twisted.web.resource import Resource | |
| 4 | from twisted.internet import reactor | |
| 5 | import os | |
| 6 | import sys | |
| 7 | import yaml | |
| 8 | import routeconf | |
| 9 | ||
| 10 | ||
| 11 | class Sample(Resource): | |
| 12 | isLeaf = True | |
| 13 | ||
| 14 | def __init__(self, routes, agent): | |
| 15 | Resource.__init__(self) | |
| 16 | self.routes = routes | |
| 17 | self.agent = agent | |
| 18 | ||
| 19 | def render_GET(self, request): | |
| 20 | routeconf.domain_for(request) | |
| 21 | return self.routes.handle(request, self.agent) | |
| 22 | ||
| 23 | ||
| 24 | def main(): | |
| 25 | if len(sys.argv) > 1: | |
| 26 | conf_dir = sys.argv[1] | |
| 27 | else: | |
| 28 | conf_dir = os.getenv('ALOYS_DIR', os.getcwd()) | |
| 29 | port = int(os.getenv('PORT', 8080)) | |
| 30 | ||
| 31 | routes = routeconf.load_routes(conf_dir) | |
| 32 | agent = Agent(reactor) | |
| 33 | sys.stderr.write(routes.pretty()) | |
| 34 | reactor.listenTCP(port, Site(Sample(routes, agent))) | |
| 35 | reactor.run() | |
| 36 | ||
| 37 | if __name__ == '__main__': | |
| 38 | main() |
| 1 | import os | |
| 2 | import sys | |
| 3 | from twisted.web.server import NOT_DONE_YET | |
| 4 | from twisted.web.client import readBody | |
| 5 | ||
| 6 | ||
| 7 | def load_routes(location): | |
| 8 | routes = [] | |
| 9 | if not os.path.isdir(location): | |
| 10 | raise Exception( | |
| 11 | 'Cannot find specified configure directory: {0}', location) | |
| 12 | for site in sorted(os.listdir(location)): | |
| 13 | try: | |
| 14 | routes.append(load_site(os.path.join(location, site))) | |
| 15 | except Exception as e: | |
| 16 | sys.stderr.write('Unable to load {0}; skipping\n'.format( | |
| 17 | os.path.abspath(os.path.join(location, site)))) | |
| 18 | sys.stderr.write(' {0}\n\n'.format(e)) | |
| 19 | return RouteList(routes) | |
| 20 | ||
| 21 | ||
| 22 | def try_load(filename, default=None): | |
| 23 | if not os.path.exists(filename): | |
| 24 | return default | |
| 25 | if not os.path.isfile(os.path.abspath(filename)): | |
| 26 | raise Exception( | |
| 27 | 'Cannot load {0}: is not a file'.format(filename)) | |
| 28 | with open(filename) as f: | |
| 29 | return f.read().strip() | |
| 30 | ||
| 31 | ||
| 32 | def domain_for(request): | |
| 33 | return request.getHeader('host') | |
| 34 | ||
| 35 | ||
| 36 | def load_site(loc): | |
| 37 | if not os.path.isdir(loc): | |
| 38 | raise Exception('Site spec not a directory: {0}'.format(loc)) | |
| 39 | ||
| 40 | if (not os.path.exists(os.path.join(loc, 'domain')) and | |
| 41 | not os.path.exists(os.path.join(loc, 'path'))): | |
| 42 | raise Exception( | |
| 43 | 'The site {0} does not specify a domain or a path'.format(loc)) | |
| 44 | domain = try_load(os.path.join(loc, 'domain'), '*') | |
| 45 | path = try_load(os.path.join(loc, 'path'), '*').lstrip('/') | |
| 46 | mode = try_load(os.path.join(loc, 'mode'), 'http') | |
| 47 | if mode == 'http': | |
| 48 | dispatch = HTTPDispatch( | |
| 49 | host=try_load(os.path.join(loc, 'host'), 'localhost'), | |
| 50 | port=int(try_load(os.path.join(loc, 'port'), 80))) | |
| 51 | elif mode == 'aloys': | |
| 52 | dispatch = AloysDispatch( | |
| 53 | loc=try_load(os.path.join(loc, 'conf'), '/dev/null')) | |
| 54 | elif mode == 'redir': | |
| 55 | dispatch = RedirDispatch( | |
| 56 | resp=int(try_load(os.path.join(loc, 'resp'), 303)), | |
| 57 | location=try_load(os.path.join(loc, 'location'), | |
| 58 | 'http://localhost/')) | |
| 59 | else: | |
| 60 | raise Exception('Unknown mode: {0}'.format(repr(mode))) | |
| 61 | return Route(domain=domain, path=path, dispatch=dispatch, disk_loc=loc) | |
| 62 | ||
| 63 | ||
| 64 | def wildcard_match(spec, string): | |
| 65 | loc_stack = [(0, 0)] | |
| 66 | while loc_stack: | |
| 67 | (i, j) = loc_stack.pop() | |
| 68 | if i >= len(spec) and j >= len(string): | |
| 69 | return True | |
| 70 | elif i >= len(spec) or j >= len(string): | |
| 71 | continue | |
| 72 | elif spec[i] == string[j]: | |
| 73 | loc_stack.append((i + 1, j + 1)) | |
| 74 | elif spec[i] == '*': | |
| 75 | loc_stack.append((i + 1, j + 1)) | |
| 76 | loc_stack.append((i, j + 1)) | |
| 77 | else: | |
| 78 | continue | |
| 79 | return False | |
| 80 | ||
| 81 | ||
| 82 | class RouteList: | |
| 83 | ''' | |
| 84 | An object which represents an ordered set of possible routing | |
| 85 | options. | |
| 86 | ''' | |
| 87 | ||
| 88 | def __init__(self, routes): | |
| 89 | self.routes = routes | |
| 90 | ||
| 91 | def handle(self, request, agent): | |
| 92 | for route in self.routes: | |
| 93 | if route.matches(request): | |
| 94 | return route.handle(request, agent) | |
| 95 | return "unable to handle request" | |
| 96 | ||
| 97 | def __repr__(self): | |
| 98 | return 'RouteList([{0}])'.format( | |
| 99 | ', '.join(repr(r) for r in self.routes)) | |
| 100 | ||
| 101 | def pretty(self, level=0): | |
| 102 | return ''.join( | |
| 103 | ' ' * level + r.pretty(level=level) | |
| 104 | for r in self.routes) | |
| 105 | ||
| 106 | ||
| 107 | class Route: | |
| 108 | ||
| 109 | def __init__(self, domain, path, dispatch, disk_loc=''): | |
| 110 | self.domain = domain | |
| 111 | self.path = path | |
| 112 | self.dispatch = dispatch | |
| 113 | self.disk_loc = disk_loc | |
| 114 | ||
| 115 | def matches(self, request): | |
| 116 | return (wildcard_match(self.path, '/' + request.path) and | |
| 117 | wildcard_match(self.domain, domain_for(request))) | |
| 118 | ||
| 119 | def handle(self, request, agent): | |
| 120 | print 'handled by {0}'.format(self.disk_loc) | |
| 121 | return self.dispatch.handle(request, agent) | |
| 122 | ||
| 123 | def __repr__(self): | |
| 124 | return 'Route({0}, {1}, {2}, disk_loc={3})'.format( | |
| 125 | repr(self.domain), | |
| 126 | repr(self.path), | |
| 127 | self.dispatch, | |
| 128 | repr(self.disk_loc)) | |
| 129 | ||
| 130 | def pretty(self, level=0): | |
| 131 | return '{0}/{1} => {2}'.format(self.domain, | |
| 132 | self.path, | |
| 133 | self.dispatch.pretty(level=level)) | |
| 134 | ||
| 135 | ||
| 136 | class Dispatch: | |
| 137 | ||
| 138 | def handle(self, request, agent): | |
| 139 | return '[no route matched your request]' | |
| 140 | ||
| 141 | def __repr__(self): | |
| 142 | return '{name}({args})'.format( | |
| 143 | name=self.__class__.__name__, | |
| 144 | args=', '.join('{0}={1}'.format(k, repr(v)) | |
| 145 | for (k, v) in self.__dict__.items())) | |
| 146 | ||
| 147 | ||
| 148 | class HTTPDispatch(Dispatch): | |
| 149 | ||
| 150 | def __init__(self, host='localhost', port=80): | |
| 151 | self.host = host | |
| 152 | self.port = port | |
| 153 | ||
| 154 | def handle(self, request, agent): | |
| 155 | def callback(response): | |
| 156 | for (k, v) in response.headers.getAllRawHeaders(): | |
| 157 | request.setHeader(k, v[0]) | |
| 158 | r = readBody(response) | |
| 159 | r.addCallback(handle_body) | |
| 160 | return r | |
| 161 | ||
| 162 | def handle_body(body): | |
| 163 | request.write(body) | |
| 164 | request.finish() | |
| 165 | ||
| 166 | url = 'http://{0}:{1}{2}'.format( | |
| 167 | self.host, self.port, request.path) | |
| 168 | print url | |
| 169 | agent.request('GET', url, request.requestHeaders, None) \ | |
| 170 | .addCallback(callback) | |
| 171 | return NOT_DONE_YET | |
| 172 | ||
| 173 | def pretty(self, level=0): | |
| 174 | return '{0}:{1}\n'.format(self.host, self.port) | |
| 175 | ||
| 176 | ||
| 177 | class AloysDispatch(Dispatch): | |
| 178 | ''' | |
| 179 | Dispatch the route down to a different configuration directory. | |
| 180 | ''' | |
| 181 | ||
| 182 | def __init__(self, loc): | |
| 183 | self.routes = load_routes(loc) | |
| 184 | ||
| 185 | def handle(self, request, agent): | |
| 186 | return self.routes.handle(request) | |
| 187 | ||
| 188 | def pretty(self, level=0): | |
| 189 | return '\n' + self.routes.pretty(level=level + 2) | |
| 190 | ||
| 191 | ||
| 192 | class RedirDispatch(Dispatch): | |
| 193 | ''' | |
| 194 | Use a redirect code (probably 303) to manually redirect to a | |
| 195 | different location. | |
| 196 | ''' | |
| 197 | ||
| 198 | def __init__(self, resp=303, location='http://localhost/'): | |
| 199 | self.resp = resp | |
| 200 | self.location = location | |
| 201 | ||
| 202 | def handle(self, request, agent): | |
| 203 | request.setResponseCode(self.resp) | |
| 204 | request.setHeader('location', self.location) | |
| 205 | return '' | |
| 206 | ||
| 207 | def pretty(self, level=0): | |
| 208 | return 'redir({0})\n'.format(self.location) |
| 1 | import os | |
| 2 | from setuptools import find_packages, setup | |
| 3 | ||
| 4 | ||
| 5 | def read_file(filename): | |
| 6 | ''' | |
| 7 | Read the contents of a file relative to this one. | |
| 8 | ''' | |
| 9 | with open(os.path.join(os.path.dirname(__file__), filename)) as f: | |
| 10 | return f.read() | |
| 11 | ||
| 12 | setup( | |
| 13 | name='aloysius', | |
| 14 | version='0.0.6', | |
| 15 | author='Getty Ritter', | |
| 16 | author_email='getty.ritter@gmail.com', | |
| 17 | description='A simple, configurable HTTP reverse proxy.', | |
| 18 | install_requires='twisted', | |
| 19 | packages=find_packages(), | |
| 20 | license='BSD', | |
| 21 | keywords='http', | |
| 22 | url='http://infinitenegativeutility.com/software/aloysius', | |
| 23 | long_description=read_file('README'), | |
| 24 | entry_points={ | |
| 25 | 'console_scripts': [ | |
| 26 | 'aloysius = aloysius:main' | |
| 27 | ] | |
| 28 | }, | |
| 29 | ) |
| 1 | from twisted.web.client import Agent | |
| 2 | from twisted.web.server import Site | |
| 3 | from twisted.web.resource import Resource | |
| 4 | from twisted.internet import reactor | |
| 5 | import yaml | |
| 6 | import routeconf | |
| 7 | ||
| 8 | ||
| 9 | class Sample(Resource): | |
| 10 | isLeaf = True | |
| 11 | ||
| 12 | def __init__(self, routes, agent): | |
| 13 | Resource.__init__(self) | |
| 14 | self.routes = routes | |
| 15 | self.agent = agent | |
| 16 | ||
| 17 | def render_GET(self, request): | |
| 18 | routeconf.domain_for(request) | |
| 19 | return self.routes.handle(request, self.agent) | |
| 20 | ||
| 21 | if __name__ == '__main__': | |
| 22 | routes = routeconf.load_routes('sample') | |
| 23 | agent = Agent(reactor) | |
| 24 | print routes.pretty() | |
| 25 | reactor.listenTCP(8080, Site(Sample(routes, agent))) | |
| 26 | reactor.run() |
| 1 | import os | |
| 2 | import sys | |
| 3 | from twisted.web.server import NOT_DONE_YET | |
| 4 | from twisted.web.client import readBody | |
| 5 | ||
| 6 | ||
| 7 | def load_routes(location): | |
| 8 | routes = [] | |
| 9 | if not os.path.isdir(location): | |
| 10 | raise Exception( | |
| 11 | 'Cannot find specified configure directory: {0}', location) | |
| 12 | for site in sorted(os.listdir(location)): | |
| 13 | try: | |
| 14 | routes.append(load_site(os.path.join(location, site))) | |
| 15 | except Exception as e: | |
| 16 | sys.stderr.write('Unable to load {0}; skipping\n'.format( | |
| 17 | os.path.abspath(os.path.join(location, site)))) | |
| 18 | sys.stderr.write(' {0}\n\n'.format(e)) | |
| 19 | return RouteList(routes) | |
| 20 | ||
| 21 | ||
| 22 | def try_load(filename, default=None): | |
| 23 | if not os.path.exists(filename): | |
| 24 | return default | |
| 25 | if not os.path.isfile(os.path.abspath(filename)): | |
| 26 | raise Exception( | |
| 27 | 'Cannot load {0}: is not a file'.format(filename)) | |
| 28 | with open(filename) as f: | |
| 29 | return f.read().strip() | |
| 30 | ||
| 31 | ||
| 32 | def domain_for(request): | |
| 33 | return request.getHeader('host') | |
| 34 | ||
| 35 | ||
| 36 | def load_site(loc): | |
| 37 | if not os.path.isdir(loc): | |
| 38 | raise Exception('Site spec not a directory: {0}'.format(loc)) | |
| 39 | ||
| 40 | if (not os.path.exists(os.path.join(loc, 'domain')) and | |
| 41 | not os.path.exists(os.path.join(loc, 'path'))): | |
| 42 | raise Exception( | |
| 43 | 'The site {0} does not specify a domain or a path'.format(loc)) | |
| 44 | domain = try_load(os.path.join(loc, 'domain'), '*') | |
| 45 | path = try_load(os.path.join(loc, 'path'), '*').lstrip('/') | |
| 46 | mode = try_load(os.path.join(loc, 'mode'), 'http') | |
| 47 | if mode == 'http': | |
| 48 | dispatch = HTTPDispatch( | |
| 49 | host=try_load(os.path.join(loc, 'host'), 'localhost'), | |
| 50 | port=int(try_load(os.path.join(loc, 'port'), 80))) | |
| 51 | elif mode == 'aloys': | |
| 52 | dispatch = AloysDispatch( | |
| 53 | loc=try_load(os.path.join(loc, 'conf'), '/dev/null')) | |
| 54 | elif mode == 'redir': | |
| 55 | dispatch = RedirDispatch( | |
| 56 | resp=int(try_load(os.path.join(loc, 'resp'), 303)), | |
| 57 | location=try_load(os.path.join(loc, 'location'), | |
| 58 | 'http://localhost/')) | |
| 59 | else: | |
| 60 | raise Exception('Unknown mode: {0}'.format(repr(mode))) | |
| 61 | return Route(domain=domain, path=path, dispatch=dispatch, disk_loc=loc) | |
| 62 | ||
| 63 | ||
| 64 | def wildcard_match(spec, string): | |
| 65 | loc_stack = [(0, 0)] | |
| 66 | while loc_stack: | |
| 67 | (i, j) = loc_stack.pop() | |
| 68 | if i >= len(spec) and j >= len(string): | |
| 69 | return True | |
| 70 | elif i >= len(spec) or j >= len(string): | |
| 71 | continue | |
| 72 | elif spec[i] == string[j]: | |
| 73 | loc_stack.append((i + 1, j + 1)) | |
| 74 | elif spec[i] == '*': | |
| 75 | loc_stack.append((i + 1, j + 1)) | |
| 76 | loc_stack.append((i, j + 1)) | |
| 77 | else: | |
| 78 | continue | |
| 79 | return False | |
| 80 | ||
| 81 | ||
| 82 | class RouteList: | |
| 83 | ''' | |
| 84 | An object which represents an ordered set of possible routing | |
| 85 | options. | |
| 86 | ''' | |
| 87 | ||
| 88 | def __init__(self, routes): | |
| 89 | self.routes = routes | |
| 90 | ||
| 91 | def handle(self, request, agent): | |
| 92 | for route in self.routes: | |
| 93 | if route.matches(request): | |
| 94 | return route.handle(request, agent) | |
| 95 | return "unable to handle request" | |
| 96 | ||
| 97 | def __repr__(self): | |
| 98 | return 'RouteList([{0}])'.format( | |
| 99 | ', '.join(repr(r) for r in self.routes)) | |
| 100 | ||
| 101 | def pretty(self, level=0): | |
| 102 | return ''.join( | |
| 103 | ' ' * level + r.pretty(level=level) | |
| 104 | for r in self.routes) | |
| 105 | ||
| 106 | ||
| 107 | class Route: | |
| 108 | ||
| 109 | def __init__(self, domain, path, dispatch, disk_loc=''): | |
| 110 | self.domain = domain | |
| 111 | self.path = path | |
| 112 | self.dispatch = dispatch | |
| 113 | self.disk_loc = disk_loc | |
| 114 | ||
| 115 | def matches(self, request): | |
| 116 | return (wildcard_match(self.path, '/' + request.path) and | |
| 117 | wildcard_match(self.domain, domain_for(request))) | |
| 118 | ||
| 119 | def handle(self, request, agent): | |
| 120 | print 'handled by {0}'.format(self.disk_loc) | |
| 121 | return self.dispatch.handle(request, agent) | |
| 122 | ||
| 123 | def __repr__(self): | |
| 124 | return 'Route({0}, {1}, {2}, disk_loc={3})'.format( | |
| 125 | repr(self.domain), | |
| 126 | repr(self.path), | |
| 127 | self.dispatch, | |
| 128 | repr(self.disk_loc)) | |
| 129 | ||
| 130 | def pretty(self, level=0): | |
| 131 | return '{0}/{1} => {2}'.format(self.domain, | |
| 132 | self.path, | |
| 133 | self.dispatch.pretty(level=level)) | |
| 134 | ||
| 135 | ||
| 136 | class Dispatch: | |
| 137 | ||
| 138 | def handle(self, request, agent): | |
| 139 | return '[no route matched your request]' | |
| 140 | ||
| 141 | def __repr__(self): | |
| 142 | return '{name}({args})'.format( | |
| 143 | name=self.__class__.__name__, | |
| 144 | args=', '.join('{0}={1}'.format(k, repr(v)) | |
| 145 | for (k, v) in self.__dict__.items())) | |
| 146 | ||
| 147 | ||
| 148 | class HTTPDispatch(Dispatch): | |
| 149 | ||
| 150 | def __init__(self, host='localhost', port=80): | |
| 151 | self.host = host | |
| 152 | self.port = port | |
| 153 | ||
| 154 | def handle(self, request, agent): | |
| 155 | def callback(response): | |
| 156 | for (k, v) in response.headers.getAllRawHeaders(): | |
| 157 | request.setHeader(k, v[0]) | |
| 158 | r = readBody(response) | |
| 159 | r.addCallback(handle_body) | |
| 160 | return r | |
| 161 | ||
| 162 | def handle_body(body): | |
| 163 | request.write(body) | |
| 164 | request.finish() | |
| 165 | ||
| 166 | url = 'http://{0}:{1}{2}'.format( | |
| 167 | self.host, self.port, request.path) | |
| 168 | print url | |
| 169 | agent.request('GET', url, request.requestHeaders, None) \ | |
| 170 | .addCallback(callback) | |
| 171 | return NOT_DONE_YET | |
| 172 | ||
| 173 | def pretty(self, level=0): | |
| 174 | return '{0}:{1}\n'.format(self.host, self.port) | |
| 175 | ||
| 176 | ||
| 177 | class AloysDispatch(Dispatch): | |
| 178 | ''' | |
| 179 | Dispatch the route down to a different configuration directory. | |
| 180 | ''' | |
| 181 | ||
| 182 | def __init__(self, loc): | |
| 183 | self.routes = load_routes(loc) | |
| 184 | ||
| 185 | def handle(self, request, agent): | |
| 186 | return self.routes.handle(request) | |
| 187 | ||
| 188 | def pretty(self, level=0): | |
| 189 | return '\n' + self.routes.pretty(level=level + 2) | |
| 190 | ||
| 191 | ||
| 192 | class RedirDispatch(Dispatch): | |
| 193 | ''' | |
| 194 | Use a redirect code (probably 303) to manually redirect to a | |
| 195 | different location. | |
| 196 | ''' | |
| 197 | ||
| 198 | def __init__(self, resp=303, location='http://localhost/'): | |
| 199 | self.resp = resp | |
| 200 | self.location = location | |
| 201 | ||
| 202 | def handle(self, request, agent): | |
| 203 | request.setResponseCode(self.resp) | |
| 204 | request.setHeader('location', self.location) | |
| 205 | return '' | |
| 206 | ||
| 207 | def pretty(self, level=0): | |
| 208 | return 'redir({0})\n'.format(self.location) |