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