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