##// END OF EJS Templates
py3: ensure _start_response() is called with system string...
Gregory Szorc -
r39852:a6088d10 default
parent child Browse files
Show More
@@ -1,369 +1,369 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 self._start_response("500 Internal Server Error", [])
105 self._write("Internal Server Error")
104 self._start_response(r"500 Internal Server Error", [])
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 177 env[r'wsgi.multiprocess'] = isinstance(self.server,
178 178 socketserver.ForkingMixIn)
179 179 env[r'wsgi.run_once'] = 0
180 180
181 181 wsgiref.validate.check_environ(env)
182 182
183 183 self.saved_status = None
184 184 self.saved_headers = []
185 185 self.length = None
186 186 self._chunked = None
187 187 for chunk in self.server.application(env, self._start_response):
188 188 self._write(chunk)
189 189 if not self.sent_headers:
190 190 self.send_headers()
191 191 self._done()
192 192
193 193 def send_headers(self):
194 194 if not self.saved_status:
195 195 raise AssertionError("Sending headers before "
196 196 "start_response() called")
197 197 saved_status = self.saved_status.split(None, 1)
198 198 saved_status[0] = int(saved_status[0])
199 199 self.send_response(*saved_status)
200 200 self.length = None
201 201 self._chunked = False
202 202 for h in self.saved_headers:
203 203 self.send_header(*h)
204 204 if h[0].lower() == 'content-length':
205 205 self.length = int(h[1])
206 206 if (self.length is None and
207 207 saved_status[0] != common.HTTP_NOT_MODIFIED):
208 208 self._chunked = (not self.close_connection and
209 209 self.request_version == "HTTP/1.1")
210 210 if self._chunked:
211 211 self.send_header(r'Transfer-Encoding', r'chunked')
212 212 else:
213 213 self.send_header(r'Connection', r'close')
214 214 self.end_headers()
215 215 self.sent_headers = True
216 216
217 217 def _start_response(self, http_status, headers, exc_info=None):
218 218 assert isinstance(http_status, str)
219 219 code, msg = http_status.split(None, 1)
220 220 code = int(code)
221 221 self.saved_status = http_status
222 222 bad_headers = ('connection', 'transfer-encoding')
223 223 self.saved_headers = [h for h in headers
224 224 if h[0].lower() not in bad_headers]
225 225 return self._write
226 226
227 227 def _write(self, data):
228 228 if not self.saved_status:
229 229 raise AssertionError("data written before start_response() called")
230 230 elif not self.sent_headers:
231 231 self.send_headers()
232 232 if self.length is not None:
233 233 if len(data) > self.length:
234 234 raise AssertionError("Content-length header sent, but more "
235 235 "bytes than specified are being written.")
236 236 self.length = self.length - len(data)
237 237 elif self._chunked and data:
238 238 data = '%x\r\n%s\r\n' % (len(data), data)
239 239 self.wfile.write(data)
240 240 self.wfile.flush()
241 241
242 242 def _done(self):
243 243 if self._chunked:
244 244 self.wfile.write('0\r\n\r\n')
245 245 self.wfile.flush()
246 246
247 247 def version_string(self):
248 248 if self.server.serverheader:
249 249 return encoding.strfromlocal(self.server.serverheader)
250 250 return httpservermod.basehttprequesthandler.version_string(self)
251 251
252 252 class _httprequesthandlerssl(_httprequesthandler):
253 253 """HTTPS handler based on Python's ssl module"""
254 254
255 255 url_scheme = 'https'
256 256
257 257 @staticmethod
258 258 def preparehttpserver(httpserver, ui):
259 259 try:
260 260 from .. import sslutil
261 261 sslutil.modernssl
262 262 except ImportError:
263 263 raise error.Abort(_("SSL support is unavailable"))
264 264
265 265 certfile = ui.config('web', 'certificate')
266 266
267 267 # These config options are currently only meant for testing. Use
268 268 # at your own risk.
269 269 cafile = ui.config('devel', 'servercafile')
270 270 reqcert = ui.configbool('devel', 'serverrequirecert')
271 271
272 272 httpserver.socket = sslutil.wrapserversocket(httpserver.socket,
273 273 ui,
274 274 certfile=certfile,
275 275 cafile=cafile,
276 276 requireclientcert=reqcert)
277 277
278 278 def setup(self):
279 279 self.connection = self.request
280 280 self.rfile = self.request.makefile(r"rb", self.rbufsize)
281 281 self.wfile = self.request.makefile(r"wb", self.wbufsize)
282 282
283 283 try:
284 284 import threading
285 285 threading.activeCount() # silence pyflakes and bypass demandimport
286 286 _mixin = socketserver.ThreadingMixIn
287 287 except ImportError:
288 288 if util.safehasattr(os, "fork"):
289 289 _mixin = socketserver.ForkingMixIn
290 290 else:
291 291 class _mixin(object):
292 292 pass
293 293
294 294 def openlog(opt, default):
295 295 if opt and opt != '-':
296 296 return open(opt, 'ab')
297 297 return default
298 298
299 299 class MercurialHTTPServer(_mixin, httpservermod.httpserver, object):
300 300
301 301 # SO_REUSEADDR has broken semantics on windows
302 302 if pycompat.iswindows:
303 303 allow_reuse_address = 0
304 304
305 305 def __init__(self, ui, app, addr, handler, **kwargs):
306 306 httpservermod.httpserver.__init__(self, addr, handler, **kwargs)
307 307 self.daemon_threads = True
308 308 self.application = app
309 309
310 310 handler.preparehttpserver(self, ui)
311 311
312 312 prefix = ui.config('web', 'prefix')
313 313 if prefix:
314 314 prefix = '/' + prefix.strip('/')
315 315 self.prefix = prefix
316 316
317 317 alog = openlog(ui.config('web', 'accesslog'), ui.fout)
318 318 elog = openlog(ui.config('web', 'errorlog'), ui.ferr)
319 319 self.accesslog = alog
320 320 self.errorlog = elog
321 321
322 322 self.addr, self.port = self.socket.getsockname()[0:2]
323 323 self.fqaddr = socket.getfqdn(addr[0])
324 324
325 325 self.serverheader = ui.config('web', 'server-header')
326 326
327 327 class IPv6HTTPServer(MercurialHTTPServer):
328 328 address_family = getattr(socket, 'AF_INET6', None)
329 329 def __init__(self, *args, **kwargs):
330 330 if self.address_family is None:
331 331 raise error.RepoError(_('IPv6 is not available on this system'))
332 332 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
333 333
334 334 def create_server(ui, app):
335 335
336 336 if ui.config('web', 'certificate'):
337 337 handler = _httprequesthandlerssl
338 338 else:
339 339 handler = _httprequesthandler
340 340
341 341 if ui.configbool('web', 'ipv6'):
342 342 cls = IPv6HTTPServer
343 343 else:
344 344 cls = MercurialHTTPServer
345 345
346 346 # ugly hack due to python issue5853 (for threaded use)
347 347 try:
348 348 import mimetypes
349 349 mimetypes.init()
350 350 except UnicodeDecodeError:
351 351 # Python 2.x's mimetypes module attempts to decode strings
352 352 # from Windows' ANSI APIs as ascii (fail), then re-encode them
353 353 # as ascii (clown fail), because the default Python Unicode
354 354 # codec is hardcoded as ascii.
355 355
356 356 sys.argv # unwrap demand-loader so that reload() works
357 357 reload(sys) # resurrect sys.setdefaultencoding()
358 358 oldenc = sys.getdefaultencoding()
359 359 sys.setdefaultencoding("latin1") # or any full 8-bit encoding
360 360 mimetypes.init()
361 361 sys.setdefaultencoding(oldenc)
362 362
363 363 address = ui.config('web', 'address')
364 364 port = util.getport(ui.config('web', 'port'))
365 365 try:
366 366 return cls(ui, app, (address, port), handler)
367 367 except socket.error as inst:
368 368 raise error.Abort(_("cannot start server at '%s:%d': %s")
369 369 % (address, port, encoding.strtolocal(inst.args[1])))
General Comments 0
You need to be logged in to leave comments. Login now