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