common.py
193 lines
| 6.4 KiB
| text/x-python
|
PythonLexer
Eric Hopper
|
r2391 | # hgweb/common.py - Utility functions needed by hgweb_mod and hgwebdir_mod | ||
Eric Hopper
|
r2356 | # | ||
# Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net> | ||||
Vadim Gelfer
|
r2859 | # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com> | ||
Eric Hopper
|
r2356 | # | ||
Martin Geisler
|
r8225 | # This software may be used and distributed according to the terms of the | ||
Matt Mackall
|
r10263 | # GNU General Public License version 2 or any later version. | ||
Eric Hopper
|
r2356 | |||
Yuya Nishihara
|
r27046 | from __future__ import absolute_import | ||
Yuya Nishihara
|
r27044 | import BaseHTTPServer | ||
Yuya Nishihara
|
r27046 | import errno | ||
import mimetypes | ||||
import os | ||||
Bryan O'Sullivan
|
r5561 | |||
Dirkjan Ochtman
|
r5993 | HTTP_OK = 200 | ||
Dirkjan Ochtman
|
r12183 | HTTP_NOT_MODIFIED = 304 | ||
Dirkjan Ochtman
|
r5993 | HTTP_BAD_REQUEST = 400 | ||
Dirkjan Ochtman
|
r6926 | HTTP_UNAUTHORIZED = 401 | ||
Rocco Rutte
|
r7029 | HTTP_FORBIDDEN = 403 | ||
Dirkjan Ochtman
|
r5993 | HTTP_NOT_FOUND = 404 | ||
Dirkjan Ochtman
|
r6926 | HTTP_METHOD_NOT_ALLOWED = 405 | ||
Dirkjan Ochtman
|
r5993 | HTTP_SERVER_ERROR = 500 | ||
Sune Foldager
|
r9910 | |||
Wagner Bruna
|
r19032 | def ismember(ui, username, userlist): | ||
"""Check if username is a member of userlist. | ||||
If userlist has a single '*' member, all users are considered members. | ||||
Mads Kiilerich
|
r19951 | Can be overridden by extensions to provide more complex authorization | ||
Wagner Bruna
|
r19032 | schemes. | ||
""" | ||||
return userlist == ['*'] or username in userlist | ||||
Sune Foldager
|
r9910 | def checkauthz(hgweb, req, op): | ||
'''Check permission for operation based on request data (including | ||||
authentication info). Return if op allowed, else raise an ErrorResponse | ||||
exception.''' | ||||
user = req.env.get('REMOTE_USER') | ||||
deny_read = hgweb.configlist('web', 'deny_read') | ||||
Wagner Bruna
|
r19032 | if deny_read and (not user or ismember(hgweb.repo.ui, user, deny_read)): | ||
Sune Foldager
|
r9910 | raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized') | ||
allow_read = hgweb.configlist('web', 'allow_read') | ||||
Wagner Bruna
|
r19032 | if allow_read and (not ismember(hgweb.repo.ui, user, allow_read)): | ||
Sune Foldager
|
r9910 | raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized') | ||
if op == 'pull' and not hgweb.allowpull: | ||||
raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized') | ||||
elif op == 'pull' or op is None: # op is None for interface requests | ||||
return | ||||
# enforce that you can only push using POST requests | ||||
if req.env['REQUEST_METHOD'] != 'POST': | ||||
msg = 'push requires POST request' | ||||
raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg) | ||||
# require ssl by default for pushing, auth info cannot be sniffed | ||||
# and replayed | ||||
scheme = req.env.get('wsgi.url_scheme') | ||||
if hgweb.configbool('web', 'push_ssl', True) and scheme != 'https': | ||||
Yuya Nishihara
|
r17456 | raise ErrorResponse(HTTP_FORBIDDEN, 'ssl required') | ||
Sune Foldager
|
r9910 | |||
deny = hgweb.configlist('web', 'deny_push') | ||||
Wagner Bruna
|
r19032 | if deny and (not user or ismember(hgweb.repo.ui, user, deny)): | ||
Sune Foldager
|
r9910 | raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized') | ||
allow = hgweb.configlist('web', 'allow_push') | ||||
Wagner Bruna
|
r19032 | if not (allow and ismember(hgweb.repo.ui, user, allow)): | ||
Sune Foldager
|
r9910 | raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized') | ||
Martin Geisler
|
r14058 | # Hooks for hgweb permission checks; extensions can add hooks here. | ||
# Each hook is invoked like this: hook(hgweb, request, operation), | ||||
# where operation is either read, pull or push. Hooks should either | ||||
# raise an ErrorResponse exception, or just return. | ||||
# | ||||
# It is possible to do both authentication and authorization through | ||||
# this. | ||||
permhooks = [checkauthz] | ||||
Sune Foldager
|
r9910 | |||
Bryan O'Sullivan
|
r5561 | class ErrorResponse(Exception): | ||
Sune Foldager
|
r7741 | def __init__(self, code, message=None, headers=[]): | ||
Mads Kiilerich
|
r13444 | if message is None: | ||
message = _statusmessage(code) | ||||
timeless@mozdev.org
|
r26200 | Exception.__init__(self, message) | ||
Bryan O'Sullivan
|
r5561 | self.code = code | ||
Sune Foldager
|
r7741 | self.headers = headers | ||
Bryan O'Sullivan
|
r5563 | |||
Augie Fackler
|
r13570 | class continuereader(object): | ||
def __init__(self, f, write): | ||||
self.f = f | ||||
self._write = write | ||||
self.continued = False | ||||
def read(self, amt=-1): | ||||
if not self.continued: | ||||
self.continued = True | ||||
self._write('HTTP/1.1 100 Continue\r\n\r\n') | ||||
return self.f.read(amt) | ||||
def __getattr__(self, attr): | ||||
if attr in ('close', 'readline', 'readlines', '__iter__'): | ||||
return getattr(self.f, attr) | ||||
Brodie Rao
|
r16687 | raise AttributeError | ||
Bryan O'Sullivan
|
r5563 | |||
def _statusmessage(code): | ||||
Yuya Nishihara
|
r27044 | responses = BaseHTTPServer.BaseHTTPRequestHandler.responses | ||
Bryan O'Sullivan
|
r5563 | return responses.get(code, ('Error', 'Unknown error'))[0] | ||
Thomas Arendsen Hein
|
r5760 | |||
Sune Foldager
|
r9694 | def statusmessage(code, message=None): | ||
return '%d %s' % (code, message or _statusmessage(code)) | ||||
Eric Hopper
|
r2356 | |||
Pierre-Yves David
|
r25717 | def get_stat(spath, fn): | ||
"""stat fn if it exists, spath otherwise""" | ||||
Anton Shestakov
|
r22577 | cl_path = os.path.join(spath, fn) | ||
Benoit Boissinot
|
r3853 | if os.path.exists(cl_path): | ||
Martin Geisler
|
r13958 | return os.stat(cl_path) | ||
Eric Hopper
|
r2356 | else: | ||
Martin Geisler
|
r13958 | return os.stat(spath) | ||
def get_mtime(spath): | ||||
Pierre-Yves David
|
r25717 | return get_stat(spath, "00changelog.i").st_mtime | ||
Eric Hopper
|
r2356 | |||
Eric Hopper
|
r2514 | def staticfile(directory, fname, req): | ||
Dirkjan Ochtman
|
r5930 | """return a file inside directory with guessed Content-Type header | ||
Eric Hopper
|
r2356 | |||
fname always uses '/' as directory separator and isn't allowed to | ||||
contain unusual path components. | ||||
Dirkjan Ochtman
|
r5930 | Content-Type is guessed using the mimetypes module. | ||
Eric Hopper
|
r2356 | Return an empty string if fname is illegal or file not found. | ||
""" | ||||
parts = fname.split('/') | ||||
for part in parts: | ||||
if (part in ('', os.curdir, os.pardir) or | ||||
os.sep in part or os.altsep is not None and os.altsep in part): | ||||
Mads Kiilerich
|
r18645 | return | ||
Brendan Cully
|
r7288 | fpath = os.path.join(*parts) | ||
if isinstance(directory, str): | ||||
directory = [directory] | ||||
for d in directory: | ||||
path = os.path.join(d, fpath) | ||||
if os.path.exists(path): | ||||
break | ||||
Eric Hopper
|
r2356 | try: | ||
os.stat(path) | ||||
ct = mimetypes.guess_type(path)[0] or "text/plain" | ||||
Dan Villiom Podlaski Christiansen
|
r13400 | fp = open(path, 'rb') | ||
data = fp.read() | ||||
fp.close() | ||||
Mads Kiilerich
|
r18352 | req.respond(HTTP_OK, ct, body=data) | ||
Bryan O'Sullivan
|
r5561 | except TypeError: | ||
timeless
|
r8761 | raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename') | ||
Gregory Szorc
|
r25660 | except OSError as err: | ||
Bryan O'Sullivan
|
r5561 | if err.errno == errno.ENOENT: | ||
Dirkjan Ochtman
|
r5993 | raise ErrorResponse(HTTP_NOT_FOUND) | ||
Bryan O'Sullivan
|
r5561 | else: | ||
Dirkjan Ochtman
|
r5993 | raise ErrorResponse(HTTP_SERVER_ERROR, err.strerror) | ||
Thomas Arendsen Hein
|
r3276 | |||
Thomas Arendsen Hein
|
r4462 | def paritygen(stripecount, offset=0): | ||
"""count parity of horizontal stripes for easier reading""" | ||||
if stripecount and offset: | ||||
# account for offset, e.g. due to building the list in reverse | ||||
count = (stripecount + offset) % stripecount | ||||
parity = (stripecount + offset) / stripecount & 1 | ||||
else: | ||||
count = 0 | ||||
parity = 0 | ||||
while True: | ||||
yield parity | ||||
count += 1 | ||||
if stripecount and count >= stripecount: | ||||
parity = 1 - parity | ||||
count = 0 | ||||
Thomas Arendsen Hein
|
r5779 | def get_contact(config): | ||
"""Return repo contact information or empty string. | ||||
web.contact is the primary source, but if that is not set, try | ||||
ui.username or $EMAIL as a fallback to display something useful. | ||||
""" | ||||
return (config("web", "contact") or | ||||
config("ui", "username") or | ||||
os.environ.get("EMAIL") or "") | ||||
Dirkjan Ochtman
|
r12183 | |||
def caching(web, req): | ||||
r29491 | tag = 'W/"%s"' % web.mtime | |||
Dirkjan Ochtman
|
r12183 | if req.env.get('HTTP_IF_NONE_MATCH') == tag: | ||
raise ErrorResponse(HTTP_NOT_MODIFIED) | ||||
req.headers.append(('ETag', tag)) | ||||