##// END OF EJS Templates
hgweb: document continuereader...
Gregory Szorc -
r36869:70666171 default
parent child Browse files
Show More
@@ -1,250 +1,257 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 stat
15 import stat
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(r'REMOTE_USER')
49 user = req.env.get(r'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[r'REQUEST_METHOD'] != r'POST':
65 if req.env[r'REQUEST_METHOD'] != r'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') and scheme != 'https':
72 if hgweb.configbool('web', 'push_ssl') 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=None):
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, pycompat.sysstr(message))
97 Exception.__init__(self, pycompat.sysstr(message))
98 self.code = code
98 self.code = code
99 if headers is None:
99 if headers is None:
100 headers = []
100 headers = []
101 self.headers = headers
101 self.headers = headers
102
102
103 class continuereader(object):
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 def __init__(self, f, write):
111 def __init__(self, f, write):
105 self.f = f
112 self.f = f
106 self._write = write
113 self._write = write
107 self.continued = False
114 self.continued = False
108
115
109 def read(self, amt=-1):
116 def read(self, amt=-1):
110 if not self.continued:
117 if not self.continued:
111 self.continued = True
118 self.continued = True
112 self._write('HTTP/1.1 100 Continue\r\n\r\n')
119 self._write('HTTP/1.1 100 Continue\r\n\r\n')
113 return self.f.read(amt)
120 return self.f.read(amt)
114
121
115 def __getattr__(self, attr):
122 def __getattr__(self, attr):
116 if attr in ('close', 'readline', 'readlines', '__iter__'):
123 if attr in ('close', 'readline', 'readlines', '__iter__'):
117 return getattr(self.f, attr)
124 return getattr(self.f, attr)
118 raise AttributeError
125 raise AttributeError
119
126
120 def _statusmessage(code):
127 def _statusmessage(code):
121 responses = httpserver.basehttprequesthandler.responses
128 responses = httpserver.basehttprequesthandler.responses
122 return responses.get(code, ('Error', 'Unknown error'))[0]
129 return responses.get(code, ('Error', 'Unknown error'))[0]
123
130
124 def statusmessage(code, message=None):
131 def statusmessage(code, message=None):
125 return '%d %s' % (code, message or _statusmessage(code))
132 return '%d %s' % (code, message or _statusmessage(code))
126
133
127 def get_stat(spath, fn):
134 def get_stat(spath, fn):
128 """stat fn if it exists, spath otherwise"""
135 """stat fn if it exists, spath otherwise"""
129 cl_path = os.path.join(spath, fn)
136 cl_path = os.path.join(spath, fn)
130 if os.path.exists(cl_path):
137 if os.path.exists(cl_path):
131 return os.stat(cl_path)
138 return os.stat(cl_path)
132 else:
139 else:
133 return os.stat(spath)
140 return os.stat(spath)
134
141
135 def get_mtime(spath):
142 def get_mtime(spath):
136 return get_stat(spath, "00changelog.i")[stat.ST_MTIME]
143 return get_stat(spath, "00changelog.i")[stat.ST_MTIME]
137
144
138 def ispathsafe(path):
145 def ispathsafe(path):
139 """Determine if a path is safe to use for filesystem access."""
146 """Determine if a path is safe to use for filesystem access."""
140 parts = path.split('/')
147 parts = path.split('/')
141 for part in parts:
148 for part in parts:
142 if (part in ('', pycompat.oscurdir, pycompat.ospardir) or
149 if (part in ('', pycompat.oscurdir, pycompat.ospardir) or
143 pycompat.ossep in part or
150 pycompat.ossep in part or
144 pycompat.osaltsep is not None and pycompat.osaltsep in part):
151 pycompat.osaltsep is not None and pycompat.osaltsep in part):
145 return False
152 return False
146
153
147 return True
154 return True
148
155
149 def staticfile(directory, fname, req):
156 def staticfile(directory, fname, req):
150 """return a file inside directory with guessed Content-Type header
157 """return a file inside directory with guessed Content-Type header
151
158
152 fname always uses '/' as directory separator and isn't allowed to
159 fname always uses '/' as directory separator and isn't allowed to
153 contain unusual path components.
160 contain unusual path components.
154 Content-Type is guessed using the mimetypes module.
161 Content-Type is guessed using the mimetypes module.
155 Return an empty string if fname is illegal or file not found.
162 Return an empty string if fname is illegal or file not found.
156
163
157 """
164 """
158 if not ispathsafe(fname):
165 if not ispathsafe(fname):
159 return
166 return
160
167
161 fpath = os.path.join(*fname.split('/'))
168 fpath = os.path.join(*fname.split('/'))
162 if isinstance(directory, str):
169 if isinstance(directory, str):
163 directory = [directory]
170 directory = [directory]
164 for d in directory:
171 for d in directory:
165 path = os.path.join(d, fpath)
172 path = os.path.join(d, fpath)
166 if os.path.exists(path):
173 if os.path.exists(path):
167 break
174 break
168 try:
175 try:
169 os.stat(path)
176 os.stat(path)
170 ct = mimetypes.guess_type(pycompat.fsdecode(path))[0] or "text/plain"
177 ct = mimetypes.guess_type(pycompat.fsdecode(path))[0] or "text/plain"
171 with open(path, 'rb') as fh:
178 with open(path, 'rb') as fh:
172 data = fh.read()
179 data = fh.read()
173
180
174 req.respond(HTTP_OK, ct, body=data)
181 req.respond(HTTP_OK, ct, body=data)
175 except TypeError:
182 except TypeError:
176 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
183 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
177 except OSError as err:
184 except OSError as err:
178 if err.errno == errno.ENOENT:
185 if err.errno == errno.ENOENT:
179 raise ErrorResponse(HTTP_NOT_FOUND)
186 raise ErrorResponse(HTTP_NOT_FOUND)
180 else:
187 else:
181 raise ErrorResponse(HTTP_SERVER_ERROR,
188 raise ErrorResponse(HTTP_SERVER_ERROR,
182 encoding.strtolocal(err.strerror))
189 encoding.strtolocal(err.strerror))
183
190
184 def paritygen(stripecount, offset=0):
191 def paritygen(stripecount, offset=0):
185 """count parity of horizontal stripes for easier reading"""
192 """count parity of horizontal stripes for easier reading"""
186 if stripecount and offset:
193 if stripecount and offset:
187 # account for offset, e.g. due to building the list in reverse
194 # account for offset, e.g. due to building the list in reverse
188 count = (stripecount + offset) % stripecount
195 count = (stripecount + offset) % stripecount
189 parity = (stripecount + offset) // stripecount & 1
196 parity = (stripecount + offset) // stripecount & 1
190 else:
197 else:
191 count = 0
198 count = 0
192 parity = 0
199 parity = 0
193 while True:
200 while True:
194 yield parity
201 yield parity
195 count += 1
202 count += 1
196 if stripecount and count >= stripecount:
203 if stripecount and count >= stripecount:
197 parity = 1 - parity
204 parity = 1 - parity
198 count = 0
205 count = 0
199
206
200 def get_contact(config):
207 def get_contact(config):
201 """Return repo contact information or empty string.
208 """Return repo contact information or empty string.
202
209
203 web.contact is the primary source, but if that is not set, try
210 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.
211 ui.username or $EMAIL as a fallback to display something useful.
205 """
212 """
206 return (config("web", "contact") or
213 return (config("web", "contact") or
207 config("ui", "username") or
214 config("ui", "username") or
208 encoding.environ.get("EMAIL") or "")
215 encoding.environ.get("EMAIL") or "")
209
216
210 def caching(web, req):
217 def caching(web, req):
211 tag = r'W/"%d"' % web.mtime
218 tag = r'W/"%d"' % web.mtime
212 if req.env.get('HTTP_IF_NONE_MATCH') == tag:
219 if req.env.get('HTTP_IF_NONE_MATCH') == tag:
213 raise ErrorResponse(HTTP_NOT_MODIFIED)
220 raise ErrorResponse(HTTP_NOT_MODIFIED)
214 req.headers.append(('ETag', tag))
221 req.headers.append(('ETag', tag))
215
222
216 def cspvalues(ui):
223 def cspvalues(ui):
217 """Obtain the Content-Security-Policy header and nonce value.
224 """Obtain the Content-Security-Policy header and nonce value.
218
225
219 Returns a 2-tuple of the CSP header value and the nonce value.
226 Returns a 2-tuple of the CSP header value and the nonce value.
220
227
221 First value is ``None`` if CSP isn't enabled. Second value is ``None``
228 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.
229 if CSP isn't enabled or if the CSP header doesn't need a nonce.
223 """
230 """
224 # Without demandimport, "import uuid" could have an immediate side-effect
231 # Without demandimport, "import uuid" could have an immediate side-effect
225 # running "ldconfig" on Linux trying to find libuuid.
232 # running "ldconfig" on Linux trying to find libuuid.
226 # With Python <= 2.7.12, that "ldconfig" is run via a shell and the shell
233 # With Python <= 2.7.12, that "ldconfig" is run via a shell and the shell
227 # may pollute the terminal with:
234 # may pollute the terminal with:
228 #
235 #
229 # shell-init: error retrieving current directory: getcwd: cannot access
236 # shell-init: error retrieving current directory: getcwd: cannot access
230 # parent directories: No such file or directory
237 # parent directories: No such file or directory
231 #
238 #
232 # Python >= 2.7.13 has fixed it by running "ldconfig" directly without a
239 # Python >= 2.7.13 has fixed it by running "ldconfig" directly without a
233 # shell (hg changeset a09ae70f3489).
240 # shell (hg changeset a09ae70f3489).
234 #
241 #
235 # Moved "import uuid" from here so it's executed after we know we have
242 # Moved "import uuid" from here so it's executed after we know we have
236 # a sane cwd (i.e. after dispatch.py cwd check).
243 # a sane cwd (i.e. after dispatch.py cwd check).
237 #
244 #
238 # We can move it back once we no longer need Python <= 2.7.12 support.
245 # We can move it back once we no longer need Python <= 2.7.12 support.
239 import uuid
246 import uuid
240
247
241 # Don't allow untrusted CSP setting since it be disable protections
248 # Don't allow untrusted CSP setting since it be disable protections
242 # from a trusted/global source.
249 # from a trusted/global source.
243 csp = ui.config('web', 'csp', untrusted=False)
250 csp = ui.config('web', 'csp', untrusted=False)
244 nonce = None
251 nonce = None
245
252
246 if csp and '%nonce%' in csp:
253 if csp and '%nonce%' in csp:
247 nonce = base64.urlsafe_b64encode(uuid.uuid4().bytes).rstrip('=')
254 nonce = base64.urlsafe_b64encode(uuid.uuid4().bytes).rstrip('=')
248 csp = csp.replace('%nonce%', nonce)
255 csp = csp.replace('%nonce%', nonce)
249
256
250 return csp, nonce
257 return csp, nonce
General Comments 0
You need to be logged in to leave comments. Login now