##// END OF EJS Templates
hgweb: detect Python 3-era libraries and use modern attribute names...
Augie Fackler -
r34720:6e1b0569 default
parent child Browse files
Show More
@@ -1,341 +1,347 b''
1 # hgweb/server.py - The standalone hg web server.
1 # hgweb/server.py - The standalone hg web server.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import errno
11 import errno
12 import os
12 import os
13 import socket
13 import socket
14 import sys
14 import sys
15 import traceback
15 import traceback
16
16
17 from ..i18n import _
17 from ..i18n import _
18
18
19 from .. import (
19 from .. import (
20 encoding,
20 encoding,
21 error,
21 error,
22 pycompat,
22 pycompat,
23 util,
23 util,
24 )
24 )
25
25
26 httpservermod = util.httpserver
26 httpservermod = util.httpserver
27 socketserver = util.socketserver
27 socketserver = util.socketserver
28 urlerr = util.urlerr
28 urlerr = util.urlerr
29 urlreq = util.urlreq
29 urlreq = util.urlreq
30
30
31 from . import (
31 from . import (
32 common,
32 common,
33 )
33 )
34
34
35 def _splitURI(uri):
35 def _splitURI(uri):
36 """Return path and query that has been split from uri
36 """Return path and query that has been split from uri
37
37
38 Just like CGI environment, the path is unquoted, the query is
38 Just like CGI environment, the path is unquoted, the query is
39 not.
39 not.
40 """
40 """
41 if r'?' in uri:
41 if r'?' in uri:
42 path, query = uri.split(r'?', 1)
42 path, query = uri.split(r'?', 1)
43 else:
43 else:
44 path, query = uri, r''
44 path, query = uri, r''
45 return urlreq.unquote(path), query
45 return urlreq.unquote(path), query
46
46
47 class _error_logger(object):
47 class _error_logger(object):
48 def __init__(self, handler):
48 def __init__(self, handler):
49 self.handler = handler
49 self.handler = handler
50 def flush(self):
50 def flush(self):
51 pass
51 pass
52 def write(self, str):
52 def write(self, str):
53 self.writelines(str.split('\n'))
53 self.writelines(str.split('\n'))
54 def writelines(self, seq):
54 def writelines(self, seq):
55 for msg in seq:
55 for msg in seq:
56 self.handler.log_error("HG error: %s", msg)
56 self.handler.log_error("HG error: %s", msg)
57
57
58 class _httprequesthandler(httpservermod.basehttprequesthandler):
58 class _httprequesthandler(httpservermod.basehttprequesthandler):
59
59
60 url_scheme = 'http'
60 url_scheme = 'http'
61
61
62 @staticmethod
62 @staticmethod
63 def preparehttpserver(httpserver, ui):
63 def preparehttpserver(httpserver, ui):
64 """Prepare .socket of new HTTPServer instance"""
64 """Prepare .socket of new HTTPServer instance"""
65
65
66 def __init__(self, *args, **kargs):
66 def __init__(self, *args, **kargs):
67 self.protocol_version = r'HTTP/1.1'
67 self.protocol_version = r'HTTP/1.1'
68 httpservermod.basehttprequesthandler.__init__(self, *args, **kargs)
68 httpservermod.basehttprequesthandler.__init__(self, *args, **kargs)
69
69
70 def _log_any(self, fp, format, *args):
70 def _log_any(self, fp, format, *args):
71 fp.write(pycompat.sysbytes(
71 fp.write(pycompat.sysbytes(
72 r"%s - - [%s] %s" % (self.client_address[0],
72 r"%s - - [%s] %s" % (self.client_address[0],
73 self.log_date_time_string(),
73 self.log_date_time_string(),
74 format % args)) + '\n')
74 format % args)) + '\n')
75 fp.flush()
75 fp.flush()
76
76
77 def log_error(self, format, *args):
77 def log_error(self, format, *args):
78 self._log_any(self.server.errorlog, format, *args)
78 self._log_any(self.server.errorlog, format, *args)
79
79
80 def log_message(self, format, *args):
80 def log_message(self, format, *args):
81 self._log_any(self.server.accesslog, format, *args)
81 self._log_any(self.server.accesslog, format, *args)
82
82
83 def log_request(self, code=r'-', size=r'-'):
83 def log_request(self, code=r'-', size=r'-'):
84 xheaders = []
84 xheaders = []
85 if util.safehasattr(self, 'headers'):
85 if util.safehasattr(self, 'headers'):
86 xheaders = [h for h in self.headers.items()
86 xheaders = [h for h in self.headers.items()
87 if h[0].startswith(r'x-')]
87 if h[0].startswith(r'x-')]
88 self.log_message(r'"%s" %s %s%s',
88 self.log_message(r'"%s" %s %s%s',
89 self.requestline, str(code), str(size),
89 self.requestline, str(code), str(size),
90 r''.join([r' %s:%s' % h for h in sorted(xheaders)]))
90 r''.join([r' %s:%s' % h for h in sorted(xheaders)]))
91
91
92 def do_write(self):
92 def do_write(self):
93 try:
93 try:
94 self.do_hgweb()
94 self.do_hgweb()
95 except socket.error as inst:
95 except socket.error as inst:
96 if inst[0] != errno.EPIPE:
96 if inst[0] != errno.EPIPE:
97 raise
97 raise
98
98
99 def do_POST(self):
99 def do_POST(self):
100 try:
100 try:
101 self.do_write()
101 self.do_write()
102 except Exception:
102 except Exception:
103 self._start_response("500 Internal Server Error", [])
103 self._start_response("500 Internal Server Error", [])
104 self._write("Internal Server Error")
104 self._write("Internal Server Error")
105 self._done()
105 self._done()
106 tb = r"".join(traceback.format_exception(*sys.exc_info()))
106 tb = r"".join(traceback.format_exception(*sys.exc_info()))
107 # We need a native-string newline to poke in the log
107 # We need a native-string newline to poke in the log
108 # message, because we won't get a newline when using an
108 # message, because we won't get a newline when using an
109 # r-string. This is the easy way out.
109 # r-string. This is the easy way out.
110 newline = chr(10)
110 newline = chr(10)
111 self.log_error(r"Exception happened during processing "
111 self.log_error(r"Exception happened during processing "
112 r"request '%s':%s%s", self.path, newline, tb)
112 r"request '%s':%s%s", self.path, newline, tb)
113
113
114 def do_GET(self):
114 def do_GET(self):
115 self.do_POST()
115 self.do_POST()
116
116
117 def do_hgweb(self):
117 def do_hgweb(self):
118 path, query = _splitURI(self.path)
118 path, query = _splitURI(self.path)
119
119
120 env = {}
120 env = {}
121 env[r'GATEWAY_INTERFACE'] = r'CGI/1.1'
121 env[r'GATEWAY_INTERFACE'] = r'CGI/1.1'
122 env[r'REQUEST_METHOD'] = self.command
122 env[r'REQUEST_METHOD'] = self.command
123 env[r'SERVER_NAME'] = self.server.server_name
123 env[r'SERVER_NAME'] = self.server.server_name
124 env[r'SERVER_PORT'] = str(self.server.server_port)
124 env[r'SERVER_PORT'] = str(self.server.server_port)
125 env[r'REQUEST_URI'] = self.path
125 env[r'REQUEST_URI'] = self.path
126 env[r'SCRIPT_NAME'] = self.server.prefix
126 env[r'SCRIPT_NAME'] = self.server.prefix
127 env[r'PATH_INFO'] = path[len(self.server.prefix):]
127 env[r'PATH_INFO'] = path[len(self.server.prefix):]
128 env[r'REMOTE_HOST'] = self.client_address[0]
128 env[r'REMOTE_HOST'] = self.client_address[0]
129 env[r'REMOTE_ADDR'] = self.client_address[0]
129 env[r'REMOTE_ADDR'] = self.client_address[0]
130 if query:
130 if query:
131 env[r'QUERY_STRING'] = query
131 env[r'QUERY_STRING'] = query
132
132
133 if True:
133 if pycompat.ispy3:
134 if self.headers.get_content_type() is None:
135 env[r'CONTENT_TYPE'] = self.headers.get_default_type()
136 else:
137 env[r'CONTENT_TYPE'] = self.headers.get_content_type()
138 length = self.headers.get('content-length')
139 else:
134 if self.headers.typeheader is None:
140 if self.headers.typeheader is None:
135 env[r'CONTENT_TYPE'] = self.headers.type
141 env[r'CONTENT_TYPE'] = self.headers.type
136 else:
142 else:
137 env[r'CONTENT_TYPE'] = self.headers.typeheader
143 env[r'CONTENT_TYPE'] = self.headers.typeheader
138 length = self.headers.getheader('content-length')
144 length = self.headers.getheader('content-length')
139 if length:
145 if length:
140 env[r'CONTENT_LENGTH'] = length
146 env[r'CONTENT_LENGTH'] = length
141 for header in [h for h in self.headers.keys()
147 for header in [h for h in self.headers.keys()
142 if h not in ('content-type', 'content-length')]:
148 if h not in ('content-type', 'content-length')]:
143 hkey = r'HTTP_' + header.replace(r'-', r'_').upper()
149 hkey = r'HTTP_' + header.replace(r'-', r'_').upper()
144 hval = self.headers.get(header)
150 hval = self.headers.get(header)
145 hval = hval.replace(r'\n', r'').strip()
151 hval = hval.replace(r'\n', r'').strip()
146 if hval:
152 if hval:
147 env[hkey] = hval
153 env[hkey] = hval
148 env[r'SERVER_PROTOCOL'] = self.request_version
154 env[r'SERVER_PROTOCOL'] = self.request_version
149 env[r'wsgi.version'] = (1, 0)
155 env[r'wsgi.version'] = (1, 0)
150 env[r'wsgi.url_scheme'] = self.url_scheme
156 env[r'wsgi.url_scheme'] = self.url_scheme
151 if env.get(r'HTTP_EXPECT', '').lower() == '100-continue':
157 if env.get(r'HTTP_EXPECT', '').lower() == '100-continue':
152 self.rfile = common.continuereader(self.rfile, self.wfile.write)
158 self.rfile = common.continuereader(self.rfile, self.wfile.write)
153
159
154 env[r'wsgi.input'] = self.rfile
160 env[r'wsgi.input'] = self.rfile
155 env[r'wsgi.errors'] = _error_logger(self)
161 env[r'wsgi.errors'] = _error_logger(self)
156 env[r'wsgi.multithread'] = isinstance(self.server,
162 env[r'wsgi.multithread'] = isinstance(self.server,
157 socketserver.ThreadingMixIn)
163 socketserver.ThreadingMixIn)
158 env[r'wsgi.multiprocess'] = isinstance(self.server,
164 env[r'wsgi.multiprocess'] = isinstance(self.server,
159 socketserver.ForkingMixIn)
165 socketserver.ForkingMixIn)
160 env[r'wsgi.run_once'] = 0
166 env[r'wsgi.run_once'] = 0
161
167
162 self.saved_status = None
168 self.saved_status = None
163 self.saved_headers = []
169 self.saved_headers = []
164 self.sent_headers = False
170 self.sent_headers = False
165 self.length = None
171 self.length = None
166 self._chunked = None
172 self._chunked = None
167 for chunk in self.server.application(env, self._start_response):
173 for chunk in self.server.application(env, self._start_response):
168 self._write(chunk)
174 self._write(chunk)
169 if not self.sent_headers:
175 if not self.sent_headers:
170 self.send_headers()
176 self.send_headers()
171 self._done()
177 self._done()
172
178
173 def send_headers(self):
179 def send_headers(self):
174 if not self.saved_status:
180 if not self.saved_status:
175 raise AssertionError("Sending headers before "
181 raise AssertionError("Sending headers before "
176 "start_response() called")
182 "start_response() called")
177 saved_status = self.saved_status.split(None, 1)
183 saved_status = self.saved_status.split(None, 1)
178 saved_status[0] = int(saved_status[0])
184 saved_status[0] = int(saved_status[0])
179 self.send_response(*saved_status)
185 self.send_response(*saved_status)
180 self.length = None
186 self.length = None
181 self._chunked = False
187 self._chunked = False
182 for h in self.saved_headers:
188 for h in self.saved_headers:
183 self.send_header(*h)
189 self.send_header(*h)
184 if h[0].lower() == 'content-length':
190 if h[0].lower() == 'content-length':
185 self.length = int(h[1])
191 self.length = int(h[1])
186 if (self.length is None and
192 if (self.length is None and
187 saved_status[0] != common.HTTP_NOT_MODIFIED):
193 saved_status[0] != common.HTTP_NOT_MODIFIED):
188 self._chunked = (not self.close_connection and
194 self._chunked = (not self.close_connection and
189 self.request_version == "HTTP/1.1")
195 self.request_version == "HTTP/1.1")
190 if self._chunked:
196 if self._chunked:
191 self.send_header('Transfer-Encoding', 'chunked')
197 self.send_header('Transfer-Encoding', 'chunked')
192 else:
198 else:
193 self.send_header('Connection', 'close')
199 self.send_header('Connection', 'close')
194 self.end_headers()
200 self.end_headers()
195 self.sent_headers = True
201 self.sent_headers = True
196
202
197 def _start_response(self, http_status, headers, exc_info=None):
203 def _start_response(self, http_status, headers, exc_info=None):
198 code, msg = http_status.split(None, 1)
204 code, msg = http_status.split(None, 1)
199 code = int(code)
205 code = int(code)
200 self.saved_status = http_status
206 self.saved_status = http_status
201 bad_headers = ('connection', 'transfer-encoding')
207 bad_headers = ('connection', 'transfer-encoding')
202 self.saved_headers = [h for h in headers
208 self.saved_headers = [h for h in headers
203 if h[0].lower() not in bad_headers]
209 if h[0].lower() not in bad_headers]
204 return self._write
210 return self._write
205
211
206 def _write(self, data):
212 def _write(self, data):
207 if not self.saved_status:
213 if not self.saved_status:
208 raise AssertionError("data written before start_response() called")
214 raise AssertionError("data written before start_response() called")
209 elif not self.sent_headers:
215 elif not self.sent_headers:
210 self.send_headers()
216 self.send_headers()
211 if self.length is not None:
217 if self.length is not None:
212 if len(data) > self.length:
218 if len(data) > self.length:
213 raise AssertionError("Content-length header sent, but more "
219 raise AssertionError("Content-length header sent, but more "
214 "bytes than specified are being written.")
220 "bytes than specified are being written.")
215 self.length = self.length - len(data)
221 self.length = self.length - len(data)
216 elif self._chunked and data:
222 elif self._chunked and data:
217 data = '%x\r\n%s\r\n' % (len(data), data)
223 data = '%x\r\n%s\r\n' % (len(data), data)
218 self.wfile.write(data)
224 self.wfile.write(data)
219 self.wfile.flush()
225 self.wfile.flush()
220
226
221 def _done(self):
227 def _done(self):
222 if self._chunked:
228 if self._chunked:
223 self.wfile.write('0\r\n\r\n')
229 self.wfile.write('0\r\n\r\n')
224 self.wfile.flush()
230 self.wfile.flush()
225
231
226 class _httprequesthandlerssl(_httprequesthandler):
232 class _httprequesthandlerssl(_httprequesthandler):
227 """HTTPS handler based on Python's ssl module"""
233 """HTTPS handler based on Python's ssl module"""
228
234
229 url_scheme = 'https'
235 url_scheme = 'https'
230
236
231 @staticmethod
237 @staticmethod
232 def preparehttpserver(httpserver, ui):
238 def preparehttpserver(httpserver, ui):
233 try:
239 try:
234 from .. import sslutil
240 from .. import sslutil
235 sslutil.modernssl
241 sslutil.modernssl
236 except ImportError:
242 except ImportError:
237 raise error.Abort(_("SSL support is unavailable"))
243 raise error.Abort(_("SSL support is unavailable"))
238
244
239 certfile = ui.config('web', 'certificate')
245 certfile = ui.config('web', 'certificate')
240
246
241 # These config options are currently only meant for testing. Use
247 # These config options are currently only meant for testing. Use
242 # at your own risk.
248 # at your own risk.
243 cafile = ui.config('devel', 'servercafile')
249 cafile = ui.config('devel', 'servercafile')
244 reqcert = ui.configbool('devel', 'serverrequirecert')
250 reqcert = ui.configbool('devel', 'serverrequirecert')
245
251
246 httpserver.socket = sslutil.wrapserversocket(httpserver.socket,
252 httpserver.socket = sslutil.wrapserversocket(httpserver.socket,
247 ui,
253 ui,
248 certfile=certfile,
254 certfile=certfile,
249 cafile=cafile,
255 cafile=cafile,
250 requireclientcert=reqcert)
256 requireclientcert=reqcert)
251
257
252 def setup(self):
258 def setup(self):
253 self.connection = self.request
259 self.connection = self.request
254 self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
260 self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
255 self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
261 self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
256
262
257 try:
263 try:
258 import threading
264 import threading
259 threading.activeCount() # silence pyflakes and bypass demandimport
265 threading.activeCount() # silence pyflakes and bypass demandimport
260 _mixin = socketserver.ThreadingMixIn
266 _mixin = socketserver.ThreadingMixIn
261 except ImportError:
267 except ImportError:
262 if util.safehasattr(os, "fork"):
268 if util.safehasattr(os, "fork"):
263 _mixin = socketserver.ForkingMixIn
269 _mixin = socketserver.ForkingMixIn
264 else:
270 else:
265 class _mixin(object):
271 class _mixin(object):
266 pass
272 pass
267
273
268 def openlog(opt, default):
274 def openlog(opt, default):
269 if opt and opt != '-':
275 if opt and opt != '-':
270 return open(opt, 'a')
276 return open(opt, 'a')
271 return default
277 return default
272
278
273 class MercurialHTTPServer(_mixin, httpservermod.httpserver, object):
279 class MercurialHTTPServer(_mixin, httpservermod.httpserver, object):
274
280
275 # SO_REUSEADDR has broken semantics on windows
281 # SO_REUSEADDR has broken semantics on windows
276 if pycompat.iswindows:
282 if pycompat.iswindows:
277 allow_reuse_address = 0
283 allow_reuse_address = 0
278
284
279 def __init__(self, ui, app, addr, handler, **kwargs):
285 def __init__(self, ui, app, addr, handler, **kwargs):
280 httpservermod.httpserver.__init__(self, addr, handler, **kwargs)
286 httpservermod.httpserver.__init__(self, addr, handler, **kwargs)
281 self.daemon_threads = True
287 self.daemon_threads = True
282 self.application = app
288 self.application = app
283
289
284 handler.preparehttpserver(self, ui)
290 handler.preparehttpserver(self, ui)
285
291
286 prefix = ui.config('web', 'prefix')
292 prefix = ui.config('web', 'prefix')
287 if prefix:
293 if prefix:
288 prefix = '/' + prefix.strip('/')
294 prefix = '/' + prefix.strip('/')
289 self.prefix = prefix
295 self.prefix = prefix
290
296
291 alog = openlog(ui.config('web', 'accesslog'), ui.fout)
297 alog = openlog(ui.config('web', 'accesslog'), ui.fout)
292 elog = openlog(ui.config('web', 'errorlog'), ui.ferr)
298 elog = openlog(ui.config('web', 'errorlog'), ui.ferr)
293 self.accesslog = alog
299 self.accesslog = alog
294 self.errorlog = elog
300 self.errorlog = elog
295
301
296 self.addr, self.port = self.socket.getsockname()[0:2]
302 self.addr, self.port = self.socket.getsockname()[0:2]
297 self.fqaddr = socket.getfqdn(addr[0])
303 self.fqaddr = socket.getfqdn(addr[0])
298
304
299 class IPv6HTTPServer(MercurialHTTPServer):
305 class IPv6HTTPServer(MercurialHTTPServer):
300 address_family = getattr(socket, 'AF_INET6', None)
306 address_family = getattr(socket, 'AF_INET6', None)
301 def __init__(self, *args, **kwargs):
307 def __init__(self, *args, **kwargs):
302 if self.address_family is None:
308 if self.address_family is None:
303 raise error.RepoError(_('IPv6 is not available on this system'))
309 raise error.RepoError(_('IPv6 is not available on this system'))
304 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
310 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
305
311
306 def create_server(ui, app):
312 def create_server(ui, app):
307
313
308 if ui.config('web', 'certificate'):
314 if ui.config('web', 'certificate'):
309 handler = _httprequesthandlerssl
315 handler = _httprequesthandlerssl
310 else:
316 else:
311 handler = _httprequesthandler
317 handler = _httprequesthandler
312
318
313 if ui.configbool('web', 'ipv6'):
319 if ui.configbool('web', 'ipv6'):
314 cls = IPv6HTTPServer
320 cls = IPv6HTTPServer
315 else:
321 else:
316 cls = MercurialHTTPServer
322 cls = MercurialHTTPServer
317
323
318 # ugly hack due to python issue5853 (for threaded use)
324 # ugly hack due to python issue5853 (for threaded use)
319 try:
325 try:
320 import mimetypes
326 import mimetypes
321 mimetypes.init()
327 mimetypes.init()
322 except UnicodeDecodeError:
328 except UnicodeDecodeError:
323 # Python 2.x's mimetypes module attempts to decode strings
329 # Python 2.x's mimetypes module attempts to decode strings
324 # from Windows' ANSI APIs as ascii (fail), then re-encode them
330 # from Windows' ANSI APIs as ascii (fail), then re-encode them
325 # as ascii (clown fail), because the default Python Unicode
331 # as ascii (clown fail), because the default Python Unicode
326 # codec is hardcoded as ascii.
332 # codec is hardcoded as ascii.
327
333
328 sys.argv # unwrap demand-loader so that reload() works
334 sys.argv # unwrap demand-loader so that reload() works
329 reload(sys) # resurrect sys.setdefaultencoding()
335 reload(sys) # resurrect sys.setdefaultencoding()
330 oldenc = sys.getdefaultencoding()
336 oldenc = sys.getdefaultencoding()
331 sys.setdefaultencoding("latin1") # or any full 8-bit encoding
337 sys.setdefaultencoding("latin1") # or any full 8-bit encoding
332 mimetypes.init()
338 mimetypes.init()
333 sys.setdefaultencoding(oldenc)
339 sys.setdefaultencoding(oldenc)
334
340
335 address = ui.config('web', 'address')
341 address = ui.config('web', 'address')
336 port = util.getport(ui.config('web', 'port'))
342 port = util.getport(ui.config('web', 'port'))
337 try:
343 try:
338 return cls(ui, app, (address, port), handler)
344 return cls(ui, app, (address, port), handler)
339 except socket.error as inst:
345 except socket.error as inst:
340 raise error.Abort(_("cannot start server at '%s:%d': %s")
346 raise error.Abort(_("cannot start server at '%s:%d': %s")
341 % (address, port, encoding.strtolocal(inst.args[1])))
347 % (address, port, encoding.strtolocal(inst.args[1])))
General Comments 0
You need to be logged in to leave comments. Login now