##// END OF EJS Templates
hgweb: add a `message` attribute to `hgweb.common.ErrorResponse`...
Connor Sheehan -
r43192:8d9322b6 default
parent child Browse files
Show More
@@ -1,262 +1,263 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 stat
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_CREATED = 201
27 27 HTTP_NOT_MODIFIED = 304
28 28 HTTP_BAD_REQUEST = 400
29 29 HTTP_UNAUTHORIZED = 401
30 30 HTTP_FORBIDDEN = 403
31 31 HTTP_NOT_FOUND = 404
32 32 HTTP_METHOD_NOT_ALLOWED = 405
33 33 HTTP_NOT_ACCEPTABLE = 406
34 34 HTTP_UNSUPPORTED_MEDIA_TYPE = 415
35 35 HTTP_SERVER_ERROR = 500
36 36
37 37
38 38 def ismember(ui, username, userlist):
39 39 """Check if username is a member of userlist.
40 40
41 41 If userlist has a single '*' member, all users are considered members.
42 42 Can be overridden by extensions to provide more complex authorization
43 43 schemes.
44 44 """
45 45 return userlist == ['*'] or username in userlist
46 46
47 47 def checkauthz(hgweb, req, op):
48 48 '''Check permission for operation based on request data (including
49 49 authentication info). Return if op allowed, else raise an ErrorResponse
50 50 exception.'''
51 51
52 52 user = req.remoteuser
53 53
54 54 deny_read = hgweb.configlist('web', 'deny_read')
55 55 if deny_read and (not user or ismember(hgweb.repo.ui, user, deny_read)):
56 56 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
57 57
58 58 allow_read = hgweb.configlist('web', 'allow_read')
59 59 if allow_read and (not ismember(hgweb.repo.ui, user, allow_read)):
60 60 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
61 61
62 62 if op == 'pull' and not hgweb.allowpull:
63 63 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
64 64 elif op == 'pull' or op is None: # op is None for interface requests
65 65 return
66 66
67 67 # Allow LFS uploading via PUT requests
68 68 if op == 'upload':
69 69 if req.method != 'PUT':
70 70 msg = 'upload requires PUT request'
71 71 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
72 72 # enforce that you can only push using POST requests
73 73 elif req.method != 'POST':
74 74 msg = 'push requires POST request'
75 75 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
76 76
77 77 # require ssl by default for pushing, auth info cannot be sniffed
78 78 # and replayed
79 79 if hgweb.configbool('web', 'push_ssl') and req.urlscheme != 'https':
80 80 raise ErrorResponse(HTTP_FORBIDDEN, 'ssl required')
81 81
82 82 deny = hgweb.configlist('web', 'deny_push')
83 83 if deny and (not user or ismember(hgweb.repo.ui, user, deny)):
84 84 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
85 85
86 86 allow = hgweb.configlist('web', 'allow-push')
87 87 if not (allow and ismember(hgweb.repo.ui, user, allow)):
88 88 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
89 89
90 90 # Hooks for hgweb permission checks; extensions can add hooks here.
91 91 # Each hook is invoked like this: hook(hgweb, request, operation),
92 92 # where operation is either read, pull, push or upload. Hooks should either
93 93 # raise an ErrorResponse exception, or just return.
94 94 #
95 95 # It is possible to do both authentication and authorization through
96 96 # this.
97 97 permhooks = [checkauthz]
98 98
99 99
100 100 class ErrorResponse(Exception):
101 101 def __init__(self, code, message=None, headers=None):
102 102 if message is None:
103 103 message = _statusmessage(code)
104 104 Exception.__init__(self, pycompat.sysstr(message))
105 105 self.code = code
106 106 if headers is None:
107 107 headers = []
108 108 self.headers = headers
109 self.message = message
109 110
110 111 class continuereader(object):
111 112 """File object wrapper to handle HTTP 100-continue.
112 113
113 114 This is used by servers so they automatically handle Expect: 100-continue
114 115 request headers. On first read of the request body, the 100 Continue
115 116 response is sent. This should trigger the client into actually sending
116 117 the request body.
117 118 """
118 119 def __init__(self, f, write):
119 120 self.f = f
120 121 self._write = write
121 122 self.continued = False
122 123
123 124 def read(self, amt=-1):
124 125 if not self.continued:
125 126 self.continued = True
126 127 self._write('HTTP/1.1 100 Continue\r\n\r\n')
127 128 return self.f.read(amt)
128 129
129 130 def __getattr__(self, attr):
130 131 if attr in ('close', 'readline', 'readlines', '__iter__'):
131 132 return getattr(self.f, attr)
132 133 raise AttributeError
133 134
134 135 def _statusmessage(code):
135 136 responses = httpserver.basehttprequesthandler.responses
136 137 return pycompat.bytesurl(
137 138 responses.get(code, (r'Error', r'Unknown error'))[0])
138 139
139 140 def statusmessage(code, message=None):
140 141 return '%d %s' % (code, message or _statusmessage(code))
141 142
142 143 def get_stat(spath, fn):
143 144 """stat fn if it exists, spath otherwise"""
144 145 cl_path = os.path.join(spath, fn)
145 146 if os.path.exists(cl_path):
146 147 return os.stat(cl_path)
147 148 else:
148 149 return os.stat(spath)
149 150
150 151 def get_mtime(spath):
151 152 return get_stat(spath, "00changelog.i")[stat.ST_MTIME]
152 153
153 154 def ispathsafe(path):
154 155 """Determine if a path is safe to use for filesystem access."""
155 156 parts = path.split('/')
156 157 for part in parts:
157 158 if (part in ('', pycompat.oscurdir, pycompat.ospardir) or
158 159 pycompat.ossep in part or
159 160 pycompat.osaltsep is not None and pycompat.osaltsep in part):
160 161 return False
161 162
162 163 return True
163 164
164 165 def staticfile(directory, fname, res):
165 166 """return a file inside directory with guessed Content-Type header
166 167
167 168 fname always uses '/' as directory separator and isn't allowed to
168 169 contain unusual path components.
169 170 Content-Type is guessed using the mimetypes module.
170 171 Return an empty string if fname is illegal or file not found.
171 172
172 173 """
173 174 if not ispathsafe(fname):
174 175 return
175 176
176 177 fpath = os.path.join(*fname.split('/'))
177 178 if isinstance(directory, str):
178 179 directory = [directory]
179 180 for d in directory:
180 181 path = os.path.join(d, fpath)
181 182 if os.path.exists(path):
182 183 break
183 184 try:
184 185 os.stat(path)
185 186 ct = pycompat.sysbytes(
186 187 mimetypes.guess_type(pycompat.fsdecode(path))[0] or r"text/plain")
187 188 with open(path, 'rb') as fh:
188 189 data = fh.read()
189 190
190 191 res.headers['Content-Type'] = ct
191 192 res.setbodybytes(data)
192 193 return res
193 194 except TypeError:
194 195 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
195 196 except OSError as err:
196 197 if err.errno == errno.ENOENT:
197 198 raise ErrorResponse(HTTP_NOT_FOUND)
198 199 else:
199 200 raise ErrorResponse(HTTP_SERVER_ERROR,
200 201 encoding.strtolocal(err.strerror))
201 202
202 203 def paritygen(stripecount, offset=0):
203 204 """count parity of horizontal stripes for easier reading"""
204 205 if stripecount and offset:
205 206 # account for offset, e.g. due to building the list in reverse
206 207 count = (stripecount + offset) % stripecount
207 208 parity = (stripecount + offset) // stripecount & 1
208 209 else:
209 210 count = 0
210 211 parity = 0
211 212 while True:
212 213 yield parity
213 214 count += 1
214 215 if stripecount and count >= stripecount:
215 216 parity = 1 - parity
216 217 count = 0
217 218
218 219 def get_contact(config):
219 220 """Return repo contact information or empty string.
220 221
221 222 web.contact is the primary source, but if that is not set, try
222 223 ui.username or $EMAIL as a fallback to display something useful.
223 224 """
224 225 return (config("web", "contact") or
225 226 config("ui", "username") or
226 227 encoding.environ.get("EMAIL") or "")
227 228
228 229 def cspvalues(ui):
229 230 """Obtain the Content-Security-Policy header and nonce value.
230 231
231 232 Returns a 2-tuple of the CSP header value and the nonce value.
232 233
233 234 First value is ``None`` if CSP isn't enabled. Second value is ``None``
234 235 if CSP isn't enabled or if the CSP header doesn't need a nonce.
235 236 """
236 237 # Without demandimport, "import uuid" could have an immediate side-effect
237 238 # running "ldconfig" on Linux trying to find libuuid.
238 239 # With Python <= 2.7.12, that "ldconfig" is run via a shell and the shell
239 240 # may pollute the terminal with:
240 241 #
241 242 # shell-init: error retrieving current directory: getcwd: cannot access
242 243 # parent directories: No such file or directory
243 244 #
244 245 # Python >= 2.7.13 has fixed it by running "ldconfig" directly without a
245 246 # shell (hg changeset a09ae70f3489).
246 247 #
247 248 # Moved "import uuid" from here so it's executed after we know we have
248 249 # a sane cwd (i.e. after dispatch.py cwd check).
249 250 #
250 251 # We can move it back once we no longer need Python <= 2.7.12 support.
251 252 import uuid
252 253
253 254 # Don't allow untrusted CSP setting since it be disable protections
254 255 # from a trusted/global source.
255 256 csp = ui.config('web', 'csp', untrusted=False)
256 257 nonce = None
257 258
258 259 if csp and '%nonce%' in csp:
259 260 nonce = base64.urlsafe_b64encode(uuid.uuid4().bytes).rstrip('=')
260 261 csp = csp.replace('%nonce%', nonce)
261 262
262 263 return csp, nonce
General Comments 0
You need to be logged in to leave comments. Login now