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