##// END OF EJS Templates
serve: send response headers even if response has no body...
Mads Kiilerich -
r18349:c007e5c5 default
parent child Browse files
Show More
@@ -1,320 +1,322
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 import os, sys, errno, urllib, BaseHTTPServer, socket, SocketServer, traceback
10 10 from mercurial import util, error
11 11 from mercurial.hgweb import common
12 12 from mercurial.i18n import _
13 13
14 14 def _splitURI(uri):
15 15 """Return path and query that has been split from uri
16 16
17 17 Just like CGI environment, the path is unquoted, the query is
18 18 not.
19 19 """
20 20 if '?' in uri:
21 21 path, query = uri.split('?', 1)
22 22 else:
23 23 path, query = uri, ''
24 24 return urllib.unquote(path), query
25 25
26 26 class _error_logger(object):
27 27 def __init__(self, handler):
28 28 self.handler = handler
29 29 def flush(self):
30 30 pass
31 31 def write(self, str):
32 32 self.writelines(str.split('\n'))
33 33 def writelines(self, seq):
34 34 for msg in seq:
35 35 self.handler.log_error("HG error: %s", msg)
36 36
37 37 class _httprequesthandler(BaseHTTPServer.BaseHTTPRequestHandler):
38 38
39 39 url_scheme = 'http'
40 40
41 41 @staticmethod
42 42 def preparehttpserver(httpserver, ssl_cert):
43 43 """Prepare .socket of new HTTPServer instance"""
44 44 pass
45 45
46 46 def __init__(self, *args, **kargs):
47 47 self.protocol_version = 'HTTP/1.1'
48 48 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kargs)
49 49
50 50 def _log_any(self, fp, format, *args):
51 51 fp.write("%s - - [%s] %s\n" % (self.client_address[0],
52 52 self.log_date_time_string(),
53 53 format % args))
54 54 fp.flush()
55 55
56 56 def log_error(self, format, *args):
57 57 self._log_any(self.server.errorlog, format, *args)
58 58
59 59 def log_message(self, format, *args):
60 60 self._log_any(self.server.accesslog, format, *args)
61 61
62 62 def log_request(self, code='-', size='-'):
63 63 xheaders = [h for h in self.headers.items() if h[0].startswith('x-')]
64 64 self.log_message('"%s" %s %s%s',
65 65 self.requestline, str(code), str(size),
66 66 ''.join([' %s:%s' % h for h in sorted(xheaders)]))
67 67
68 68 def do_write(self):
69 69 try:
70 70 self.do_hgweb()
71 71 except socket.error, inst:
72 72 if inst[0] != errno.EPIPE:
73 73 raise
74 74
75 75 def do_POST(self):
76 76 try:
77 77 self.do_write()
78 78 except Exception:
79 79 self._start_response("500 Internal Server Error", [])
80 80 self._write("Internal Server Error")
81 81 tb = "".join(traceback.format_exception(*sys.exc_info()))
82 82 self.log_error("Exception happened during processing "
83 83 "request '%s':\n%s", self.path, tb)
84 84
85 85 def do_GET(self):
86 86 self.do_POST()
87 87
88 88 def do_hgweb(self):
89 89 path, query = _splitURI(self.path)
90 90
91 91 env = {}
92 92 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
93 93 env['REQUEST_METHOD'] = self.command
94 94 env['SERVER_NAME'] = self.server.server_name
95 95 env['SERVER_PORT'] = str(self.server.server_port)
96 96 env['REQUEST_URI'] = self.path
97 97 env['SCRIPT_NAME'] = self.server.prefix
98 98 env['PATH_INFO'] = path[len(self.server.prefix):]
99 99 env['REMOTE_HOST'] = self.client_address[0]
100 100 env['REMOTE_ADDR'] = self.client_address[0]
101 101 if query:
102 102 env['QUERY_STRING'] = query
103 103
104 104 if self.headers.typeheader is None:
105 105 env['CONTENT_TYPE'] = self.headers.type
106 106 else:
107 107 env['CONTENT_TYPE'] = self.headers.typeheader
108 108 length = self.headers.getheader('content-length')
109 109 if length:
110 110 env['CONTENT_LENGTH'] = length
111 111 for header in [h for h in self.headers.keys()
112 112 if h not in ('content-type', 'content-length')]:
113 113 hkey = 'HTTP_' + header.replace('-', '_').upper()
114 114 hval = self.headers.getheader(header)
115 115 hval = hval.replace('\n', '').strip()
116 116 if hval:
117 117 env[hkey] = hval
118 118 env['SERVER_PROTOCOL'] = self.request_version
119 119 env['wsgi.version'] = (1, 0)
120 120 env['wsgi.url_scheme'] = self.url_scheme
121 121 if env.get('HTTP_EXPECT', '').lower() == '100-continue':
122 122 self.rfile = common.continuereader(self.rfile, self.wfile.write)
123 123
124 124 env['wsgi.input'] = self.rfile
125 125 env['wsgi.errors'] = _error_logger(self)
126 126 env['wsgi.multithread'] = isinstance(self.server,
127 127 SocketServer.ThreadingMixIn)
128 128 env['wsgi.multiprocess'] = isinstance(self.server,
129 129 SocketServer.ForkingMixIn)
130 130 env['wsgi.run_once'] = 0
131 131
132 132 self.close_connection = True
133 133 self.saved_status = None
134 134 self.saved_headers = []
135 135 self.sent_headers = False
136 136 self.length = None
137 137 for chunk in self.server.application(env, self._start_response):
138 138 self._write(chunk)
139 if not self.sent_headers:
140 self.send_headers()
139 141
140 142 def send_headers(self):
141 143 if not self.saved_status:
142 144 raise AssertionError("Sending headers before "
143 145 "start_response() called")
144 146 saved_status = self.saved_status.split(None, 1)
145 147 saved_status[0] = int(saved_status[0])
146 148 self.send_response(*saved_status)
147 149 should_close = True
148 150 for h in self.saved_headers:
149 151 self.send_header(*h)
150 152 if h[0].lower() == 'content-length':
151 153 should_close = False
152 154 self.length = int(h[1])
153 155 # The value of the Connection header is a list of case-insensitive
154 156 # tokens separated by commas and optional whitespace.
155 157 if 'close' in [token.strip().lower() for token in
156 158 self.headers.get('connection', '').split(',')]:
157 159 should_close = True
158 160 if should_close:
159 161 self.send_header('Connection', 'close')
160 162 self.close_connection = should_close
161 163 self.end_headers()
162 164 self.sent_headers = True
163 165
164 166 def _start_response(self, http_status, headers, exc_info=None):
165 167 code, msg = http_status.split(None, 1)
166 168 code = int(code)
167 169 self.saved_status = http_status
168 170 bad_headers = ('connection', 'transfer-encoding')
169 171 self.saved_headers = [h for h in headers
170 172 if h[0].lower() not in bad_headers]
171 173 return self._write
172 174
173 175 def _write(self, data):
174 176 if not self.saved_status:
175 177 raise AssertionError("data written before start_response() called")
176 178 elif not self.sent_headers:
177 179 self.send_headers()
178 180 if self.length is not None:
179 181 if len(data) > self.length:
180 182 raise AssertionError("Content-length header sent, but more "
181 183 "bytes than specified are being written.")
182 184 self.length = self.length - len(data)
183 185 self.wfile.write(data)
184 186 self.wfile.flush()
185 187
186 188 class _httprequesthandleropenssl(_httprequesthandler):
187 189 """HTTPS handler based on pyOpenSSL"""
188 190
189 191 url_scheme = 'https'
190 192
191 193 @staticmethod
192 194 def preparehttpserver(httpserver, ssl_cert):
193 195 try:
194 196 import OpenSSL
195 197 OpenSSL.SSL.Context
196 198 except ImportError:
197 199 raise util.Abort(_("SSL support is unavailable"))
198 200 ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
199 201 ctx.use_privatekey_file(ssl_cert)
200 202 ctx.use_certificate_file(ssl_cert)
201 203 sock = socket.socket(httpserver.address_family, httpserver.socket_type)
202 204 httpserver.socket = OpenSSL.SSL.Connection(ctx, sock)
203 205 httpserver.server_bind()
204 206 httpserver.server_activate()
205 207
206 208 def setup(self):
207 209 self.connection = self.request
208 210 self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
209 211 self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
210 212
211 213 def do_write(self):
212 214 import OpenSSL
213 215 try:
214 216 _httprequesthandler.do_write(self)
215 217 except OpenSSL.SSL.SysCallError, inst:
216 218 if inst.args[0] != errno.EPIPE:
217 219 raise
218 220
219 221 def handle_one_request(self):
220 222 import OpenSSL
221 223 try:
222 224 _httprequesthandler.handle_one_request(self)
223 225 except (OpenSSL.SSL.SysCallError, OpenSSL.SSL.ZeroReturnError):
224 226 self.close_connection = True
225 227 pass
226 228
227 229 class _httprequesthandlerssl(_httprequesthandler):
228 230 """HTTPS handler based on Pythons ssl module (introduced in 2.6)"""
229 231
230 232 url_scheme = 'https'
231 233
232 234 @staticmethod
233 235 def preparehttpserver(httpserver, ssl_cert):
234 236 try:
235 237 import ssl
236 238 ssl.wrap_socket
237 239 except ImportError:
238 240 raise util.Abort(_("SSL support is unavailable"))
239 241 httpserver.socket = ssl.wrap_socket(httpserver.socket, server_side=True,
240 242 certfile=ssl_cert, ssl_version=ssl.PROTOCOL_SSLv23)
241 243
242 244 def setup(self):
243 245 self.connection = self.request
244 246 self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
245 247 self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
246 248
247 249 try:
248 250 from threading import activeCount
249 251 activeCount() # silence pyflakes
250 252 _mixin = SocketServer.ThreadingMixIn
251 253 except ImportError:
252 254 if util.safehasattr(os, "fork"):
253 255 _mixin = SocketServer.ForkingMixIn
254 256 else:
255 257 class _mixin(object):
256 258 pass
257 259
258 260 def openlog(opt, default):
259 261 if opt and opt != '-':
260 262 return open(opt, 'a')
261 263 return default
262 264
263 265 class MercurialHTTPServer(object, _mixin, BaseHTTPServer.HTTPServer):
264 266
265 267 # SO_REUSEADDR has broken semantics on windows
266 268 if os.name == 'nt':
267 269 allow_reuse_address = 0
268 270
269 271 def __init__(self, ui, app, addr, handler, **kwargs):
270 272 BaseHTTPServer.HTTPServer.__init__(self, addr, handler, **kwargs)
271 273 self.daemon_threads = True
272 274 self.application = app
273 275
274 276 handler.preparehttpserver(self, ui.config('web', 'certificate'))
275 277
276 278 prefix = ui.config('web', 'prefix', '')
277 279 if prefix:
278 280 prefix = '/' + prefix.strip('/')
279 281 self.prefix = prefix
280 282
281 283 alog = openlog(ui.config('web', 'accesslog', '-'), sys.stdout)
282 284 elog = openlog(ui.config('web', 'errorlog', '-'), sys.stderr)
283 285 self.accesslog = alog
284 286 self.errorlog = elog
285 287
286 288 self.addr, self.port = self.socket.getsockname()[0:2]
287 289 self.fqaddr = socket.getfqdn(addr[0])
288 290
289 291 class IPv6HTTPServer(MercurialHTTPServer):
290 292 address_family = getattr(socket, 'AF_INET6', None)
291 293 def __init__(self, *args, **kwargs):
292 294 if self.address_family is None:
293 295 raise error.RepoError(_('IPv6 is not available on this system'))
294 296 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
295 297
296 298 def create_server(ui, app):
297 299
298 300 if ui.config('web', 'certificate'):
299 301 if sys.version_info >= (2, 6):
300 302 handler = _httprequesthandlerssl
301 303 else:
302 304 handler = _httprequesthandleropenssl
303 305 else:
304 306 handler = _httprequesthandler
305 307
306 308 if ui.configbool('web', 'ipv6'):
307 309 cls = IPv6HTTPServer
308 310 else:
309 311 cls = MercurialHTTPServer
310 312
311 313 # ugly hack due to python issue5853 (for threaded use)
312 314 import mimetypes; mimetypes.init()
313 315
314 316 address = ui.config('web', 'address', '')
315 317 port = util.getport(ui.config('web', 'port', 8000))
316 318 try:
317 319 return cls(ui, app, (address, port), handler)
318 320 except socket.error, inst:
319 321 raise util.Abort(_("cannot start server at '%s:%d': %s")
320 322 % (address, port, inst.args[1]))
General Comments 0
You need to be logged in to leave comments. Login now