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