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