##// END OF EJS Templates
py3: compare http server's command with a native string...
Denis Laxalde -
r43793:92e24a13 stable
parent child Browse files
Show More
@@ -1,442 +1,442 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 r'?' in uri:
47 47 path, query = uri.split(r'?', 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(r"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=r'-', size=r'-'):
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(r'x-')
105 105 ]
106 106 self.log_message(
107 107 r'"%s" %s %s%s',
108 108 self.requestline,
109 109 str(code),
110 110 str(size),
111 111 r''.join([r' %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 = r"".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 r"request '%s':%s%s",
139 139 self.path,
140 140 newline,
141 141 tb,
142 142 )
143 143
144 144 self._start_response(r"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 if self.command == b'POST':
163 if self.command == r'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 = [(r'Connection', r'Close')]
170 170 self._write(b"Not Found")
171 171 self._done()
172 172 return
173 173
174 174 env = {}
175 175 env[r'GATEWAY_INTERFACE'] = r'CGI/1.1'
176 176 env[r'REQUEST_METHOD'] = self.command
177 177 env[r'SERVER_NAME'] = self.server.server_name
178 178 env[r'SERVER_PORT'] = str(self.server.server_port)
179 179 env[r'REQUEST_URI'] = self.path
180 180 env[r'SCRIPT_NAME'] = pycompat.sysstr(self.server.prefix)
181 181 env[r'PATH_INFO'] = pycompat.sysstr(path[len(self.server.prefix) :])
182 182 env[r'REMOTE_HOST'] = self.client_address[0]
183 183 env[r'REMOTE_ADDR'] = self.client_address[0]
184 184 env[r'QUERY_STRING'] = query or r''
185 185
186 186 if pycompat.ispy3:
187 187 if self.headers.get_content_type() is None:
188 188 env[r'CONTENT_TYPE'] = self.headers.get_default_type()
189 189 else:
190 190 env[r'CONTENT_TYPE'] = self.headers.get_content_type()
191 191 length = self.headers.get(r'content-length')
192 192 else:
193 193 if self.headers.typeheader is None:
194 194 env[r'CONTENT_TYPE'] = self.headers.type
195 195 else:
196 196 env[r'CONTENT_TYPE'] = self.headers.typeheader
197 197 length = self.headers.getheader(r'content-length')
198 198 if length:
199 199 env[r'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 (r'content-type', r'content-length')
204 204 ]:
205 205 hkey = r'HTTP_' + header.replace(r'-', r'_').upper()
206 206 hval = self.headers.get(header)
207 207 hval = hval.replace(r'\n', r'').strip()
208 208 if hval:
209 209 env[hkey] = hval
210 210 env[r'SERVER_PROTOCOL'] = self.request_version
211 211 env[r'wsgi.version'] = (1, 0)
212 212 env[r'wsgi.url_scheme'] = pycompat.sysstr(self.url_scheme)
213 213 if env.get(r'HTTP_EXPECT', b'').lower() == b'100-continue':
214 214 self.rfile = common.continuereader(self.rfile, self.wfile.write)
215 215
216 216 env[r'wsgi.input'] = self.rfile
217 217 env[r'wsgi.errors'] = _error_logger(self)
218 218 env[r'wsgi.multithread'] = isinstance(
219 219 self.server, socketserver.ThreadingMixIn
220 220 )
221 221 if util.safehasattr(socketserver, b'ForkingMixIn'):
222 222 env[r'wsgi.multiprocess'] = isinstance(
223 223 self.server, socketserver.ForkingMixIn
224 224 )
225 225 else:
226 226 env[r'wsgi.multiprocess'] = False
227 227
228 228 env[r'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() == r'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
259 259 and self.request_version == r'HTTP/1.1'
260 260 )
261 261 if self._chunked:
262 262 self.send_header(r'Transfer-Encoding', r'chunked')
263 263 else:
264 264 self.send_header(r'Connection', r'close')
265 265 self.end_headers()
266 266 self.sent_headers = True
267 267
268 268 def _start_response(self, http_status, headers, exc_info=None):
269 269 assert isinstance(http_status, str)
270 270 code, msg = http_status.split(None, 1)
271 271 code = int(code)
272 272 self.saved_status = http_status
273 273 bad_headers = (r'connection', r'transfer-encoding')
274 274 self.saved_headers = [
275 275 h for h in headers if h[0].lower() not in bad_headers
276 276 ]
277 277 return self._write
278 278
279 279 def _write(self, data):
280 280 if not self.saved_status:
281 281 raise AssertionError(b"data written before start_response() called")
282 282 elif not self.sent_headers:
283 283 self.send_headers()
284 284 if self.length is not None:
285 285 if len(data) > self.length:
286 286 raise AssertionError(
287 287 b"Content-length header sent, but more "
288 288 b"bytes than specified are being written."
289 289 )
290 290 self.length = self.length - len(data)
291 291 elif self._chunked and data:
292 292 data = b'%x\r\n%s\r\n' % (len(data), data)
293 293 self.wfile.write(data)
294 294 self.wfile.flush()
295 295
296 296 def _done(self):
297 297 if self._chunked:
298 298 self.wfile.write(b'0\r\n\r\n')
299 299 self.wfile.flush()
300 300
301 301 def version_string(self):
302 302 if self.server.serverheader:
303 303 return encoding.strfromlocal(self.server.serverheader)
304 304 return httpservermod.basehttprequesthandler.version_string(self)
305 305
306 306
307 307 class _httprequesthandlerssl(_httprequesthandler):
308 308 """HTTPS handler based on Python's ssl module"""
309 309
310 310 url_scheme = b'https'
311 311
312 312 @staticmethod
313 313 def preparehttpserver(httpserver, ui):
314 314 try:
315 315 from .. import sslutil
316 316
317 317 sslutil.modernssl
318 318 except ImportError:
319 319 raise error.Abort(_(b"SSL support is unavailable"))
320 320
321 321 certfile = ui.config(b'web', b'certificate')
322 322
323 323 # These config options are currently only meant for testing. Use
324 324 # at your own risk.
325 325 cafile = ui.config(b'devel', b'servercafile')
326 326 reqcert = ui.configbool(b'devel', b'serverrequirecert')
327 327
328 328 httpserver.socket = sslutil.wrapserversocket(
329 329 httpserver.socket,
330 330 ui,
331 331 certfile=certfile,
332 332 cafile=cafile,
333 333 requireclientcert=reqcert,
334 334 )
335 335
336 336 def setup(self):
337 337 self.connection = self.request
338 338 self.rfile = self.request.makefile(r"rb", self.rbufsize)
339 339 self.wfile = self.request.makefile(r"wb", self.wbufsize)
340 340
341 341
342 342 try:
343 343 import threading
344 344
345 345 threading.activeCount() # silence pyflakes and bypass demandimport
346 346 _mixin = socketserver.ThreadingMixIn
347 347 except ImportError:
348 348 if util.safehasattr(os, b"fork"):
349 349 _mixin = socketserver.ForkingMixIn
350 350 else:
351 351
352 352 class _mixin(object):
353 353 pass
354 354
355 355
356 356 def openlog(opt, default):
357 357 if opt and opt != b'-':
358 358 return open(opt, b'ab')
359 359 return default
360 360
361 361
362 362 class MercurialHTTPServer(_mixin, httpservermod.httpserver, object):
363 363
364 364 # SO_REUSEADDR has broken semantics on windows
365 365 if pycompat.iswindows:
366 366 allow_reuse_address = 0
367 367
368 368 def __init__(self, ui, app, addr, handler, **kwargs):
369 369 httpservermod.httpserver.__init__(self, addr, handler, **kwargs)
370 370 self.daemon_threads = True
371 371 self.application = app
372 372
373 373 handler.preparehttpserver(self, ui)
374 374
375 375 prefix = ui.config(b'web', b'prefix')
376 376 if prefix:
377 377 prefix = b'/' + prefix.strip(b'/')
378 378 self.prefix = prefix
379 379
380 380 alog = openlog(ui.config(b'web', b'accesslog'), ui.fout)
381 381 elog = openlog(ui.config(b'web', b'errorlog'), ui.ferr)
382 382 self.accesslog = alog
383 383 self.errorlog = elog
384 384
385 385 self.addr, self.port = self.socket.getsockname()[0:2]
386 386 self.fqaddr = socket.getfqdn(addr[0])
387 387
388 388 self.serverheader = ui.config(b'web', b'server-header')
389 389
390 390
391 391 class IPv6HTTPServer(MercurialHTTPServer):
392 392 address_family = getattr(socket, 'AF_INET6', None)
393 393
394 394 def __init__(self, *args, **kwargs):
395 395 if self.address_family is None:
396 396 raise error.RepoError(_(b'IPv6 is not available on this system'))
397 397 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
398 398
399 399
400 400 def create_server(ui, app):
401 401
402 402 if ui.config(b'web', b'certificate'):
403 403 handler = _httprequesthandlerssl
404 404 else:
405 405 handler = _httprequesthandler
406 406
407 407 if ui.configbool(b'web', b'ipv6'):
408 408 cls = IPv6HTTPServer
409 409 else:
410 410 cls = MercurialHTTPServer
411 411
412 412 # ugly hack due to python issue5853 (for threaded use)
413 413 try:
414 414 import mimetypes
415 415
416 416 mimetypes.init()
417 417 except UnicodeDecodeError:
418 418 # Python 2.x's mimetypes module attempts to decode strings
419 419 # from Windows' ANSI APIs as ascii (fail), then re-encode them
420 420 # as ascii (clown fail), because the default Python Unicode
421 421 # codec is hardcoded as ascii.
422 422
423 423 sys.argv # unwrap demand-loader so that reload() works
424 424 # resurrect sys.setdefaultencoding()
425 425 try:
426 426 importlib.reload(sys)
427 427 except AttributeError:
428 428 reload(sys)
429 429 oldenc = sys.getdefaultencoding()
430 430 sys.setdefaultencoding(b"latin1") # or any full 8-bit encoding
431 431 mimetypes.init()
432 432 sys.setdefaultencoding(oldenc)
433 433
434 434 address = ui.config(b'web', b'address')
435 435 port = util.getport(ui.config(b'web', b'port'))
436 436 try:
437 437 return cls(ui, app, (address, port), handler)
438 438 except socket.error as inst:
439 439 raise error.Abort(
440 440 _(b"cannot start server at '%s:%d': %s")
441 441 % (address, port, encoding.strtolocal(inst.args[1]))
442 442 )
General Comments 0
You need to be logged in to leave comments. Login now