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