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