##// END OF EJS Templates
hgweb: validate WSGI environment dict...
Gregory Szorc -
r36821:8e1556ac default
parent child Browse files
Show More
@@ -1,347 +1,349 b''
1 1 # hgweb/server.py - The standalone hg web server.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005-2007 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 errno
12 12 import os
13 13 import socket
14 14 import sys
15 15 import traceback
16 import wsgiref.validate
16 17
17 18 from ..i18n import _
18 19
19 20 from .. import (
20 21 encoding,
21 22 error,
22 23 pycompat,
23 24 util,
24 25 )
25 26
26 27 httpservermod = util.httpserver
27 28 socketserver = util.socketserver
28 29 urlerr = util.urlerr
29 30 urlreq = util.urlreq
30 31
31 32 from . import (
32 33 common,
33 34 )
34 35
35 36 def _splitURI(uri):
36 37 """Return path and query that has been split from uri
37 38
38 39 Just like CGI environment, the path is unquoted, the query is
39 40 not.
40 41 """
41 42 if r'?' in uri:
42 43 path, query = uri.split(r'?', 1)
43 44 else:
44 45 path, query = uri, r''
45 46 return urlreq.unquote(path), query
46 47
47 48 class _error_logger(object):
48 49 def __init__(self, handler):
49 50 self.handler = handler
50 51 def flush(self):
51 52 pass
52 53 def write(self, str):
53 54 self.writelines(str.split('\n'))
54 55 def writelines(self, seq):
55 56 for msg in seq:
56 57 self.handler.log_error("HG error: %s", msg)
57 58
58 59 class _httprequesthandler(httpservermod.basehttprequesthandler):
59 60
60 61 url_scheme = 'http'
61 62
62 63 @staticmethod
63 64 def preparehttpserver(httpserver, ui):
64 65 """Prepare .socket of new HTTPServer instance"""
65 66
66 67 def __init__(self, *args, **kargs):
67 68 self.protocol_version = r'HTTP/1.1'
68 69 httpservermod.basehttprequesthandler.__init__(self, *args, **kargs)
69 70
70 71 def _log_any(self, fp, format, *args):
71 72 fp.write(pycompat.sysbytes(
72 73 r"%s - - [%s] %s" % (self.client_address[0],
73 74 self.log_date_time_string(),
74 75 format % args)) + '\n')
75 76 fp.flush()
76 77
77 78 def log_error(self, format, *args):
78 79 self._log_any(self.server.errorlog, format, *args)
79 80
80 81 def log_message(self, format, *args):
81 82 self._log_any(self.server.accesslog, format, *args)
82 83
83 84 def log_request(self, code=r'-', size=r'-'):
84 85 xheaders = []
85 86 if util.safehasattr(self, 'headers'):
86 87 xheaders = [h for h in self.headers.items()
87 88 if h[0].startswith(r'x-')]
88 89 self.log_message(r'"%s" %s %s%s',
89 90 self.requestline, str(code), str(size),
90 91 r''.join([r' %s:%s' % h for h in sorted(xheaders)]))
91 92
92 93 def do_write(self):
93 94 try:
94 95 self.do_hgweb()
95 96 except socket.error as inst:
96 97 if inst[0] != errno.EPIPE:
97 98 raise
98 99
99 100 def do_POST(self):
100 101 try:
101 102 self.do_write()
102 103 except Exception:
103 104 self._start_response("500 Internal Server Error", [])
104 105 self._write("Internal Server Error")
105 106 self._done()
106 107 tb = r"".join(traceback.format_exception(*sys.exc_info()))
107 108 # We need a native-string newline to poke in the log
108 109 # message, because we won't get a newline when using an
109 110 # r-string. This is the easy way out.
110 111 newline = chr(10)
111 112 self.log_error(r"Exception happened during processing "
112 113 r"request '%s':%s%s", self.path, newline, tb)
113 114
114 115 def do_GET(self):
115 116 self.do_POST()
116 117
117 118 def do_hgweb(self):
118 119 self.sent_headers = False
119 120 path, query = _splitURI(self.path)
120 121
121 122 env = {}
122 123 env[r'GATEWAY_INTERFACE'] = r'CGI/1.1'
123 124 env[r'REQUEST_METHOD'] = self.command
124 125 env[r'SERVER_NAME'] = self.server.server_name
125 126 env[r'SERVER_PORT'] = str(self.server.server_port)
126 127 env[r'REQUEST_URI'] = self.path
127 128 env[r'SCRIPT_NAME'] = pycompat.sysstr(self.server.prefix)
128 129 env[r'PATH_INFO'] = pycompat.sysstr(path[len(self.server.prefix):])
129 130 env[r'REMOTE_HOST'] = self.client_address[0]
130 131 env[r'REMOTE_ADDR'] = self.client_address[0]
131 if query:
132 env[r'QUERY_STRING'] = query
132 env[r'QUERY_STRING'] = query or r''
133 133
134 134 if pycompat.ispy3:
135 135 if self.headers.get_content_type() is None:
136 136 env[r'CONTENT_TYPE'] = self.headers.get_default_type()
137 137 else:
138 138 env[r'CONTENT_TYPE'] = self.headers.get_content_type()
139 139 length = self.headers.get('content-length')
140 140 else:
141 141 if self.headers.typeheader is None:
142 142 env[r'CONTENT_TYPE'] = self.headers.type
143 143 else:
144 144 env[r'CONTENT_TYPE'] = self.headers.typeheader
145 145 length = self.headers.getheader('content-length')
146 146 if length:
147 147 env[r'CONTENT_LENGTH'] = length
148 148 for header in [h for h in self.headers.keys()
149 149 if h not in ('content-type', 'content-length')]:
150 150 hkey = r'HTTP_' + header.replace(r'-', r'_').upper()
151 151 hval = self.headers.get(header)
152 152 hval = hval.replace(r'\n', r'').strip()
153 153 if hval:
154 154 env[hkey] = hval
155 155 env[r'SERVER_PROTOCOL'] = self.request_version
156 156 env[r'wsgi.version'] = (1, 0)
157 157 env[r'wsgi.url_scheme'] = pycompat.sysstr(self.url_scheme)
158 158 if env.get(r'HTTP_EXPECT', '').lower() == '100-continue':
159 159 self.rfile = common.continuereader(self.rfile, self.wfile.write)
160 160
161 161 env[r'wsgi.input'] = self.rfile
162 162 env[r'wsgi.errors'] = _error_logger(self)
163 163 env[r'wsgi.multithread'] = isinstance(self.server,
164 164 socketserver.ThreadingMixIn)
165 165 env[r'wsgi.multiprocess'] = isinstance(self.server,
166 166 socketserver.ForkingMixIn)
167 167 env[r'wsgi.run_once'] = 0
168 168
169 wsgiref.validate.check_environ(env)
170
169 171 self.saved_status = None
170 172 self.saved_headers = []
171 173 self.length = None
172 174 self._chunked = None
173 175 for chunk in self.server.application(env, self._start_response):
174 176 self._write(chunk)
175 177 if not self.sent_headers:
176 178 self.send_headers()
177 179 self._done()
178 180
179 181 def send_headers(self):
180 182 if not self.saved_status:
181 183 raise AssertionError("Sending headers before "
182 184 "start_response() called")
183 185 saved_status = self.saved_status.split(None, 1)
184 186 saved_status[0] = int(saved_status[0])
185 187 self.send_response(*saved_status)
186 188 self.length = None
187 189 self._chunked = False
188 190 for h in self.saved_headers:
189 191 self.send_header(*h)
190 192 if h[0].lower() == 'content-length':
191 193 self.length = int(h[1])
192 194 if (self.length is None and
193 195 saved_status[0] != common.HTTP_NOT_MODIFIED):
194 196 self._chunked = (not self.close_connection and
195 197 self.request_version == "HTTP/1.1")
196 198 if self._chunked:
197 199 self.send_header(r'Transfer-Encoding', r'chunked')
198 200 else:
199 201 self.send_header(r'Connection', r'close')
200 202 self.end_headers()
201 203 self.sent_headers = True
202 204
203 205 def _start_response(self, http_status, headers, exc_info=None):
204 206 code, msg = http_status.split(None, 1)
205 207 code = int(code)
206 208 self.saved_status = http_status
207 209 bad_headers = ('connection', 'transfer-encoding')
208 210 self.saved_headers = [h for h in headers
209 211 if h[0].lower() not in bad_headers]
210 212 return self._write
211 213
212 214 def _write(self, data):
213 215 if not self.saved_status:
214 216 raise AssertionError("data written before start_response() called")
215 217 elif not self.sent_headers:
216 218 self.send_headers()
217 219 if self.length is not None:
218 220 if len(data) > self.length:
219 221 raise AssertionError("Content-length header sent, but more "
220 222 "bytes than specified are being written.")
221 223 self.length = self.length - len(data)
222 224 elif self._chunked and data:
223 225 data = '%x\r\n%s\r\n' % (len(data), data)
224 226 self.wfile.write(data)
225 227 self.wfile.flush()
226 228
227 229 def _done(self):
228 230 if self._chunked:
229 231 self.wfile.write('0\r\n\r\n')
230 232 self.wfile.flush()
231 233
232 234 class _httprequesthandlerssl(_httprequesthandler):
233 235 """HTTPS handler based on Python's ssl module"""
234 236
235 237 url_scheme = 'https'
236 238
237 239 @staticmethod
238 240 def preparehttpserver(httpserver, ui):
239 241 try:
240 242 from .. import sslutil
241 243 sslutil.modernssl
242 244 except ImportError:
243 245 raise error.Abort(_("SSL support is unavailable"))
244 246
245 247 certfile = ui.config('web', 'certificate')
246 248
247 249 # These config options are currently only meant for testing. Use
248 250 # at your own risk.
249 251 cafile = ui.config('devel', 'servercafile')
250 252 reqcert = ui.configbool('devel', 'serverrequirecert')
251 253
252 254 httpserver.socket = sslutil.wrapserversocket(httpserver.socket,
253 255 ui,
254 256 certfile=certfile,
255 257 cafile=cafile,
256 258 requireclientcert=reqcert)
257 259
258 260 def setup(self):
259 261 self.connection = self.request
260 262 self.rfile = self.request.makefile(r"rb", self.rbufsize)
261 263 self.wfile = self.request.makefile(r"wb", self.wbufsize)
262 264
263 265 try:
264 266 import threading
265 267 threading.activeCount() # silence pyflakes and bypass demandimport
266 268 _mixin = socketserver.ThreadingMixIn
267 269 except ImportError:
268 270 if util.safehasattr(os, "fork"):
269 271 _mixin = socketserver.ForkingMixIn
270 272 else:
271 273 class _mixin(object):
272 274 pass
273 275
274 276 def openlog(opt, default):
275 277 if opt and opt != '-':
276 278 return open(opt, 'ab')
277 279 return default
278 280
279 281 class MercurialHTTPServer(_mixin, httpservermod.httpserver, object):
280 282
281 283 # SO_REUSEADDR has broken semantics on windows
282 284 if pycompat.iswindows:
283 285 allow_reuse_address = 0
284 286
285 287 def __init__(self, ui, app, addr, handler, **kwargs):
286 288 httpservermod.httpserver.__init__(self, addr, handler, **kwargs)
287 289 self.daemon_threads = True
288 290 self.application = app
289 291
290 292 handler.preparehttpserver(self, ui)
291 293
292 294 prefix = ui.config('web', 'prefix')
293 295 if prefix:
294 296 prefix = '/' + prefix.strip('/')
295 297 self.prefix = prefix
296 298
297 299 alog = openlog(ui.config('web', 'accesslog'), ui.fout)
298 300 elog = openlog(ui.config('web', 'errorlog'), ui.ferr)
299 301 self.accesslog = alog
300 302 self.errorlog = elog
301 303
302 304 self.addr, self.port = self.socket.getsockname()[0:2]
303 305 self.fqaddr = socket.getfqdn(addr[0])
304 306
305 307 class IPv6HTTPServer(MercurialHTTPServer):
306 308 address_family = getattr(socket, 'AF_INET6', None)
307 309 def __init__(self, *args, **kwargs):
308 310 if self.address_family is None:
309 311 raise error.RepoError(_('IPv6 is not available on this system'))
310 312 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
311 313
312 314 def create_server(ui, app):
313 315
314 316 if ui.config('web', 'certificate'):
315 317 handler = _httprequesthandlerssl
316 318 else:
317 319 handler = _httprequesthandler
318 320
319 321 if ui.configbool('web', 'ipv6'):
320 322 cls = IPv6HTTPServer
321 323 else:
322 324 cls = MercurialHTTPServer
323 325
324 326 # ugly hack due to python issue5853 (for threaded use)
325 327 try:
326 328 import mimetypes
327 329 mimetypes.init()
328 330 except UnicodeDecodeError:
329 331 # Python 2.x's mimetypes module attempts to decode strings
330 332 # from Windows' ANSI APIs as ascii (fail), then re-encode them
331 333 # as ascii (clown fail), because the default Python Unicode
332 334 # codec is hardcoded as ascii.
333 335
334 336 sys.argv # unwrap demand-loader so that reload() works
335 337 reload(sys) # resurrect sys.setdefaultencoding()
336 338 oldenc = sys.getdefaultencoding()
337 339 sys.setdefaultencoding("latin1") # or any full 8-bit encoding
338 340 mimetypes.init()
339 341 sys.setdefaultencoding(oldenc)
340 342
341 343 address = ui.config('web', 'address')
342 344 port = util.getport(ui.config('web', 'port'))
343 345 try:
344 346 return cls(ui, app, (address, port), handler)
345 347 except socket.error as inst:
346 348 raise error.Abort(_("cannot start server at '%s:%d': %s")
347 349 % (address, port, encoding.strtolocal(inst.args[1])))
General Comments 0
You need to be logged in to leave comments. Login now