Reset tempfile module after fork where useful
[ganeti-local] / lib / http / server.py
1 #
2 #
3
4 # Copyright (C) 2007, 2008 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21 """HTTP server module.
22
23 """
24
25 import BaseHTTPServer
26 import cgi
27 import logging
28 import os
29 import socket
30 import time
31 import signal
32 import asyncore
33
34 from ganeti import http
35 from ganeti import utils
36
37
38 WEEKDAYNAME = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
39 MONTHNAME = [None,
40              'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
41              'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
42
43 # Default error message
44 DEFAULT_ERROR_CONTENT_TYPE = "text/html"
45 DEFAULT_ERROR_MESSAGE = """\
46 <html>
47 <head>
48 <title>Error response</title>
49 </head>
50 <body>
51 <h1>Error response</h1>
52 <p>Error code %(code)d.
53 <p>Message: %(message)s.
54 <p>Error code explanation: %(code)s = %(explain)s.
55 </body>
56 </html>
57 """
58
59
60 def _DateTimeHeader(gmnow=None):
61   """Return the current date and time formatted for a message header.
62
63   The time MUST be in the GMT timezone.
64
65   """
66   if gmnow is None:
67     gmnow = time.gmtime()
68   (year, month, day, hh, mm, ss, wd, _, _) = gmnow
69   return ("%s, %02d %3s %4d %02d:%02d:%02d GMT" %
70           (WEEKDAYNAME[wd], day, MONTHNAME[month], year, hh, mm, ss))
71
72
73 class _HttpServerRequest(object):
74   """Data structure for HTTP request on server side.
75
76   """
77   def __init__(self, request_msg):
78     # Request attributes
79     self.request_method = request_msg.start_line.method
80     self.request_path = request_msg.start_line.path
81     self.request_headers = request_msg.headers
82     self.request_body = request_msg.decoded_body
83
84     # Response attributes
85     self.resp_headers = {}
86
87     # Private data for request handler (useful in combination with
88     # authentication)
89     self.private = None
90
91
92 class _HttpServerToClientMessageWriter(http.HttpMessageWriter):
93   """Writes an HTTP response to client.
94
95   """
96   def __init__(self, sock, request_msg, response_msg, write_timeout):
97     """Writes the response to the client.
98
99     @type sock: socket
100     @param sock: Target socket
101     @type request_msg: http.HttpMessage
102     @param request_msg: Request message, required to determine whether
103         response may have a message body
104     @type response_msg: http.HttpMessage
105     @param response_msg: Response message
106     @type write_timeout: float
107     @param write_timeout: Write timeout for socket
108
109     """
110     self._request_msg = request_msg
111     self._response_msg = response_msg
112     http.HttpMessageWriter.__init__(self, sock, response_msg, write_timeout)
113
114   def HasMessageBody(self):
115     """Logic to detect whether response should contain a message body.
116
117     """
118     if self._request_msg.start_line:
119       request_method = self._request_msg.start_line.method
120     else:
121       request_method = None
122
123     response_code = self._response_msg.start_line.code
124
125     # RFC2616, section 4.3: "A message-body MUST NOT be included in a request
126     # if the specification of the request method (section 5.1.1) does not allow
127     # sending an entity-body in requests"
128     #
129     # RFC2616, section 9.4: "The HEAD method is identical to GET except that
130     # the server MUST NOT return a message-body in the response."
131     #
132     # RFC2616, section 10.2.5: "The 204 response MUST NOT include a
133     # message-body [...]"
134     #
135     # RFC2616, section 10.3.5: "The 304 response MUST NOT contain a
136     # message-body, [...]"
137
138     return (http.HttpMessageWriter.HasMessageBody(self) and
139             (request_method is not None and
140              request_method != http.HTTP_HEAD) and
141             response_code >= http.HTTP_OK and
142             response_code not in (http.HTTP_NO_CONTENT,
143                                   http.HTTP_NOT_MODIFIED))
144
145
146 class _HttpClientToServerMessageReader(http.HttpMessageReader):
147   """Reads an HTTP request sent by client.
148
149   """
150   # Length limits
151   START_LINE_LENGTH_MAX = 4096
152   HEADER_LENGTH_MAX = 4096
153
154   def ParseStartLine(self, start_line):
155     """Parses the start line sent by client.
156
157     Example: "GET /index.html HTTP/1.1"
158
159     @type start_line: string
160     @param start_line: Start line
161
162     """
163     # Empty lines are skipped when reading
164     assert start_line
165
166     logging.debug("HTTP request: %s", start_line)
167
168     words = start_line.split()
169
170     if len(words) == 3:
171       [method, path, version] = words
172       if version[:5] != 'HTTP/':
173         raise http.HttpBadRequest("Bad request version (%r)" % version)
174
175       try:
176         base_version_number = version.split("/", 1)[1]
177         version_number = base_version_number.split(".")
178
179         # RFC 2145 section 3.1 says there can be only one "." and
180         #   - major and minor numbers MUST be treated as
181         #      separate integers;
182         #   - HTTP/2.4 is a lower version than HTTP/2.13, which in
183         #      turn is lower than HTTP/12.3;
184         #   - Leading zeros MUST be ignored by recipients.
185         if len(version_number) != 2:
186           raise http.HttpBadRequest("Bad request version (%r)" % version)
187
188         version_number = (int(version_number[0]), int(version_number[1]))
189       except (ValueError, IndexError):
190         raise http.HttpBadRequest("Bad request version (%r)" % version)
191
192       if version_number >= (2, 0):
193         raise http.HttpVersionNotSupported("Invalid HTTP Version (%s)" %
194                                       base_version_number)
195
196     elif len(words) == 2:
197       version = http.HTTP_0_9
198       [method, path] = words
199       if method != http.HTTP_GET:
200         raise http.HttpBadRequest("Bad HTTP/0.9 request type (%r)" % method)
201
202     else:
203       raise http.HttpBadRequest("Bad request syntax (%r)" % start_line)
204
205     return http.HttpClientToServerStartLine(method, path, version)
206
207
208 class HttpServerRequestExecutor(object):
209   """Implements server side of HTTP.
210
211   This class implements the server side of HTTP. It's based on code of
212   Python's BaseHTTPServer, from both version 2.4 and 3k. It does not
213   support non-ASCII character encodings. Keep-alive connections are
214   not supported.
215
216   """
217   # The default request version.  This only affects responses up until
218   # the point where the request line is parsed, so it mainly decides what
219   # the client gets back when sending a malformed request line.
220   # Most web servers default to HTTP 0.9, i.e. don't send a status line.
221   default_request_version = http.HTTP_0_9
222
223   # Error message settings
224   error_message_format = DEFAULT_ERROR_MESSAGE
225   error_content_type = DEFAULT_ERROR_CONTENT_TYPE
226
227   responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
228
229   # Timeouts in seconds for socket layer
230   WRITE_TIMEOUT = 10
231   READ_TIMEOUT = 10
232   CLOSE_TIMEOUT = 1
233
234   def __init__(self, server, sock, client_addr):
235     """Initializes this class.
236
237     """
238     self.server = server
239     self.sock = sock
240     self.client_addr = client_addr
241
242     self.request_msg = http.HttpMessage()
243     self.response_msg = http.HttpMessage()
244
245     self.response_msg.start_line = \
246       http.HttpServerToClientStartLine(version=self.default_request_version,
247                                        code=None, reason=None)
248
249     # Disable Python's timeout
250     self.sock.settimeout(None)
251
252     # Operate in non-blocking mode
253     self.sock.setblocking(0)
254
255     logging.debug("Connection from %s:%s", client_addr[0], client_addr[1])
256     try:
257       request_msg_reader = None
258       force_close = True
259       try:
260         # Do the secret SSL handshake
261         if self.server.using_ssl:
262           self.sock.set_accept_state()
263           try:
264             http.Handshake(self.sock, self.WRITE_TIMEOUT)
265           except http.HttpSessionHandshakeUnexpectedEOF:
266             # Ignore rest
267             return
268
269         try:
270           try:
271             request_msg_reader = self._ReadRequest()
272             self._HandleRequest()
273
274             # Only wait for client to close if we didn't have any exception.
275             force_close = False
276           except http.HttpException, err:
277             self._SetErrorStatus(err)
278         finally:
279           # Try to send a response
280           self._SendResponse()
281       finally:
282         http.ShutdownConnection(sock, self.CLOSE_TIMEOUT, self.WRITE_TIMEOUT,
283                                 request_msg_reader, force_close)
284
285       self.sock.close()
286       self.sock = None
287     finally:
288       logging.debug("Disconnected %s:%s", client_addr[0], client_addr[1])
289
290   def _ReadRequest(self):
291     """Reads a request sent by client.
292
293     """
294     try:
295       request_msg_reader = \
296         _HttpClientToServerMessageReader(self.sock, self.request_msg,
297                                          self.READ_TIMEOUT)
298     except http.HttpSocketTimeout:
299       raise http.HttpError("Timeout while reading request")
300     except socket.error, err:
301       raise http.HttpError("Error reading request: %s" % err)
302
303     self.response_msg.start_line.version = self.request_msg.start_line.version
304
305     return request_msg_reader
306
307   def _HandleRequest(self):
308     """Calls the handler function for the current request.
309
310     """
311     handler_context = _HttpServerRequest(self.request_msg)
312
313     try:
314       try:
315         # Authentication, etc.
316         self.server.PreHandleRequest(handler_context)
317
318         # Call actual request handler
319         result = self.server.HandleRequest(handler_context)
320       except (http.HttpException, KeyboardInterrupt, SystemExit):
321         raise
322       except Exception, err:
323         logging.exception("Caught exception")
324         raise http.HttpInternalServerError(message=str(err))
325       except:
326         logging.exception("Unknown exception")
327         raise http.HttpInternalServerError(message="Unknown error")
328
329       # TODO: Content-type
330       encoder = http.HttpJsonConverter()
331       self.response_msg.start_line.code = http.HTTP_OK
332       self.response_msg.body = encoder.Encode(result)
333       self.response_msg.headers = handler_context.resp_headers
334       self.response_msg.headers[http.HTTP_CONTENT_TYPE] = encoder.CONTENT_TYPE
335     finally:
336       # No reason to keep this any longer, even for exceptions
337       handler_context.private = None
338
339   def _SendResponse(self):
340     """Sends the response to the client.
341
342     """
343     if self.response_msg.start_line.code is None:
344       return
345
346     if not self.response_msg.headers:
347       self.response_msg.headers = {}
348
349     self.response_msg.headers.update({
350       # TODO: Keep-alive is not supported
351       http.HTTP_CONNECTION: "close",
352       http.HTTP_DATE: _DateTimeHeader(),
353       http.HTTP_SERVER: http.HTTP_GANETI_VERSION,
354       })
355
356     # Get response reason based on code
357     response_code = self.response_msg.start_line.code
358     if response_code in self.responses:
359       response_reason = self.responses[response_code][0]
360     else:
361       response_reason = ""
362     self.response_msg.start_line.reason = response_reason
363
364     logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1],
365                  self.request_msg.start_line, response_code)
366
367     try:
368       _HttpServerToClientMessageWriter(self.sock, self.request_msg,
369                                        self.response_msg, self.WRITE_TIMEOUT)
370     except http.HttpSocketTimeout:
371       raise http.HttpError("Timeout while sending response")
372     except socket.error, err:
373       raise http.HttpError("Error sending response: %s" % err)
374
375   def _SetErrorStatus(self, err):
376     """Sets the response code and body from a HttpException.
377
378     @type err: HttpException
379     @param err: Exception instance
380
381     """
382     try:
383       (shortmsg, longmsg) = self.responses[err.code]
384     except KeyError:
385       shortmsg = longmsg = "Unknown"
386
387     if err.message:
388       message = err.message
389     else:
390       message = shortmsg
391
392     values = {
393       "code": err.code,
394       "message": cgi.escape(message),
395       "explain": longmsg,
396       }
397
398     self.response_msg.start_line.code = err.code
399
400     headers = {}
401     if err.headers:
402       headers.update(err.headers)
403     headers[http.HTTP_CONTENT_TYPE] = self.error_content_type
404     self.response_msg.headers = headers
405
406     self.response_msg.body = self._FormatErrorMessage(values)
407
408   def _FormatErrorMessage(self, values):
409     """Formats the body of an error message.
410
411     @type values: dict
412     @param values: dictionary with keys code, message and explain.
413     @rtype: string
414     @return: the body of the message
415
416     """
417     return self.error_message_format % values
418
419 class HttpServer(http.HttpBase, asyncore.dispatcher):
420   """Generic HTTP server class
421
422   Users of this class must subclass it and override the HandleRequest function.
423
424   """
425   MAX_CHILDREN = 20
426
427   def __init__(self, mainloop, local_address, port,
428                ssl_params=None, ssl_verify_peer=False,
429                request_executor_class=None):
430     """Initializes the HTTP server
431
432     @type mainloop: ganeti.daemon.Mainloop
433     @param mainloop: Mainloop used to poll for I/O events
434     @type local_address: string
435     @param local_address: Local IP address to bind to
436     @type port: int
437     @param port: TCP port to listen on
438     @type ssl_params: HttpSslParams
439     @param ssl_params: SSL key and certificate
440     @type ssl_verify_peer: bool
441     @param ssl_verify_peer: Whether to require client certificate
442         and compare it with our certificate
443     @type request_executor_class: class
444     @param request_executor_class: an class derived from the
445         HttpServerRequestExecutor class
446
447     """
448     http.HttpBase.__init__(self)
449     asyncore.dispatcher.__init__(self)
450
451     if request_executor_class is None:
452       self.request_executor = HttpServerRequestExecutor
453     else:
454       self.request_executor = request_executor_class
455
456     self.mainloop = mainloop
457     self.local_address = local_address
458     self.port = port
459
460     self.socket = self._CreateSocket(ssl_params, ssl_verify_peer)
461
462     # Allow port to be reused
463     self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
464
465     self._children = []
466     self.set_socket(self.socket)
467     self.accepting = True
468     mainloop.RegisterSignal(self)
469
470   def Start(self):
471     self.socket.bind((self.local_address, self.port))
472     self.socket.listen(1024)
473
474   def Stop(self):
475     self.socket.close()
476
477   def handle_accept(self):
478     self._IncomingConnection()
479
480   def OnSignal(self, signum):
481     if signum == signal.SIGCHLD:
482       self._CollectChildren(True)
483
484   def _CollectChildren(self, quick):
485     """Checks whether any child processes are done
486
487     @type quick: bool
488     @param quick: Whether to only use non-blocking functions
489
490     """
491     if not quick:
492       # Don't wait for other processes if it should be a quick check
493       while len(self._children) > self.MAX_CHILDREN:
494         try:
495           # Waiting without a timeout brings us into a potential DoS situation.
496           # As soon as too many children run, we'll not respond to new
497           # requests. The real solution would be to add a timeout for children
498           # and killing them after some time.
499           pid, _ = os.waitpid(0, 0)
500         except os.error:
501           pid = None
502         if pid and pid in self._children:
503           self._children.remove(pid)
504
505     for child in self._children:
506       try:
507         pid, _ = os.waitpid(child, os.WNOHANG)
508       except os.error:
509         pid = None
510       if pid and pid in self._children:
511         self._children.remove(pid)
512
513   def _IncomingConnection(self):
514     """Called for each incoming connection
515
516     """
517     # pylint: disable-msg=W0212
518     (connection, client_addr) = self.socket.accept()
519
520     self._CollectChildren(False)
521
522     pid = os.fork()
523     if pid == 0:
524       # Child process
525       try:
526         # The client shouldn't keep the listening socket open. If the parent
527         # process is restarted, it would fail when there's already something
528         # listening (in this case its own child from a previous run) on the
529         # same port.
530         try:
531           self.socket.close()
532         except socket.error:
533           pass
534         self.socket = None
535
536         # In case the handler code uses temporary files
537         utils.ResetTempfileModule()
538
539         self.request_executor(self, connection, client_addr)
540       except Exception: # pylint: disable-msg=W0703
541         logging.exception("Error while handling request from %s:%s",
542                           client_addr[0], client_addr[1])
543         os._exit(1)
544       os._exit(0)
545     else:
546       self._children.append(pid)
547
548   def PreHandleRequest(self, req):
549     """Called before handling a request.
550
551     Can be overridden by a subclass.
552
553     """
554
555   def HandleRequest(self, req):
556     """Handles a request.
557
558     Must be overridden by subclass.
559
560     """
561     raise NotImplementedError()