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