|
1 # hgweb/common.py - Utility functions needed by hgweb_mod and hgwebdir_mod |
|
2 # |
|
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net> |
|
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com> |
|
5 # |
|
6 # This software may be used and distributed according to the terms of the |
|
7 # GNU General Public License version 2 or any later version. |
|
8 |
|
9 import errno, mimetypes, os |
|
10 |
|
11 HTTP_OK = 200 |
|
12 HTTP_NOT_MODIFIED = 304 |
|
13 HTTP_BAD_REQUEST = 400 |
|
14 HTTP_UNAUTHORIZED = 401 |
|
15 HTTP_FORBIDDEN = 403 |
|
16 HTTP_NOT_FOUND = 404 |
|
17 HTTP_METHOD_NOT_ALLOWED = 405 |
|
18 HTTP_SERVER_ERROR = 500 |
|
19 |
|
20 # Hooks for hgweb permission checks; extensions can add hooks here. Each hook |
|
21 # is invoked like this: hook(hgweb, request, operation), where operation is |
|
22 # either read, pull or push. Hooks should either raise an ErrorResponse |
|
23 # exception, or just return. |
|
24 # It is possible to do both authentication and authorization through this. |
|
25 permhooks = [] |
|
26 |
|
27 def checkauthz(hgweb, req, op): |
|
28 '''Check permission for operation based on request data (including |
|
29 authentication info). Return if op allowed, else raise an ErrorResponse |
|
30 exception.''' |
|
31 |
|
32 user = req.env.get('REMOTE_USER') |
|
33 |
|
34 deny_read = hgweb.configlist('web', 'deny_read') |
|
35 if deny_read and (not user or deny_read == ['*'] or user in deny_read): |
|
36 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized') |
|
37 |
|
38 allow_read = hgweb.configlist('web', 'allow_read') |
|
39 result = (not allow_read) or (allow_read == ['*']) |
|
40 if not (result or user in allow_read): |
|
41 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized') |
|
42 |
|
43 if op == 'pull' and not hgweb.allowpull: |
|
44 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized') |
|
45 elif op == 'pull' or op is None: # op is None for interface requests |
|
46 return |
|
47 |
|
48 # enforce that you can only push using POST requests |
|
49 if req.env['REQUEST_METHOD'] != 'POST': |
|
50 msg = 'push requires POST request' |
|
51 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg) |
|
52 |
|
53 # require ssl by default for pushing, auth info cannot be sniffed |
|
54 # and replayed |
|
55 scheme = req.env.get('wsgi.url_scheme') |
|
56 if hgweb.configbool('web', 'push_ssl', True) and scheme != 'https': |
|
57 raise ErrorResponse(HTTP_OK, 'ssl required') |
|
58 |
|
59 deny = hgweb.configlist('web', 'deny_push') |
|
60 if deny and (not user or deny == ['*'] or user in deny): |
|
61 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized') |
|
62 |
|
63 allow = hgweb.configlist('web', 'allow_push') |
|
64 result = allow and (allow == ['*'] or user in allow) |
|
65 if not result: |
|
66 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized') |
|
67 |
|
68 # Add the default permhook, which provides simple authorization. |
|
69 permhooks.append(checkauthz) |
|
70 |
|
71 |
|
72 class ErrorResponse(Exception): |
|
73 def __init__(self, code, message=None, headers=[]): |
|
74 Exception.__init__(self) |
|
75 self.code = code |
|
76 self.headers = headers |
|
77 if message is not None: |
|
78 self.message = message |
|
79 else: |
|
80 self.message = _statusmessage(code) |
|
81 |
|
82 def _statusmessage(code): |
|
83 from BaseHTTPServer import BaseHTTPRequestHandler |
|
84 responses = BaseHTTPRequestHandler.responses |
|
85 return responses.get(code, ('Error', 'Unknown error'))[0] |
|
86 |
|
87 def statusmessage(code, message=None): |
|
88 return '%d %s' % (code, message or _statusmessage(code)) |
|
89 |
|
90 def get_mtime(spath): |
|
91 cl_path = os.path.join(spath, "00changelog.i") |
|
92 if os.path.exists(cl_path): |
|
93 return os.stat(cl_path).st_mtime |
|
94 else: |
|
95 return os.stat(spath).st_mtime |
|
96 |
|
97 def staticfile(directory, fname, req): |
|
98 """return a file inside directory with guessed Content-Type header |
|
99 |
|
100 fname always uses '/' as directory separator and isn't allowed to |
|
101 contain unusual path components. |
|
102 Content-Type is guessed using the mimetypes module. |
|
103 Return an empty string if fname is illegal or file not found. |
|
104 |
|
105 """ |
|
106 parts = fname.split('/') |
|
107 for part in parts: |
|
108 if (part in ('', os.curdir, os.pardir) or |
|
109 os.sep in part or os.altsep is not None and os.altsep in part): |
|
110 return "" |
|
111 fpath = os.path.join(*parts) |
|
112 if isinstance(directory, str): |
|
113 directory = [directory] |
|
114 for d in directory: |
|
115 path = os.path.join(d, fpath) |
|
116 if os.path.exists(path): |
|
117 break |
|
118 try: |
|
119 os.stat(path) |
|
120 ct = mimetypes.guess_type(path)[0] or "text/plain" |
|
121 req.respond(HTTP_OK, ct, length = os.path.getsize(path)) |
|
122 return open(path, 'rb').read() |
|
123 except TypeError: |
|
124 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename') |
|
125 except OSError, err: |
|
126 if err.errno == errno.ENOENT: |
|
127 raise ErrorResponse(HTTP_NOT_FOUND) |
|
128 else: |
|
129 raise ErrorResponse(HTTP_SERVER_ERROR, err.strerror) |
|
130 |
|
131 def paritygen(stripecount, offset=0): |
|
132 """count parity of horizontal stripes for easier reading""" |
|
133 if stripecount and offset: |
|
134 # account for offset, e.g. due to building the list in reverse |
|
135 count = (stripecount + offset) % stripecount |
|
136 parity = (stripecount + offset) / stripecount & 1 |
|
137 else: |
|
138 count = 0 |
|
139 parity = 0 |
|
140 while True: |
|
141 yield parity |
|
142 count += 1 |
|
143 if stripecount and count >= stripecount: |
|
144 parity = 1 - parity |
|
145 count = 0 |
|
146 |
|
147 def get_contact(config): |
|
148 """Return repo contact information or empty string. |
|
149 |
|
150 web.contact is the primary source, but if that is not set, try |
|
151 ui.username or $EMAIL as a fallback to display something useful. |
|
152 """ |
|
153 return (config("web", "contact") or |
|
154 config("ui", "username") or |
|
155 os.environ.get("EMAIL") or "") |
|
156 |
|
157 def caching(web, req): |
|
158 tag = str(web.mtime) |
|
159 if req.env.get('HTTP_IF_NONE_MATCH') == tag: |
|
160 raise ErrorResponse(HTTP_NOT_MODIFIED) |
|
161 req.headers.append(('ETag', tag)) |