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