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