##// END OF EJS Templates
hgweb: do not import uuid immediately to avoid its side effect...
Jun Wu -
r34644:f42dec9c default
parent child Browse files
Show More
@@ -1,233 +1,249 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
16
15
17 from .. import (
16 from .. import (
18 encoding,
17 encoding,
19 pycompat,
18 pycompat,
20 util,
19 util,
21 )
20 )
22
21
23 httpserver = util.httpserver
22 httpserver = util.httpserver
24
23
25 HTTP_OK = 200
24 HTTP_OK = 200
26 HTTP_NOT_MODIFIED = 304
25 HTTP_NOT_MODIFIED = 304
27 HTTP_BAD_REQUEST = 400
26 HTTP_BAD_REQUEST = 400
28 HTTP_UNAUTHORIZED = 401
27 HTTP_UNAUTHORIZED = 401
29 HTTP_FORBIDDEN = 403
28 HTTP_FORBIDDEN = 403
30 HTTP_NOT_FOUND = 404
29 HTTP_NOT_FOUND = 404
31 HTTP_METHOD_NOT_ALLOWED = 405
30 HTTP_METHOD_NOT_ALLOWED = 405
32 HTTP_SERVER_ERROR = 500
31 HTTP_SERVER_ERROR = 500
33
32
34
33
35 def ismember(ui, username, userlist):
34 def ismember(ui, username, userlist):
36 """Check if username is a member of userlist.
35 """Check if username is a member of userlist.
37
36
38 If userlist has a single '*' member, all users are considered members.
37 If userlist has a single '*' member, all users are considered members.
39 Can be overridden by extensions to provide more complex authorization
38 Can be overridden by extensions to provide more complex authorization
40 schemes.
39 schemes.
41 """
40 """
42 return userlist == ['*'] or username in userlist
41 return userlist == ['*'] or username in userlist
43
42
44 def checkauthz(hgweb, req, op):
43 def checkauthz(hgweb, req, op):
45 '''Check permission for operation based on request data (including
44 '''Check permission for operation based on request data (including
46 authentication info). Return if op allowed, else raise an ErrorResponse
45 authentication info). Return if op allowed, else raise an ErrorResponse
47 exception.'''
46 exception.'''
48
47
49 user = req.env.get('REMOTE_USER')
48 user = req.env.get('REMOTE_USER')
50
49
51 deny_read = hgweb.configlist('web', 'deny_read')
50 deny_read = hgweb.configlist('web', 'deny_read')
52 if deny_read and (not user or ismember(hgweb.repo.ui, user, deny_read)):
51 if deny_read and (not user or ismember(hgweb.repo.ui, user, deny_read)):
53 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
52 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
54
53
55 allow_read = hgweb.configlist('web', 'allow_read')
54 allow_read = hgweb.configlist('web', 'allow_read')
56 if allow_read and (not ismember(hgweb.repo.ui, user, allow_read)):
55 if allow_read and (not ismember(hgweb.repo.ui, user, allow_read)):
57 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
56 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
58
57
59 if op == 'pull' and not hgweb.allowpull:
58 if op == 'pull' and not hgweb.allowpull:
60 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
59 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
61 elif op == 'pull' or op is None: # op is None for interface requests
60 elif op == 'pull' or op is None: # op is None for interface requests
62 return
61 return
63
62
64 # enforce that you can only push using POST requests
63 # enforce that you can only push using POST requests
65 if req.env['REQUEST_METHOD'] != 'POST':
64 if req.env['REQUEST_METHOD'] != 'POST':
66 msg = 'push requires POST request'
65 msg = 'push requires POST request'
67 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
66 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
68
67
69 # require ssl by default for pushing, auth info cannot be sniffed
68 # require ssl by default for pushing, auth info cannot be sniffed
70 # and replayed
69 # and replayed
71 scheme = req.env.get('wsgi.url_scheme')
70 scheme = req.env.get('wsgi.url_scheme')
72 if hgweb.configbool('web', 'push_ssl') and scheme != 'https':
71 if hgweb.configbool('web', 'push_ssl') and scheme != 'https':
73 raise ErrorResponse(HTTP_FORBIDDEN, 'ssl required')
72 raise ErrorResponse(HTTP_FORBIDDEN, 'ssl required')
74
73
75 deny = hgweb.configlist('web', 'deny_push')
74 deny = hgweb.configlist('web', 'deny_push')
76 if deny and (not user or ismember(hgweb.repo.ui, user, deny)):
75 if deny and (not user or ismember(hgweb.repo.ui, user, deny)):
77 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
76 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
78
77
79 allow = hgweb.configlist('web', 'allow_push')
78 allow = hgweb.configlist('web', 'allow_push')
80 if not (allow and ismember(hgweb.repo.ui, user, allow)):
79 if not (allow and ismember(hgweb.repo.ui, user, allow)):
81 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
80 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
82
81
83 # Hooks for hgweb permission checks; extensions can add hooks here.
82 # Hooks for hgweb permission checks; extensions can add hooks here.
84 # Each hook is invoked like this: hook(hgweb, request, operation),
83 # Each hook is invoked like this: hook(hgweb, request, operation),
85 # where operation is either read, pull or push. Hooks should either
84 # where operation is either read, pull or push. Hooks should either
86 # raise an ErrorResponse exception, or just return.
85 # raise an ErrorResponse exception, or just return.
87 #
86 #
88 # It is possible to do both authentication and authorization through
87 # It is possible to do both authentication and authorization through
89 # this.
88 # this.
90 permhooks = [checkauthz]
89 permhooks = [checkauthz]
91
90
92
91
93 class ErrorResponse(Exception):
92 class ErrorResponse(Exception):
94 def __init__(self, code, message=None, headers=None):
93 def __init__(self, code, message=None, headers=None):
95 if message is None:
94 if message is None:
96 message = _statusmessage(code)
95 message = _statusmessage(code)
97 Exception.__init__(self, message)
96 Exception.__init__(self, message)
98 self.code = code
97 self.code = code
99 if headers is None:
98 if headers is None:
100 headers = []
99 headers = []
101 self.headers = headers
100 self.headers = headers
102
101
103 class continuereader(object):
102 class continuereader(object):
104 def __init__(self, f, write):
103 def __init__(self, f, write):
105 self.f = f
104 self.f = f
106 self._write = write
105 self._write = write
107 self.continued = False
106 self.continued = False
108
107
109 def read(self, amt=-1):
108 def read(self, amt=-1):
110 if not self.continued:
109 if not self.continued:
111 self.continued = True
110 self.continued = True
112 self._write('HTTP/1.1 100 Continue\r\n\r\n')
111 self._write('HTTP/1.1 100 Continue\r\n\r\n')
113 return self.f.read(amt)
112 return self.f.read(amt)
114
113
115 def __getattr__(self, attr):
114 def __getattr__(self, attr):
116 if attr in ('close', 'readline', 'readlines', '__iter__'):
115 if attr in ('close', 'readline', 'readlines', '__iter__'):
117 return getattr(self.f, attr)
116 return getattr(self.f, attr)
118 raise AttributeError
117 raise AttributeError
119
118
120 def _statusmessage(code):
119 def _statusmessage(code):
121 responses = httpserver.basehttprequesthandler.responses
120 responses = httpserver.basehttprequesthandler.responses
122 return responses.get(code, ('Error', 'Unknown error'))[0]
121 return responses.get(code, ('Error', 'Unknown error'))[0]
123
122
124 def statusmessage(code, message=None):
123 def statusmessage(code, message=None):
125 return '%d %s' % (code, message or _statusmessage(code))
124 return '%d %s' % (code, message or _statusmessage(code))
126
125
127 def get_stat(spath, fn):
126 def get_stat(spath, fn):
128 """stat fn if it exists, spath otherwise"""
127 """stat fn if it exists, spath otherwise"""
129 cl_path = os.path.join(spath, fn)
128 cl_path = os.path.join(spath, fn)
130 if os.path.exists(cl_path):
129 if os.path.exists(cl_path):
131 return os.stat(cl_path)
130 return os.stat(cl_path)
132 else:
131 else:
133 return os.stat(spath)
132 return os.stat(spath)
134
133
135 def get_mtime(spath):
134 def get_mtime(spath):
136 return get_stat(spath, "00changelog.i").st_mtime
135 return get_stat(spath, "00changelog.i").st_mtime
137
136
138 def ispathsafe(path):
137 def ispathsafe(path):
139 """Determine if a path is safe to use for filesystem access."""
138 """Determine if a path is safe to use for filesystem access."""
140 parts = path.split('/')
139 parts = path.split('/')
141 for part in parts:
140 for part in parts:
142 if (part in ('', os.curdir, os.pardir) or
141 if (part in ('', os.curdir, os.pardir) or
143 pycompat.ossep in part or
142 pycompat.ossep in part or
144 pycompat.osaltsep is not None and pycompat.osaltsep in part):
143 pycompat.osaltsep is not None and pycompat.osaltsep in part):
145 return False
144 return False
146
145
147 return True
146 return True
148
147
149 def staticfile(directory, fname, req):
148 def staticfile(directory, fname, req):
150 """return a file inside directory with guessed Content-Type header
149 """return a file inside directory with guessed Content-Type header
151
150
152 fname always uses '/' as directory separator and isn't allowed to
151 fname always uses '/' as directory separator and isn't allowed to
153 contain unusual path components.
152 contain unusual path components.
154 Content-Type is guessed using the mimetypes module.
153 Content-Type is guessed using the mimetypes module.
155 Return an empty string if fname is illegal or file not found.
154 Return an empty string if fname is illegal or file not found.
156
155
157 """
156 """
158 if not ispathsafe(fname):
157 if not ispathsafe(fname):
159 return
158 return
160
159
161 fpath = os.path.join(*fname.split('/'))
160 fpath = os.path.join(*fname.split('/'))
162 if isinstance(directory, str):
161 if isinstance(directory, str):
163 directory = [directory]
162 directory = [directory]
164 for d in directory:
163 for d in directory:
165 path = os.path.join(d, fpath)
164 path = os.path.join(d, fpath)
166 if os.path.exists(path):
165 if os.path.exists(path):
167 break
166 break
168 try:
167 try:
169 os.stat(path)
168 os.stat(path)
170 ct = mimetypes.guess_type(path)[0] or "text/plain"
169 ct = mimetypes.guess_type(path)[0] or "text/plain"
171 with open(path, 'rb') as fh:
170 with open(path, 'rb') as fh:
172 data = fh.read()
171 data = fh.read()
173
172
174 req.respond(HTTP_OK, ct, body=data)
173 req.respond(HTTP_OK, ct, body=data)
175 except TypeError:
174 except TypeError:
176 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
175 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
177 except OSError as err:
176 except OSError as err:
178 if err.errno == errno.ENOENT:
177 if err.errno == errno.ENOENT:
179 raise ErrorResponse(HTTP_NOT_FOUND)
178 raise ErrorResponse(HTTP_NOT_FOUND)
180 else:
179 else:
181 raise ErrorResponse(HTTP_SERVER_ERROR,
180 raise ErrorResponse(HTTP_SERVER_ERROR,
182 encoding.strtolocal(err.strerror))
181 encoding.strtolocal(err.strerror))
183
182
184 def paritygen(stripecount, offset=0):
183 def paritygen(stripecount, offset=0):
185 """count parity of horizontal stripes for easier reading"""
184 """count parity of horizontal stripes for easier reading"""
186 if stripecount and offset:
185 if stripecount and offset:
187 # account for offset, e.g. due to building the list in reverse
186 # account for offset, e.g. due to building the list in reverse
188 count = (stripecount + offset) % stripecount
187 count = (stripecount + offset) % stripecount
189 parity = (stripecount + offset) / stripecount & 1
188 parity = (stripecount + offset) / stripecount & 1
190 else:
189 else:
191 count = 0
190 count = 0
192 parity = 0
191 parity = 0
193 while True:
192 while True:
194 yield parity
193 yield parity
195 count += 1
194 count += 1
196 if stripecount and count >= stripecount:
195 if stripecount and count >= stripecount:
197 parity = 1 - parity
196 parity = 1 - parity
198 count = 0
197 count = 0
199
198
200 def get_contact(config):
199 def get_contact(config):
201 """Return repo contact information or empty string.
200 """Return repo contact information or empty string.
202
201
203 web.contact is the primary source, but if that is not set, try
202 web.contact is the primary source, but if that is not set, try
204 ui.username or $EMAIL as a fallback to display something useful.
203 ui.username or $EMAIL as a fallback to display something useful.
205 """
204 """
206 return (config("web", "contact") or
205 return (config("web", "contact") or
207 config("ui", "username") or
206 config("ui", "username") or
208 encoding.environ.get("EMAIL") or "")
207 encoding.environ.get("EMAIL") or "")
209
208
210 def caching(web, req):
209 def caching(web, req):
211 tag = r'W/"%d"' % web.mtime
210 tag = r'W/"%d"' % web.mtime
212 if req.env.get('HTTP_IF_NONE_MATCH') == tag:
211 if req.env.get('HTTP_IF_NONE_MATCH') == tag:
213 raise ErrorResponse(HTTP_NOT_MODIFIED)
212 raise ErrorResponse(HTTP_NOT_MODIFIED)
214 req.headers.append(('ETag', tag))
213 req.headers.append(('ETag', tag))
215
214
216 def cspvalues(ui):
215 def cspvalues(ui):
217 """Obtain the Content-Security-Policy header and nonce value.
216 """Obtain the Content-Security-Policy header and nonce value.
218
217
219 Returns a 2-tuple of the CSP header value and the nonce value.
218 Returns a 2-tuple of the CSP header value and the nonce value.
220
219
221 First value is ``None`` if CSP isn't enabled. Second value is ``None``
220 First value is ``None`` if CSP isn't enabled. Second value is ``None``
222 if CSP isn't enabled or if the CSP header doesn't need a nonce.
221 if CSP isn't enabled or if the CSP header doesn't need a nonce.
223 """
222 """
223 # Without demandimport, "import uuid" could have an immediate side-effect
224 # running "ldconfig" on Linux trying to find libuuid.
225 # With Python <= 2.7.12, that "ldconfig" is run via a shell and the shell
226 # may pollute the terminal with:
227 #
228 # shell-init: error retrieving current directory: getcwd: cannot access
229 # parent directories: No such file or directory
230 #
231 # Python >= 2.7.13 has fixed it by running "ldconfig" directly without a
232 # shell (hg changeset a09ae70f3489).
233 #
234 # Moved "import uuid" from here so it's executed after we know we have
235 # a sane cwd (i.e. after dispatch.py cwd check).
236 #
237 # We can move it back once we no longer need Python <= 2.7.12 support.
238 import uuid
239
224 # Don't allow untrusted CSP setting since it be disable protections
240 # Don't allow untrusted CSP setting since it be disable protections
225 # from a trusted/global source.
241 # from a trusted/global source.
226 csp = ui.config('web', 'csp', untrusted=False)
242 csp = ui.config('web', 'csp', untrusted=False)
227 nonce = None
243 nonce = None
228
244
229 if csp and '%nonce%' in csp:
245 if csp and '%nonce%' in csp:
230 nonce = base64.urlsafe_b64encode(uuid.uuid4().bytes).rstrip('=')
246 nonce = base64.urlsafe_b64encode(uuid.uuid4().bytes).rstrip('=')
231 csp = csp.replace('%nonce%', nonce)
247 csp = csp.replace('%nonce%', nonce)
232
248
233 return csp, nonce
249 return csp, nonce
@@ -1,62 +1,75 b''
1 test command parsing and dispatch
1 test command parsing and dispatch
2
2
3 $ hg init a
3 $ hg init a
4 $ cd a
4 $ cd a
5
5
6 Redundant options used to crash (issue436):
6 Redundant options used to crash (issue436):
7 $ hg -v log -v
7 $ hg -v log -v
8 $ hg -v log -v x
8 $ hg -v log -v x
9
9
10 $ echo a > a
10 $ echo a > a
11 $ hg ci -Ama
11 $ hg ci -Ama
12 adding a
12 adding a
13
13
14 Missing arg:
14 Missing arg:
15
15
16 $ hg cat
16 $ hg cat
17 hg cat: invalid arguments
17 hg cat: invalid arguments
18 hg cat [OPTION]... FILE...
18 hg cat [OPTION]... FILE...
19
19
20 output the current or given revision of files
20 output the current or given revision of files
21
21
22 options ([+] can be repeated):
22 options ([+] can be repeated):
23
23
24 -o --output FORMAT print output to file with formatted name
24 -o --output FORMAT print output to file with formatted name
25 -r --rev REV print the given revision
25 -r --rev REV print the given revision
26 --decode apply any matching decode filter
26 --decode apply any matching decode filter
27 -I --include PATTERN [+] include names matching the given patterns
27 -I --include PATTERN [+] include names matching the given patterns
28 -X --exclude PATTERN [+] exclude names matching the given patterns
28 -X --exclude PATTERN [+] exclude names matching the given patterns
29
29
30 (use 'hg cat -h' to show more help)
30 (use 'hg cat -h' to show more help)
31 [255]
31 [255]
32
32
33 [defaults]
33 [defaults]
34
34
35 $ hg cat a
35 $ hg cat a
36 a
36 a
37 $ cat >> $HGRCPATH <<EOF
37 $ cat >> $HGRCPATH <<EOF
38 > [defaults]
38 > [defaults]
39 > cat = -r null
39 > cat = -r null
40 > EOF
40 > EOF
41 $ hg cat a
41 $ hg cat a
42 a: no such file in rev 000000000000
42 a: no such file in rev 000000000000
43 [1]
43 [1]
44
44
45 $ cd "$TESTTMP"
45 $ cd "$TESTTMP"
46
46
47 OSError "No such file or directory" / "The system cannot find the path
47 OSError "No such file or directory" / "The system cannot find the path
48 specified" should include filename even when it is empty
48 specified" should include filename even when it is empty
49
49
50 $ hg -R a archive ''
50 $ hg -R a archive ''
51 abort: *: '' (glob)
51 abort: *: '' (glob)
52 [255]
52 [255]
53
53
54 #if no-outer-repo
54 #if no-outer-repo
55
55
56 No repo:
56 No repo:
57
57
58 $ hg cat
58 $ hg cat
59 abort: no repository found in '$TESTTMP' (.hg not found)!
59 abort: no repository found in '$TESTTMP' (.hg not found)!
60 [255]
60 [255]
61
61
62 #endif
62 #endif
63
64 #if rmcwd
65
66 Current directory removed:
67
68 $ mkdir $TESTTMP/repo1
69 $ cd $TESTTMP/repo1
70 $ rm -rf $TESTTMP/repo1
71 $ HGDEMANDIMPORT=disable hg version -q
72 abort: error getting current working directory: * (glob)
73 [255]
74
75 #endif
General Comments 0
You need to be logged in to leave comments. Login now