http.server: Improve request logging in debug mode
[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, method, path, headers, body):
78     # Request attributes
79     self.request_method = method
80     self.request_path = path
81     self.request_headers = headers
82     self.request_body = 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   def __repr__(self):
92     status = ["%s.%s" % (self.__class__.__module__, self.__class__.__name__),
93               self.request_method, self.request_path,
94               "headers=%r" % str(self.request_headers),
95               "body=%r" % (self.request_body, )]
96
97     return "<%s at %#x>" % (" ".join(status), id(self))
98
99
100 class _HttpServerToClientMessageWriter(http.HttpMessageWriter):
101   """Writes an HTTP response to client.
102
103   """
104   def __init__(self, sock, request_msg, response_msg, write_timeout):
105     """Writes the response to the client.
106
107     @type sock: socket
108     @param sock: Target socket
109     @type request_msg: http.HttpMessage
110     @param request_msg: Request message, required to determine whether
111         response may have a message body
112     @type response_msg: http.HttpMessage
113     @param response_msg: Response message
114     @type write_timeout: float
115     @param write_timeout: Write timeout for socket
116
117     """
118     self._request_msg = request_msg
119     self._response_msg = response_msg
120     http.HttpMessageWriter.__init__(self, sock, response_msg, write_timeout)
121
122   def HasMessageBody(self):
123     """Logic to detect whether response should contain a message body.
124
125     """
126     if self._request_msg.start_line:
127       request_method = self._request_msg.start_line.method
128     else:
129       request_method = None
130
131     response_code = self._response_msg.start_line.code
132
133     # RFC2616, section 4.3: "A message-body MUST NOT be included in a request
134     # if the specification of the request method (section 5.1.1) does not allow
135     # sending an entity-body in requests"
136     #
137     # RFC2616, section 9.4: "The HEAD method is identical to GET except that
138     # the server MUST NOT return a message-body in the response."
139     #
140     # RFC2616, section 10.2.5: "The 204 response MUST NOT include a
141     # message-body [...]"
142     #
143     # RFC2616, section 10.3.5: "The 304 response MUST NOT contain a
144     # message-body, [...]"
145
146     return (http.HttpMessageWriter.HasMessageBody(self) and
147             (request_method is not None and
148              request_method != http.HTTP_HEAD) and
149             response_code >= http.HTTP_OK and
150             response_code not in (http.HTTP_NO_CONTENT,
151                                   http.HTTP_NOT_MODIFIED))
152
153
154 class _HttpClientToServerMessageReader(http.HttpMessageReader):
155   """Reads an HTTP request sent by client.
156
157   """
158   # Length limits
159   START_LINE_LENGTH_MAX = 4096
160   HEADER_LENGTH_MAX = 4096
161
162   def ParseStartLine(self, start_line):
163     """Parses the start line sent by client.
164
165     Example: "GET /index.html HTTP/1.1"
166
167     @type start_line: string
168     @param start_line: Start line
169
170     """
171     # Empty lines are skipped when reading
172     assert start_line
173
174     logging.debug("HTTP request: %s", start_line)
175
176     words = start_line.split()
177
178     if len(words) == 3:
179       [method, path, version] = words
180       if version[:5] != 'HTTP/':
181         raise http.HttpBadRequest("Bad request version (%r)" % version)
182
183       try:
184         base_version_number = version.split("/", 1)[1]
185         version_number = base_version_number.split(".")
186
187         # RFC 2145 section 3.1 says there can be only one "." and
188         #   - major and minor numbers MUST be treated as
189         #      separate integers;
190         #   - HTTP/2.4 is a lower version than HTTP/2.13, which in
191         #      turn is lower than HTTP/12.3;
192         #   - Leading zeros MUST be ignored by recipients.
193         if len(version_number) != 2:
194           raise http.HttpBadRequest("Bad request version (%r)" % version)
195
196         version_number = (int(version_number[0]), int(version_number[1]))
197       except (ValueError, IndexError):
198         raise http.HttpBadRequest("Bad request version (%r)" % version)
199
200       if version_number >= (2, 0):
201         raise http.HttpVersionNotSupported("Invalid HTTP Version (%s)" %
202                                       base_version_number)
203
204     elif len(words) == 2:
205       version = http.HTTP_0_9
206       [method, path] = words
207       if method != http.HTTP_GET:
208         raise http.HttpBadRequest("Bad HTTP/0.9 request type (%r)" % method)
209
210     else:
211       raise http.HttpBadRequest("Bad request syntax (%r)" % start_line)
212
213     return http.HttpClientToServerStartLine(method, path, version)
214
215
216 class HttpServerRequestExecutor(object):
217   """Implements server side of HTTP.
218
219   This class implements the server side of HTTP. It's based on code of
220   Python's BaseHTTPServer, from both version 2.4 and 3k. It does not
221   support non-ASCII character encodings. Keep-alive connections are
222   not supported.
223
224   """
225   # The default request version.  This only affects responses up until
226   # the point where the request line is parsed, so it mainly decides what
227   # the client gets back when sending a malformed request line.
228   # Most web servers default to HTTP 0.9, i.e. don't send a status line.
229   default_request_version = http.HTTP_0_9
230
231   # Error message settings
232   error_message_format = DEFAULT_ERROR_MESSAGE
233   error_content_type = DEFAULT_ERROR_CONTENT_TYPE
234
235   responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
236
237   # Timeouts in seconds for socket layer
238   WRITE_TIMEOUT = 10
239   READ_TIMEOUT = 10
240   CLOSE_TIMEOUT = 1
241
242   def __init__(self, server, sock, client_addr):
243     """Initializes this class.
244
245     """
246     self.server = server
247     self.sock = sock
248     self.client_addr = client_addr
249
250     self.request_msg = http.HttpMessage()
251     self.response_msg = http.HttpMessage()
252
253     self.response_msg.start_line = \
254       http.HttpServerToClientStartLine(version=self.default_request_version,
255                                        code=None, reason=None)
256
257     # Disable Python's timeout
258     self.sock.settimeout(None)
259
260     # Operate in non-blocking mode
261     self.sock.setblocking(0)
262
263     logging.debug("Connection from %s:%s", client_addr[0], client_addr[1])
264     try:
265       request_msg_reader = None
266       force_close = True
267       try:
268         # Do the secret SSL handshake
269         if self.server.using_ssl:
270           self.sock.set_accept_state()
271           try:
272             http.Handshake(self.sock, self.WRITE_TIMEOUT)
273           except http.HttpSessionHandshakeUnexpectedEOF:
274             # Ignore rest
275             return
276
277         try:
278           try:
279             request_msg_reader = self._ReadRequest()
280             self._HandleRequest()
281
282             # Only wait for client to close if we didn't have any exception.
283             force_close = False
284           except http.HttpException, err:
285             self._SetErrorStatus(err)
286         finally:
287           # Try to send a response
288           self._SendResponse()
289       finally:
290         http.ShutdownConnection(sock, self.CLOSE_TIMEOUT, self.WRITE_TIMEOUT,
291                                 request_msg_reader, force_close)
292
293       self.sock.close()
294       self.sock = None
295     finally:
296       logging.debug("Disconnected %s:%s", client_addr[0], client_addr[1])
297
298   def _ReadRequest(self):
299     """Reads a request sent by client.
300
301     """
302     try:
303       request_msg_reader = \
304         _HttpClientToServerMessageReader(self.sock, self.request_msg,
305                                          self.READ_TIMEOUT)
306     except http.HttpSocketTimeout:
307       raise http.HttpError("Timeout while reading request")
308     except socket.error, err:
309       raise http.HttpError("Error reading request: %s" % err)
310
311     self.response_msg.start_line.version = self.request_msg.start_line.version
312
313     return request_msg_reader
314
315   def _HandleRequest(self):
316     """Calls the handler function for the current request.
317
318     """
319     handler_context = _HttpServerRequest(self.request_msg.start_line.method,
320                                          self.request_msg.start_line.path,
321                                          self.request_msg.headers,
322                                          self.request_msg.decoded_body)
323
324     logging.debug("Handling request %r", handler_context)
325
326     try:
327       try:
328         # Authentication, etc.
329         self.server.PreHandleRequest(handler_context)
330
331         # Call actual request handler
332         result = self.server.HandleRequest(handler_context)
333       except (http.HttpException, KeyboardInterrupt, SystemExit):
334         raise
335       except Exception, err:
336         logging.exception("Caught exception")
337         raise http.HttpInternalServerError(message=str(err))
338       except:
339         logging.exception("Unknown exception")
340         raise http.HttpInternalServerError(message="Unknown error")
341
342       # TODO: Content-type
343       encoder = http.HttpJsonConverter()
344       self.response_msg.start_line.code = http.HTTP_OK
345       self.response_msg.body = encoder.Encode(result)
346       self.response_msg.headers = handler_context.resp_headers
347       self.response_msg.headers[http.HTTP_CONTENT_TYPE] = encoder.CONTENT_TYPE
348     finally:
349       # No reason to keep this any longer, even for exceptions
350       handler_context.private = None
351
352   def _SendResponse(self):
353     """Sends the response to the client.
354
355     """
356     if self.response_msg.start_line.code is None:
357       return
358
359     if not self.response_msg.headers:
360       self.response_msg.headers = {}
361
362     self.response_msg.headers.update({
363       # TODO: Keep-alive is not supported
364       http.HTTP_CONNECTION: "close",
365       http.HTTP_DATE: _DateTimeHeader(),
366       http.HTTP_SERVER: http.HTTP_GANETI_VERSION,
367       })
368
369     # Get response reason based on code
370     response_code = self.response_msg.start_line.code
371     if response_code in self.responses:
372       response_reason = self.responses[response_code][0]
373     else:
374       response_reason = ""
375     self.response_msg.start_line.reason = response_reason
376
377     logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1],
378                  self.request_msg.start_line, response_code)
379
380     try:
381       _HttpServerToClientMessageWriter(self.sock, self.request_msg,
382                                        self.response_msg, self.WRITE_TIMEOUT)
383     except http.HttpSocketTimeout:
384       raise http.HttpError("Timeout while sending response")
385     except socket.error, err:
386       raise http.HttpError("Error sending response: %s" % err)
387
388   def _SetErrorStatus(self, err):
389     """Sets the response code and body from a HttpException.
390
391     @type err: HttpException
392     @param err: Exception instance
393
394     """
395     try:
396       (shortmsg, longmsg) = self.responses[err.code]
397     except KeyError:
398       shortmsg = longmsg = "Unknown"
399
400     if err.message:
401       message = err.message
402     else:
403       message = shortmsg
404
405     values = {
406       "code": err.code,
407       "message": cgi.escape(message),
408       "explain": longmsg,
409       }
410
411     self.response_msg.start_line.code = err.code
412
413     headers = {}
414     if err.headers:
415       headers.update(err.headers)
416     headers[http.HTTP_CONTENT_TYPE] = self.error_content_type
417     self.response_msg.headers = headers
418
419     self.response_msg.body = self._FormatErrorMessage(values)
420
421   def _FormatErrorMessage(self, values):
422     """Formats the body of an error message.
423
424     @type values: dict
425     @param values: dictionary with keys code, message and explain.
426     @rtype: string
427     @return: the body of the message
428
429     """
430     return self.error_message_format % values
431
432 class HttpServer(http.HttpBase, asyncore.dispatcher):
433   """Generic HTTP server class
434
435   Users of this class must subclass it and override the HandleRequest function.
436
437   """
438   MAX_CHILDREN = 20
439
440   def __init__(self, mainloop, local_address, port,
441                ssl_params=None, ssl_verify_peer=False,
442                request_executor_class=None):
443     """Initializes the HTTP server
444
445     @type mainloop: ganeti.daemon.Mainloop
446     @param mainloop: Mainloop used to poll for I/O events
447     @type local_address: string
448     @param local_address: Local IP address to bind to
449     @type port: int
450     @param port: TCP port to listen on
451     @type ssl_params: HttpSslParams
452     @param ssl_params: SSL key and certificate
453     @type ssl_verify_peer: bool
454     @param ssl_verify_peer: Whether to require client certificate
455         and compare it with our certificate
456     @type request_executor_class: class
457     @param request_executor_class: an class derived from the
458         HttpServerRequestExecutor class
459
460     """
461     http.HttpBase.__init__(self)
462     asyncore.dispatcher.__init__(self)
463
464     if request_executor_class is None:
465       self.request_executor = HttpServerRequestExecutor
466     else:
467       self.request_executor = request_executor_class
468
469     self.mainloop = mainloop
470     self.local_address = local_address
471     self.port = port
472
473     self.socket = self._CreateSocket(ssl_params, ssl_verify_peer)
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-msg=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, connection, client_addr)
553       except Exception: # pylint: disable-msg=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   def PreHandleRequest(self, req):
562     """Called before handling a request.
563
564     Can be overridden by a subclass.
565
566     """
567
568   def HandleRequest(self, req):
569     """Handles a request.
570
571     Must be overridden by subclass.
572
573     """
574     raise NotImplementedError()