##// END OF EJS Templates
hgweb: these strings should be sysstrs, not bytes...
Augie Fackler -
r37752:a1110db1 default
parent child Browse files
Show More
@@ -1,260 +1,261 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 109
110 110 class continuereader(object):
111 111 """File object wrapper to handle HTTP 100-continue.
112 112
113 113 This is used by servers so they automatically handle Expect: 100-continue
114 114 request headers. On first read of the request body, the 100 Continue
115 115 response is sent. This should trigger the client into actually sending
116 116 the request body.
117 117 """
118 118 def __init__(self, f, write):
119 119 self.f = f
120 120 self._write = write
121 121 self.continued = False
122 122
123 123 def read(self, amt=-1):
124 124 if not self.continued:
125 125 self.continued = True
126 126 self._write('HTTP/1.1 100 Continue\r\n\r\n')
127 127 return self.f.read(amt)
128 128
129 129 def __getattr__(self, attr):
130 130 if attr in ('close', 'readline', 'readlines', '__iter__'):
131 131 return getattr(self.f, attr)
132 132 raise AttributeError
133 133
134 134 def _statusmessage(code):
135 135 responses = httpserver.basehttprequesthandler.responses
136 return responses.get(code, ('Error', 'Unknown error'))[0]
136 return pycompat.bytesurl(
137 responses.get(code, (r'Error', r'Unknown error'))[0])
137 138
138 139 def statusmessage(code, message=None):
139 140 return '%d %s' % (code, message or _statusmessage(code))
140 141
141 142 def get_stat(spath, fn):
142 143 """stat fn if it exists, spath otherwise"""
143 144 cl_path = os.path.join(spath, fn)
144 145 if os.path.exists(cl_path):
145 146 return os.stat(cl_path)
146 147 else:
147 148 return os.stat(spath)
148 149
149 150 def get_mtime(spath):
150 151 return get_stat(spath, "00changelog.i")[stat.ST_MTIME]
151 152
152 153 def ispathsafe(path):
153 154 """Determine if a path is safe to use for filesystem access."""
154 155 parts = path.split('/')
155 156 for part in parts:
156 157 if (part in ('', pycompat.oscurdir, pycompat.ospardir) or
157 158 pycompat.ossep in part or
158 159 pycompat.osaltsep is not None and pycompat.osaltsep in part):
159 160 return False
160 161
161 162 return True
162 163
163 164 def staticfile(directory, fname, res):
164 165 """return a file inside directory with guessed Content-Type header
165 166
166 167 fname always uses '/' as directory separator and isn't allowed to
167 168 contain unusual path components.
168 169 Content-Type is guessed using the mimetypes module.
169 170 Return an empty string if fname is illegal or file not found.
170 171
171 172 """
172 173 if not ispathsafe(fname):
173 174 return
174 175
175 176 fpath = os.path.join(*fname.split('/'))
176 177 if isinstance(directory, str):
177 178 directory = [directory]
178 179 for d in directory:
179 180 path = os.path.join(d, fpath)
180 181 if os.path.exists(path):
181 182 break
182 183 try:
183 184 os.stat(path)
184 185 ct = mimetypes.guess_type(pycompat.fsdecode(path))[0] or "text/plain"
185 186 with open(path, 'rb') as fh:
186 187 data = fh.read()
187 188
188 189 res.headers['Content-Type'] = ct
189 190 res.setbodybytes(data)
190 191 return res
191 192 except TypeError:
192 193 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
193 194 except OSError as err:
194 195 if err.errno == errno.ENOENT:
195 196 raise ErrorResponse(HTTP_NOT_FOUND)
196 197 else:
197 198 raise ErrorResponse(HTTP_SERVER_ERROR,
198 199 encoding.strtolocal(err.strerror))
199 200
200 201 def paritygen(stripecount, offset=0):
201 202 """count parity of horizontal stripes for easier reading"""
202 203 if stripecount and offset:
203 204 # account for offset, e.g. due to building the list in reverse
204 205 count = (stripecount + offset) % stripecount
205 206 parity = (stripecount + offset) // stripecount & 1
206 207 else:
207 208 count = 0
208 209 parity = 0
209 210 while True:
210 211 yield parity
211 212 count += 1
212 213 if stripecount and count >= stripecount:
213 214 parity = 1 - parity
214 215 count = 0
215 216
216 217 def get_contact(config):
217 218 """Return repo contact information or empty string.
218 219
219 220 web.contact is the primary source, but if that is not set, try
220 221 ui.username or $EMAIL as a fallback to display something useful.
221 222 """
222 223 return (config("web", "contact") or
223 224 config("ui", "username") or
224 225 encoding.environ.get("EMAIL") or "")
225 226
226 227 def cspvalues(ui):
227 228 """Obtain the Content-Security-Policy header and nonce value.
228 229
229 230 Returns a 2-tuple of the CSP header value and the nonce value.
230 231
231 232 First value is ``None`` if CSP isn't enabled. Second value is ``None``
232 233 if CSP isn't enabled or if the CSP header doesn't need a nonce.
233 234 """
234 235 # Without demandimport, "import uuid" could have an immediate side-effect
235 236 # running "ldconfig" on Linux trying to find libuuid.
236 237 # With Python <= 2.7.12, that "ldconfig" is run via a shell and the shell
237 238 # may pollute the terminal with:
238 239 #
239 240 # shell-init: error retrieving current directory: getcwd: cannot access
240 241 # parent directories: No such file or directory
241 242 #
242 243 # Python >= 2.7.13 has fixed it by running "ldconfig" directly without a
243 244 # shell (hg changeset a09ae70f3489).
244 245 #
245 246 # Moved "import uuid" from here so it's executed after we know we have
246 247 # a sane cwd (i.e. after dispatch.py cwd check).
247 248 #
248 249 # We can move it back once we no longer need Python <= 2.7.12 support.
249 250 import uuid
250 251
251 252 # Don't allow untrusted CSP setting since it be disable protections
252 253 # from a trusted/global source.
253 254 csp = ui.config('web', 'csp', untrusted=False)
254 255 nonce = None
255 256
256 257 if csp and '%nonce%' in csp:
257 258 nonce = base64.urlsafe_b64encode(uuid.uuid4().bytes).rstrip('=')
258 259 csp = csp.replace('%nonce%', nonce)
259 260
260 261 return csp, nonce
General Comments 0
You need to be logged in to leave comments. Login now