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