##// END OF EJS Templates
server: always close http socket if responding with an error (issue6033)...
Augie Fackler -
r41544:197f092b 4.8.2 stable
parent child Browse files
Show More
@@ -1,373 +1,380 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 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 if self.command == 'POST':
131 # Paranoia: tell the client we're going to close the
132 # socket so they don't try and reuse a socket that
133 # might have a POST body waiting to confuse us. We do
134 # this by directly munging self.saved_headers because
135 # self._start_response ignores Connection headers.
136 self.saved_headers = [(r'Connection', r'Close')]
130 137 self._write(b"Not Found")
131 138 self._done()
132 139 return
133 140
134 141 env = {}
135 142 env[r'GATEWAY_INTERFACE'] = r'CGI/1.1'
136 143 env[r'REQUEST_METHOD'] = self.command
137 144 env[r'SERVER_NAME'] = self.server.server_name
138 145 env[r'SERVER_PORT'] = str(self.server.server_port)
139 146 env[r'REQUEST_URI'] = self.path
140 147 env[r'SCRIPT_NAME'] = pycompat.sysstr(self.server.prefix)
141 148 env[r'PATH_INFO'] = pycompat.sysstr(path[len(self.server.prefix):])
142 149 env[r'REMOTE_HOST'] = self.client_address[0]
143 150 env[r'REMOTE_ADDR'] = self.client_address[0]
144 151 env[r'QUERY_STRING'] = query or r''
145 152
146 153 if pycompat.ispy3:
147 154 if self.headers.get_content_type() is None:
148 155 env[r'CONTENT_TYPE'] = self.headers.get_default_type()
149 156 else:
150 157 env[r'CONTENT_TYPE'] = self.headers.get_content_type()
151 158 length = self.headers.get(r'content-length')
152 159 else:
153 160 if self.headers.typeheader is None:
154 161 env[r'CONTENT_TYPE'] = self.headers.type
155 162 else:
156 163 env[r'CONTENT_TYPE'] = self.headers.typeheader
157 164 length = self.headers.getheader(r'content-length')
158 165 if length:
159 166 env[r'CONTENT_LENGTH'] = length
160 167 for header in [h for h in self.headers.keys()
161 168 if h not in (r'content-type', r'content-length')]:
162 169 hkey = r'HTTP_' + header.replace(r'-', r'_').upper()
163 170 hval = self.headers.get(header)
164 171 hval = hval.replace(r'\n', r'').strip()
165 172 if hval:
166 173 env[hkey] = hval
167 174 env[r'SERVER_PROTOCOL'] = self.request_version
168 175 env[r'wsgi.version'] = (1, 0)
169 176 env[r'wsgi.url_scheme'] = pycompat.sysstr(self.url_scheme)
170 177 if env.get(r'HTTP_EXPECT', '').lower() == '100-continue':
171 178 self.rfile = common.continuereader(self.rfile, self.wfile.write)
172 179
173 180 env[r'wsgi.input'] = self.rfile
174 181 env[r'wsgi.errors'] = _error_logger(self)
175 182 env[r'wsgi.multithread'] = isinstance(self.server,
176 183 socketserver.ThreadingMixIn)
177 184 if util.safehasattr(socketserver, 'ForkingMixIn'):
178 185 env[r'wsgi.multiprocess'] = isinstance(self.server,
179 186 socketserver.ForkingMixIn)
180 187 else:
181 188 env[r'wsgi.multiprocess'] = False
182 189
183 190 env[r'wsgi.run_once'] = 0
184 191
185 192 wsgiref.validate.check_environ(env)
186 193
187 194 self.saved_status = None
188 195 self.saved_headers = []
189 196 self.length = None
190 197 self._chunked = None
191 198 for chunk in self.server.application(env, self._start_response):
192 199 self._write(chunk)
193 200 if not self.sent_headers:
194 201 self.send_headers()
195 202 self._done()
196 203
197 204 def send_headers(self):
198 205 if not self.saved_status:
199 206 raise AssertionError("Sending headers before "
200 207 "start_response() called")
201 208 saved_status = self.saved_status.split(None, 1)
202 209 saved_status[0] = int(saved_status[0])
203 210 self.send_response(*saved_status)
204 211 self.length = None
205 212 self._chunked = False
206 213 for h in self.saved_headers:
207 214 self.send_header(*h)
208 215 if h[0].lower() == r'content-length':
209 216 self.length = int(h[1])
210 217 if (self.length is None and
211 218 saved_status[0] != common.HTTP_NOT_MODIFIED):
212 219 self._chunked = (not self.close_connection and
213 220 self.request_version == r'HTTP/1.1')
214 221 if self._chunked:
215 222 self.send_header(r'Transfer-Encoding', r'chunked')
216 223 else:
217 224 self.send_header(r'Connection', r'close')
218 225 self.end_headers()
219 226 self.sent_headers = True
220 227
221 228 def _start_response(self, http_status, headers, exc_info=None):
222 229 assert isinstance(http_status, str)
223 230 code, msg = http_status.split(None, 1)
224 231 code = int(code)
225 232 self.saved_status = http_status
226 233 bad_headers = (r'connection', r'transfer-encoding')
227 234 self.saved_headers = [h for h in headers
228 235 if h[0].lower() not in bad_headers]
229 236 return self._write
230 237
231 238 def _write(self, data):
232 239 if not self.saved_status:
233 240 raise AssertionError("data written before start_response() called")
234 241 elif not self.sent_headers:
235 242 self.send_headers()
236 243 if self.length is not None:
237 244 if len(data) > self.length:
238 245 raise AssertionError("Content-length header sent, but more "
239 246 "bytes than specified are being written.")
240 247 self.length = self.length - len(data)
241 248 elif self._chunked and data:
242 249 data = '%x\r\n%s\r\n' % (len(data), data)
243 250 self.wfile.write(data)
244 251 self.wfile.flush()
245 252
246 253 def _done(self):
247 254 if self._chunked:
248 255 self.wfile.write('0\r\n\r\n')
249 256 self.wfile.flush()
250 257
251 258 def version_string(self):
252 259 if self.server.serverheader:
253 260 return encoding.strfromlocal(self.server.serverheader)
254 261 return httpservermod.basehttprequesthandler.version_string(self)
255 262
256 263 class _httprequesthandlerssl(_httprequesthandler):
257 264 """HTTPS handler based on Python's ssl module"""
258 265
259 266 url_scheme = 'https'
260 267
261 268 @staticmethod
262 269 def preparehttpserver(httpserver, ui):
263 270 try:
264 271 from .. import sslutil
265 272 sslutil.modernssl
266 273 except ImportError:
267 274 raise error.Abort(_("SSL support is unavailable"))
268 275
269 276 certfile = ui.config('web', 'certificate')
270 277
271 278 # These config options are currently only meant for testing. Use
272 279 # at your own risk.
273 280 cafile = ui.config('devel', 'servercafile')
274 281 reqcert = ui.configbool('devel', 'serverrequirecert')
275 282
276 283 httpserver.socket = sslutil.wrapserversocket(httpserver.socket,
277 284 ui,
278 285 certfile=certfile,
279 286 cafile=cafile,
280 287 requireclientcert=reqcert)
281 288
282 289 def setup(self):
283 290 self.connection = self.request
284 291 self.rfile = self.request.makefile(r"rb", self.rbufsize)
285 292 self.wfile = self.request.makefile(r"wb", self.wbufsize)
286 293
287 294 try:
288 295 import threading
289 296 threading.activeCount() # silence pyflakes and bypass demandimport
290 297 _mixin = socketserver.ThreadingMixIn
291 298 except ImportError:
292 299 if util.safehasattr(os, "fork"):
293 300 _mixin = socketserver.ForkingMixIn
294 301 else:
295 302 class _mixin(object):
296 303 pass
297 304
298 305 def openlog(opt, default):
299 306 if opt and opt != '-':
300 307 return open(opt, 'ab')
301 308 return default
302 309
303 310 class MercurialHTTPServer(_mixin, httpservermod.httpserver, object):
304 311
305 312 # SO_REUSEADDR has broken semantics on windows
306 313 if pycompat.iswindows:
307 314 allow_reuse_address = 0
308 315
309 316 def __init__(self, ui, app, addr, handler, **kwargs):
310 317 httpservermod.httpserver.__init__(self, addr, handler, **kwargs)
311 318 self.daemon_threads = True
312 319 self.application = app
313 320
314 321 handler.preparehttpserver(self, ui)
315 322
316 323 prefix = ui.config('web', 'prefix')
317 324 if prefix:
318 325 prefix = '/' + prefix.strip('/')
319 326 self.prefix = prefix
320 327
321 328 alog = openlog(ui.config('web', 'accesslog'), ui.fout)
322 329 elog = openlog(ui.config('web', 'errorlog'), ui.ferr)
323 330 self.accesslog = alog
324 331 self.errorlog = elog
325 332
326 333 self.addr, self.port = self.socket.getsockname()[0:2]
327 334 self.fqaddr = socket.getfqdn(addr[0])
328 335
329 336 self.serverheader = ui.config('web', 'server-header')
330 337
331 338 class IPv6HTTPServer(MercurialHTTPServer):
332 339 address_family = getattr(socket, 'AF_INET6', None)
333 340 def __init__(self, *args, **kwargs):
334 341 if self.address_family is None:
335 342 raise error.RepoError(_('IPv6 is not available on this system'))
336 343 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
337 344
338 345 def create_server(ui, app):
339 346
340 347 if ui.config('web', 'certificate'):
341 348 handler = _httprequesthandlerssl
342 349 else:
343 350 handler = _httprequesthandler
344 351
345 352 if ui.configbool('web', 'ipv6'):
346 353 cls = IPv6HTTPServer
347 354 else:
348 355 cls = MercurialHTTPServer
349 356
350 357 # ugly hack due to python issue5853 (for threaded use)
351 358 try:
352 359 import mimetypes
353 360 mimetypes.init()
354 361 except UnicodeDecodeError:
355 362 # Python 2.x's mimetypes module attempts to decode strings
356 363 # from Windows' ANSI APIs as ascii (fail), then re-encode them
357 364 # as ascii (clown fail), because the default Python Unicode
358 365 # codec is hardcoded as ascii.
359 366
360 367 sys.argv # unwrap demand-loader so that reload() works
361 368 reload(sys) # resurrect sys.setdefaultencoding()
362 369 oldenc = sys.getdefaultencoding()
363 370 sys.setdefaultencoding("latin1") # or any full 8-bit encoding
364 371 mimetypes.init()
365 372 sys.setdefaultencoding(oldenc)
366 373
367 374 address = ui.config('web', 'address')
368 375 port = util.getport(ui.config('web', 'port'))
369 376 try:
370 377 return cls(ui, app, (address, port), handler)
371 378 except socket.error as inst:
372 379 raise error.Abort(_("cannot start server at '%s:%d': %s")
373 380 % (address, port, encoding.strtolocal(inst.args[1])))
General Comments 0
You need to be logged in to leave comments. Login now