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