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