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