Renamed project to Melvil
Getty Ritter
9 years ago
| 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 | 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 routeconf | |
| 8 | ||
| 9 | ||
| 10 | class Melvil(Resource): | |
| 11 | ''' | |
| 12 | The Twisted resource representing a set of redirects. This | |
| 13 | includes a RouteList object, which represents a set of possible | |
| 14 | routes. | |
| 15 | ''' | |
| 16 | isLeaf = True | |
| 17 | ||
| 18 | def __init__(self, routes, agent): | |
| 19 | Resource.__init__(self) | |
| 20 | self.routes = routes | |
| 21 | self.agent = agent | |
| 22 | ||
| 23 | def render_GET(self, request): | |
| 24 | routeconf.domain_for(request) | |
| 25 | return self.routes.handle(request, self.agent) | |
| 26 | ||
| 27 | @staticmethod | |
| 28 | def run_melvil(): | |
| 29 | ''' | |
| 30 | Run a Melvil instance. | |
| 31 | ''' | |
| 32 | if len(sys.argv) > 1: | |
| 33 | conf_dir = sys.argv[1] | |
| 34 | else: | |
| 35 | conf_dir = os.getenv('MELVIL_DIR', os.getcwd()) | |
| 36 | port = int(os.getenv('PORT', 8080)) | |
| 37 | ||
| 38 | routes = routeconf.load_routes(conf_dir) | |
| 39 | agent = Agent(reactor) | |
| 40 | sys.stderr.write(routes.pretty()) | |
| 41 | reactor.listenTCP(port, Site(Melvil(routes, agent))) | |
| 42 | reactor.run() | |
| 43 | ||
| 44 | if __name__ == '__main__': | |
| 45 | Melvil.run_melvil() |
| 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 == 'melvil': | |
| 52 | dispatch = MelvilDispatch( | |
| 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 MelvilDispatch(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) |
| 10 | 10 | return f.read() |
| 11 | 11 | |
| 12 | 12 | setup( |
| 13 |
name=' |
|
| 13 | name='melvil', | |
| 14 | 14 | version='0.0.6', |
| 15 | 15 | author='Getty Ritter', |
| 16 | 16 | author_email='getty.ritter@gmail.com', |
| 23 | 23 | long_description=read_file('README'), |
| 24 | 24 | entry_points={ |
| 25 | 25 | 'console_scripts': [ |
| 26 |
' |
|
| 26 | 'melvil = melvil:main' | |
| 27 | 27 | ] |
| 28 | 28 | }, |
| 29 | 29 | ) |