OLD | NEW |
(Empty) | |
| 1 import cgi |
| 2 import json |
| 3 import os |
| 4 import traceback |
| 5 import urllib |
| 6 import urlparse |
| 7 |
| 8 from constants import content_types |
| 9 from pipes import Pipeline, template |
| 10 from ranges import RangeParser |
| 11 from request import Authentication |
| 12 from response import MultipartContent |
| 13 from utils import HTTPException |
| 14 |
| 15 __all__ = ["file_handler", "python_script_handler", |
| 16 "FunctionHandler", "handler", "json_handler", |
| 17 "as_is_handler", "ErrorHandler", "BasicAuthHandler"] |
| 18 |
| 19 |
| 20 def guess_content_type(path): |
| 21 ext = os.path.splitext(path)[1].lstrip(".") |
| 22 if ext in content_types: |
| 23 return content_types[ext] |
| 24 |
| 25 return "application/octet-stream" |
| 26 |
| 27 |
| 28 |
| 29 def filesystem_path(base_path, request, url_base="/"): |
| 30 if base_path is None: |
| 31 base_path = request.doc_root |
| 32 |
| 33 path = request.url_parts.path |
| 34 |
| 35 if path.startswith(url_base): |
| 36 path = path[len(url_base):] |
| 37 |
| 38 if ".." in path: |
| 39 raise HTTPException(404) |
| 40 |
| 41 new_path = os.path.join(base_path, path) |
| 42 |
| 43 # Otherwise setting path to / allows access outside the root directory |
| 44 if not new_path.startswith(base_path): |
| 45 raise HTTPException(404) |
| 46 |
| 47 return new_path |
| 48 |
| 49 class DirectoryHandler(object): |
| 50 def __init__(self, base_path=None, url_base="/"): |
| 51 self.base_path = base_path |
| 52 self.url_base = url_base |
| 53 |
| 54 def __call__(self, request, response): |
| 55 if not request.url_parts.path.endswith("/"): |
| 56 raise HTTPException(404) |
| 57 |
| 58 path = filesystem_path(self.base_path, request, self.url_base) |
| 59 |
| 60 assert os.path.isdir(path) |
| 61 |
| 62 response.headers = [("Content-Type", "text/html")] |
| 63 response.content = """<!doctype html> |
| 64 <meta name="viewport" content="width=device-width"> |
| 65 <title>Directory listing for %(path)s</title> |
| 66 <h1>Directory listing for %(path)s</h1> |
| 67 <ul> |
| 68 %(items)s |
| 69 </li> |
| 70 """ % {"path": cgi.escape(request.url_parts.path), |
| 71 "items": "\n".join(self.list_items(request, path))} |
| 72 |
| 73 def list_items(self, request, path): |
| 74 # TODO: this won't actually list all routes, only the |
| 75 # ones that correspond to a real filesystem path. It's |
| 76 # not possible to list every route that will match |
| 77 # something, but it should be possible to at least list the |
| 78 # statically defined ones |
| 79 base_path = request.url_parts.path |
| 80 |
| 81 if not base_path.endswith("/"): |
| 82 base_path += "/" |
| 83 if base_path != "/": |
| 84 link = urlparse.urljoin(base_path, "..") |
| 85 yield ("""<li class="dir"><a href="%(link)s">%(name)s</a>""" % |
| 86 {"link": link, "name": ".."}) |
| 87 for item in sorted(os.listdir(path)): |
| 88 link = cgi.escape(urllib.quote(item)) |
| 89 if os.path.isdir(os.path.join(path, item)): |
| 90 link += "/" |
| 91 class_ = "dir" |
| 92 else: |
| 93 class_ = "file" |
| 94 yield ("""<li class="%(class)s"><a href="%(link)s">%(name)s</a>""" % |
| 95 {"link": link, "name": cgi.escape(item), "class": class_}) |
| 96 |
| 97 |
| 98 directory_handler = DirectoryHandler() |
| 99 |
| 100 |
| 101 class FileHandler(object): |
| 102 def __init__(self, base_path=None, url_base="/"): |
| 103 self.base_path = base_path |
| 104 self.url_base = url_base |
| 105 self.directory_handler = DirectoryHandler(self.base_path) |
| 106 |
| 107 def __call__(self, request, response): |
| 108 path = filesystem_path(self.base_path, request, self.url_base) |
| 109 |
| 110 if os.path.isdir(path): |
| 111 return self.directory_handler(request, response) |
| 112 try: |
| 113 #This is probably racy with some other process trying to change the
file |
| 114 file_size = os.stat(path).st_size |
| 115 response.headers.update(self.get_headers(request, path)) |
| 116 if "Range" in request.headers: |
| 117 try: |
| 118 byte_ranges = RangeParser()(request.headers['Range'], file_s
ize) |
| 119 except HTTPException as e: |
| 120 if e.code == 416: |
| 121 response.headers.set("Content-Range", "bytes */%i" % fil
e_size) |
| 122 raise |
| 123 else: |
| 124 byte_ranges = None |
| 125 data = self.get_data(response, path, byte_ranges) |
| 126 response.content = data |
| 127 query = urlparse.parse_qs(request.url_parts.query) |
| 128 |
| 129 pipeline = None |
| 130 if "pipe" in query: |
| 131 pipeline = Pipeline(query["pipe"][-1]) |
| 132 elif os.path.splitext(path)[0].endswith(".sub"): |
| 133 pipeline = Pipeline("sub") |
| 134 if pipeline is not None: |
| 135 response = pipeline(request, response) |
| 136 |
| 137 return response |
| 138 |
| 139 except (OSError, IOError): |
| 140 raise HTTPException(404) |
| 141 |
| 142 def get_headers(self, request, path): |
| 143 rv = self.default_headers(path) |
| 144 rv.extend(self.load_headers(request, os.path.join(os.path.split(path)[0]
, "__dir__"))) |
| 145 rv.extend(self.load_headers(request, path)) |
| 146 return rv |
| 147 |
| 148 def load_headers(self, request, path): |
| 149 headers_path = path + ".sub.headers" |
| 150 if os.path.exists(headers_path): |
| 151 use_sub = True |
| 152 else: |
| 153 headers_path = path + ".headers" |
| 154 use_sub = False |
| 155 |
| 156 try: |
| 157 with open(headers_path) as headers_file: |
| 158 data = headers_file.read() |
| 159 except IOError: |
| 160 return [] |
| 161 else: |
| 162 if use_sub: |
| 163 data = template(request, data) |
| 164 return [tuple(item.strip() for item in line.split(":", 1)) |
| 165 for line in data.splitlines() if line] |
| 166 |
| 167 def get_data(self, response, path, byte_ranges): |
| 168 with open(path, 'rb') as f: |
| 169 if byte_ranges is None: |
| 170 return f.read() |
| 171 else: |
| 172 response.status = 206 |
| 173 if len(byte_ranges) > 1: |
| 174 parts_content_type, content = self.set_response_multipart(re
sponse, |
| 175 by
te_ranges, |
| 176 f) |
| 177 for byte_range in byte_ranges: |
| 178 content.append_part(self.get_range_data(f, byte_range), |
| 179 parts_content_type, |
| 180 [("Content-Range", byte_range.header
_value())]) |
| 181 return content |
| 182 else: |
| 183 response.headers.set("Content-Range", byte_ranges[0].header_
value()) |
| 184 return self.get_range_data(f, byte_ranges[0]) |
| 185 |
| 186 def set_response_multipart(self, response, ranges, f): |
| 187 parts_content_type = response.headers.get("Content-Type") |
| 188 if parts_content_type: |
| 189 parts_content_type = parts_content_type[-1] |
| 190 else: |
| 191 parts_content_type = None |
| 192 content = MultipartContent() |
| 193 response.headers.set("Content-Type", "multipart/byteranges; boundary=%s"
% content.boundary) |
| 194 return parts_content_type, content |
| 195 |
| 196 def get_range_data(self, f, byte_range): |
| 197 f.seek(byte_range.lower) |
| 198 return f.read(byte_range.upper - byte_range.lower) |
| 199 |
| 200 def default_headers(self, path): |
| 201 return [("Content-Type", guess_content_type(path))] |
| 202 |
| 203 |
| 204 file_handler = FileHandler() |
| 205 |
| 206 |
| 207 class PythonScriptHandler(object): |
| 208 def __init__(self, base_path=None, url_base="/"): |
| 209 self.base_path = base_path |
| 210 self.url_base = url_base |
| 211 |
| 212 def __call__(self, request, response): |
| 213 path = filesystem_path(self.base_path, request, self.url_base) |
| 214 |
| 215 try: |
| 216 environ = {"__file__": path} |
| 217 execfile(path, environ, environ) |
| 218 if "main" in environ: |
| 219 handler = FunctionHandler(environ["main"]) |
| 220 handler(request, response) |
| 221 else: |
| 222 raise HTTPException(500, "No main function in script %s" % path) |
| 223 except IOError: |
| 224 raise HTTPException(404) |
| 225 |
| 226 python_script_handler = PythonScriptHandler() |
| 227 |
| 228 class FunctionHandler(object): |
| 229 def __init__(self, func): |
| 230 self.func = func |
| 231 |
| 232 def __call__(self, request, response): |
| 233 try: |
| 234 rv = self.func(request, response) |
| 235 except Exception: |
| 236 msg = traceback.format_exc() |
| 237 raise HTTPException(500, message=msg) |
| 238 if rv is not None: |
| 239 if isinstance(rv, tuple): |
| 240 if len(rv) == 3: |
| 241 status, headers, content = rv |
| 242 response.status = status |
| 243 elif len(rv) == 2: |
| 244 headers, content = rv |
| 245 else: |
| 246 raise HTTPException(500) |
| 247 response.headers.update(headers) |
| 248 else: |
| 249 content = rv |
| 250 response.content = content |
| 251 |
| 252 |
| 253 #The generic name here is so that this can be used as a decorator |
| 254 def handler(func): |
| 255 return FunctionHandler(func) |
| 256 |
| 257 |
| 258 class JsonHandler(object): |
| 259 def __init__(self, func): |
| 260 self.func = func |
| 261 |
| 262 def __call__(self, request, response): |
| 263 return FunctionHandler(self.handle_request)(request, response) |
| 264 |
| 265 def handle_request(self, request, response): |
| 266 rv = self.func(request, response) |
| 267 response.headers.set("Content-Type", "application/json") |
| 268 enc = json.dumps |
| 269 if isinstance(rv, tuple): |
| 270 rv = list(rv) |
| 271 value = tuple(rv[:-1] + [enc(rv[-1])]) |
| 272 length = len(value[-1]) |
| 273 else: |
| 274 value = enc(rv) |
| 275 length = len(value) |
| 276 response.headers.set("Content-Length", length) |
| 277 return value |
| 278 |
| 279 def json_handler(func): |
| 280 return JsonHandler(func) |
| 281 |
| 282 class AsIsHandler(object): |
| 283 def __init__(self, base_path=None, url_base="/"): |
| 284 self.base_path = base_path |
| 285 self.url_base = url_base |
| 286 |
| 287 def __call__(self, request, response): |
| 288 path = filesystem_path(self.base_path, request, self.url_base) |
| 289 |
| 290 try: |
| 291 with open(path) as f: |
| 292 response.writer.write_content(f.read()) |
| 293 response.close_connection = True |
| 294 except IOError: |
| 295 raise HTTPException(404) |
| 296 |
| 297 as_is_handler = AsIsHandler() |
| 298 |
| 299 class BasicAuthHandler(object): |
| 300 def __init__(self, handler, user, password): |
| 301 """ |
| 302 A Basic Auth handler |
| 303 |
| 304 :Args: |
| 305 - handler: a secondary handler for the request after authentication is
successful (example file_handler) |
| 306 - user: string of the valid user name or None if any / all credentials
are allowed |
| 307 - password: string of the password required |
| 308 """ |
| 309 self.user = user |
| 310 self.password = password |
| 311 self.handler = handler |
| 312 |
| 313 def __call__(self, request, response): |
| 314 if "authorization" not in request.headers: |
| 315 response.status = 401 |
| 316 response.headers.set("WWW-Authenticate", "Basic") |
| 317 return response |
| 318 else: |
| 319 auth = Authentication(request.headers) |
| 320 if self.user is not None and (self.user != auth.username or self.pas
sword != auth.password): |
| 321 response.set_error(403, "Invalid username or password") |
| 322 return response |
| 323 return self.handler(request, response) |
| 324 |
| 325 basic_auth_handler = BasicAuthHandler(file_handler, None, None) |
| 326 |
| 327 class ErrorHandler(object): |
| 328 def __init__(self, status): |
| 329 self.status = status |
| 330 |
| 331 def __call__(self, request, response): |
| 332 response.set_error(self.status) |
OLD | NEW |