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