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