##// END OF EJS Templates
hgweb: don't use mutable default argument value
Gregory Szorc -
r31390:7dafa8d0 default
parent child Browse files
Show More
@@ -1,222 +1,222 b''
1 1 # hgweb/common.py - Utility functions needed by hgweb_mod and hgwebdir_mod
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import base64
12 12 import errno
13 13 import mimetypes
14 14 import os
15 15 import uuid
16 16
17 17 from .. import (
18 18 encoding,
19 19 pycompat,
20 20 util,
21 21 )
22 22
23 23 httpserver = util.httpserver
24 24
25 25 HTTP_OK = 200
26 26 HTTP_NOT_MODIFIED = 304
27 27 HTTP_BAD_REQUEST = 400
28 28 HTTP_UNAUTHORIZED = 401
29 29 HTTP_FORBIDDEN = 403
30 30 HTTP_NOT_FOUND = 404
31 31 HTTP_METHOD_NOT_ALLOWED = 405
32 32 HTTP_SERVER_ERROR = 500
33 33
34 34
35 35 def ismember(ui, username, userlist):
36 36 """Check if username is a member of userlist.
37 37
38 38 If userlist has a single '*' member, all users are considered members.
39 39 Can be overridden by extensions to provide more complex authorization
40 40 schemes.
41 41 """
42 42 return userlist == ['*'] or username in userlist
43 43
44 44 def checkauthz(hgweb, req, op):
45 45 '''Check permission for operation based on request data (including
46 46 authentication info). Return if op allowed, else raise an ErrorResponse
47 47 exception.'''
48 48
49 49 user = req.env.get('REMOTE_USER')
50 50
51 51 deny_read = hgweb.configlist('web', 'deny_read')
52 52 if deny_read and (not user or ismember(hgweb.repo.ui, user, deny_read)):
53 53 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
54 54
55 55 allow_read = hgweb.configlist('web', 'allow_read')
56 56 if allow_read and (not ismember(hgweb.repo.ui, user, allow_read)):
57 57 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
58 58
59 59 if op == 'pull' and not hgweb.allowpull:
60 60 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
61 61 elif op == 'pull' or op is None: # op is None for interface requests
62 62 return
63 63
64 64 # enforce that you can only push using POST requests
65 65 if req.env['REQUEST_METHOD'] != 'POST':
66 66 msg = 'push requires POST request'
67 67 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
68 68
69 69 # require ssl by default for pushing, auth info cannot be sniffed
70 70 # and replayed
71 71 scheme = req.env.get('wsgi.url_scheme')
72 72 if hgweb.configbool('web', 'push_ssl', True) and scheme != 'https':
73 73 raise ErrorResponse(HTTP_FORBIDDEN, 'ssl required')
74 74
75 75 deny = hgweb.configlist('web', 'deny_push')
76 76 if deny and (not user or ismember(hgweb.repo.ui, user, deny)):
77 77 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
78 78
79 79 allow = hgweb.configlist('web', 'allow_push')
80 80 if not (allow and ismember(hgweb.repo.ui, user, allow)):
81 81 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
82 82
83 83 # Hooks for hgweb permission checks; extensions can add hooks here.
84 84 # Each hook is invoked like this: hook(hgweb, request, operation),
85 85 # where operation is either read, pull or push. Hooks should either
86 86 # raise an ErrorResponse exception, or just return.
87 87 #
88 88 # It is possible to do both authentication and authorization through
89 89 # this.
90 90 permhooks = [checkauthz]
91 91
92 92
93 93 class ErrorResponse(Exception):
94 def __init__(self, code, message=None, headers=[]):
94 def __init__(self, code, message=None, headers=None):
95 95 if message is None:
96 96 message = _statusmessage(code)
97 97 Exception.__init__(self, message)
98 98 self.code = code
99 self.headers = headers
99 self.headers = headers or []
100 100
101 101 class continuereader(object):
102 102 def __init__(self, f, write):
103 103 self.f = f
104 104 self._write = write
105 105 self.continued = False
106 106
107 107 def read(self, amt=-1):
108 108 if not self.continued:
109 109 self.continued = True
110 110 self._write('HTTP/1.1 100 Continue\r\n\r\n')
111 111 return self.f.read(amt)
112 112
113 113 def __getattr__(self, attr):
114 114 if attr in ('close', 'readline', 'readlines', '__iter__'):
115 115 return getattr(self.f, attr)
116 116 raise AttributeError
117 117
118 118 def _statusmessage(code):
119 119 responses = httpserver.basehttprequesthandler.responses
120 120 return responses.get(code, ('Error', 'Unknown error'))[0]
121 121
122 122 def statusmessage(code, message=None):
123 123 return '%d %s' % (code, message or _statusmessage(code))
124 124
125 125 def get_stat(spath, fn):
126 126 """stat fn if it exists, spath otherwise"""
127 127 cl_path = os.path.join(spath, fn)
128 128 if os.path.exists(cl_path):
129 129 return os.stat(cl_path)
130 130 else:
131 131 return os.stat(spath)
132 132
133 133 def get_mtime(spath):
134 134 return get_stat(spath, "00changelog.i").st_mtime
135 135
136 136 def staticfile(directory, fname, req):
137 137 """return a file inside directory with guessed Content-Type header
138 138
139 139 fname always uses '/' as directory separator and isn't allowed to
140 140 contain unusual path components.
141 141 Content-Type is guessed using the mimetypes module.
142 142 Return an empty string if fname is illegal or file not found.
143 143
144 144 """
145 145 parts = fname.split('/')
146 146 for part in parts:
147 147 if (part in ('', os.curdir, os.pardir) or
148 148 pycompat.ossep in part or
149 149 pycompat.osaltsep is not None and pycompat.osaltsep in part):
150 150 return
151 151 fpath = os.path.join(*parts)
152 152 if isinstance(directory, str):
153 153 directory = [directory]
154 154 for d in directory:
155 155 path = os.path.join(d, fpath)
156 156 if os.path.exists(path):
157 157 break
158 158 try:
159 159 os.stat(path)
160 160 ct = mimetypes.guess_type(path)[0] or "text/plain"
161 161 fp = open(path, 'rb')
162 162 data = fp.read()
163 163 fp.close()
164 164 req.respond(HTTP_OK, ct, body=data)
165 165 except TypeError:
166 166 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
167 167 except OSError as err:
168 168 if err.errno == errno.ENOENT:
169 169 raise ErrorResponse(HTTP_NOT_FOUND)
170 170 else:
171 171 raise ErrorResponse(HTTP_SERVER_ERROR, err.strerror)
172 172
173 173 def paritygen(stripecount, offset=0):
174 174 """count parity of horizontal stripes for easier reading"""
175 175 if stripecount and offset:
176 176 # account for offset, e.g. due to building the list in reverse
177 177 count = (stripecount + offset) % stripecount
178 178 parity = (stripecount + offset) / stripecount & 1
179 179 else:
180 180 count = 0
181 181 parity = 0
182 182 while True:
183 183 yield parity
184 184 count += 1
185 185 if stripecount and count >= stripecount:
186 186 parity = 1 - parity
187 187 count = 0
188 188
189 189 def get_contact(config):
190 190 """Return repo contact information or empty string.
191 191
192 192 web.contact is the primary source, but if that is not set, try
193 193 ui.username or $EMAIL as a fallback to display something useful.
194 194 """
195 195 return (config("web", "contact") or
196 196 config("ui", "username") or
197 197 encoding.environ.get("EMAIL") or "")
198 198
199 199 def caching(web, req):
200 200 tag = 'W/"%s"' % web.mtime
201 201 if req.env.get('HTTP_IF_NONE_MATCH') == tag:
202 202 raise ErrorResponse(HTTP_NOT_MODIFIED)
203 203 req.headers.append(('ETag', tag))
204 204
205 205 def cspvalues(ui):
206 206 """Obtain the Content-Security-Policy header and nonce value.
207 207
208 208 Returns a 2-tuple of the CSP header value and the nonce value.
209 209
210 210 First value is ``None`` if CSP isn't enabled. Second value is ``None``
211 211 if CSP isn't enabled or if the CSP header doesn't need a nonce.
212 212 """
213 213 # Don't allow untrusted CSP setting since it be disable protections
214 214 # from a trusted/global source.
215 215 csp = ui.config('web', 'csp', untrusted=False)
216 216 nonce = None
217 217
218 218 if csp and '%nonce%' in csp:
219 219 nonce = base64.urlsafe_b64encode(uuid.uuid4().bytes).rstrip('=')
220 220 csp = csp.replace('%nonce%', nonce)
221 221
222 222 return csp, nonce
General Comments 0
You need to be logged in to leave comments. Login now