RAPI: format error messages as JSON
[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
214   Python's BaseHTTPServer, from both version 2.4 and 3k. It does not
215   support non-ASCII character encodings. Keep-alive connections are
216   not supported.
217
218   """
219   # The default request version.  This only affects responses up until
220   # the point where the request line is parsed, so it mainly decides what
221   # the client gets back when sending a malformed request line.
222   # Most web servers default to HTTP 0.9, i.e. don't send a status line.
223   default_request_version = http.HTTP_0_9
224
225   # Error message settings
226   error_message_format = DEFAULT_ERROR_MESSAGE
227   error_content_type = DEFAULT_ERROR_CONTENT_TYPE
228
229   responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
230
231   # Timeouts in seconds for socket layer
232   WRITE_TIMEOUT = 10
233   READ_TIMEOUT = 10
234   CLOSE_TIMEOUT = 1
235
236   def __init__(self, server, sock, client_addr):
237     """Initializes this class.
238
239     """
240     self.server = server
241     self.sock = sock
242     self.client_addr = client_addr
243
244     self.request_msg = http.HttpMessage()
245     self.response_msg = http.HttpMessage()
246
247     self.response_msg.start_line = \
248       http.HttpServerToClientStartLine(version=self.default_request_version,
249                                        code=None, reason=None)
250
251     # Disable Python's timeout
252     self.sock.settimeout(None)
253
254     # Operate in non-blocking mode
255     self.sock.setblocking(0)
256
257     logging.debug("Connection from %s:%s", client_addr[0], client_addr[1])
258     try:
259       request_msg_reader = None
260       force_close = True
261       try:
262         # Do the secret SSL handshake
263         if self.server.using_ssl:
264           self.sock.set_accept_state()
265           try:
266             http.Handshake(self.sock, self.WRITE_TIMEOUT)
267           except http.HttpSessionHandshakeUnexpectedEOF:
268             # Ignore rest
269             return
270
271         try:
272           try:
273             request_msg_reader = self._ReadRequest()
274             self._HandleRequest()
275
276             # Only wait for client to close if we didn't have any exception.
277             force_close = False
278           except http.HttpException, err:
279             self._SetErrorStatus(err)
280         finally:
281           # Try to send a response
282           self._SendResponse()
283       finally:
284         http.ShutdownConnection(sock, self.CLOSE_TIMEOUT, self.WRITE_TIMEOUT,
285                                 request_msg_reader, force_close)
286
287       self.sock.close()
288       self.sock = None
289     finally:
290       logging.debug("Disconnected %s:%s", client_addr[0], client_addr[1])
291
292   def _ReadRequest(self):
293     """Reads a request sent by client.
294
295     """
296     try:
297       request_msg_reader = \
298         _HttpClientToServerMessageReader(self.sock, self.request_msg,
299                                          self.READ_TIMEOUT)
300     except http.HttpSocketTimeout:
301       raise http.HttpError("Timeout while reading request")
302     except socket.error, err:
303       raise http.HttpError("Error reading request: %s" % err)
304
305     self.response_msg.start_line.version = self.request_msg.start_line.version
306
307     return request_msg_reader
308
309   def _HandleRequest(self):
310     """Calls the handler function for the current request.
311
312     """
313     handler_context = _HttpServerRequest(self.request_msg)
314
315     try:
316       try:
317         # Authentication, etc.
318         self.server.PreHandleRequest(handler_context)
319
320         # Call actual request handler
321         result = self.server.HandleRequest(handler_context)
322       except (http.HttpException, KeyboardInterrupt, SystemExit):
323         raise
324       except Exception, err:
325         logging.exception("Caught exception")
326         raise http.HttpInternalServerError(message=str(err))
327       except:
328         logging.exception("Unknown exception")
329         raise http.HttpInternalServerError(message="Unknown error")
330
331       # TODO: Content-type
332       encoder = http.HttpJsonConverter()
333       self.response_msg.start_line.code = http.HTTP_OK
334       self.response_msg.body = encoder.Encode(result)
335       self.response_msg.headers = handler_context.resp_headers
336       self.response_msg.headers[http.HTTP_CONTENT_TYPE] = encoder.CONTENT_TYPE
337     finally:
338       # No reason to keep this any longer, even for exceptions
339       handler_context.private = None
340
341   def _SendResponse(self):
342     """Sends the response to the client.
343
344     """
345     if self.response_msg.start_line.code is None:
346       return
347
348     if not self.response_msg.headers:
349       self.response_msg.headers = {}
350
351     self.response_msg.headers.update({
352       # TODO: Keep-alive is not supported
353       http.HTTP_CONNECTION: "close",
354       http.HTTP_DATE: _DateTimeHeader(),
355       http.HTTP_SERVER: http.HTTP_GANETI_VERSION,
356       })
357
358     # Get response reason based on code
359     response_code = self.response_msg.start_line.code
360     if response_code in self.responses:
361       response_reason = self.responses[response_code][0]
362     else:
363       response_reason = ""
364     self.response_msg.start_line.reason = response_reason
365
366     logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1],
367                  self.request_msg.start_line, response_code)
368
369     try:
370       _HttpServerToClientMessageWriter(self.sock, self.request_msg,
371                                        self.response_msg, self.WRITE_TIMEOUT)
372     except http.HttpSocketTimeout:
373       raise http.HttpError("Timeout while sending response")
374     except socket.error, err:
375       raise http.HttpError("Error sending response: %s" % err)
376
377   def _SetErrorStatus(self, err):
378     """Sets the response code and body from a HttpException.
379
380     @type err: HttpException
381     @param err: Exception instance
382
383     """
384     try:
385       (shortmsg, longmsg) = self.responses[err.code]
386     except KeyError:
387       shortmsg = longmsg = "Unknown"
388
389     if err.message:
390       message = err.message
391     else:
392       message = shortmsg
393
394     values = {
395       "code": err.code,
396       "message": cgi.escape(message),
397       "explain": longmsg,
398       }
399
400     self.response_msg.start_line.code = err.code
401
402     headers = {}
403     if err.headers:
404       headers.update(err.headers)
405     headers[http.HTTP_CONTENT_TYPE] = self.error_content_type
406     self.response_msg.headers = headers
407
408     self.response_msg.body = self._FormatErrorMessage(values)
409
410   def _FormatErrorMessage(self, values):
411     """Formats the body of an error message.
412
413     @type values: dict
414     @param values: dictionary with keys code, message and explain.
415     @rtype: string
416     @return: the body of the message
417
418     """
419     return self.error_message_format % values
420
421 class HttpServer(http.HttpBase):
422   """Generic HTTP server class
423
424   Users of this class must subclass it and override the HandleRequest function.
425
426   """
427   MAX_CHILDREN = 20
428
429   def __init__(self, mainloop, local_address, port,
430                ssl_params=None, ssl_verify_peer=False,
431                request_executor_class=None):
432     """Initializes the HTTP server
433
434     @type mainloop: ganeti.daemon.Mainloop
435     @param mainloop: Mainloop used to poll for I/O events
436     @type local_address: string
437     @param local_address: Local IP address to bind to
438     @type port: int
439     @param port: TCP port to listen on
440     @type ssl_params: HttpSslParams
441     @param ssl_params: SSL key and certificate
442     @type ssl_verify_peer: bool
443     @param ssl_verify_peer: Whether to require client certificate
444         and compare it with our certificate
445     @type request_executor_class: class
446     @param request_executor_class: an class derived from the
447         HttpServerRequestExecutor class
448
449     """
450     http.HttpBase.__init__(self)
451
452     if request_executor_class is None:
453       self.request_executor = HttpServerRequestExecutor
454     else:
455       self.request_executor = request_executor_class
456
457     self.mainloop = mainloop
458     self.local_address = local_address
459     self.port = port
460
461     self.socket = self._CreateSocket(ssl_params, ssl_verify_peer)
462
463     # Allow port to be reused
464     self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
465
466     self._children = []
467
468     mainloop.RegisterIO(self, self.socket.fileno(), select.POLLIN)
469     mainloop.RegisterSignal(self)
470
471   def Start(self):
472     self.socket.bind((self.local_address, self.port))
473     self.socket.listen(1024)
474
475   def Stop(self):
476     self.socket.close()
477
478   def OnIO(self, fd, condition):
479     if condition & select.POLLIN:
480       self._IncomingConnection()
481
482   def OnSignal(self, signum):
483     if signum == signal.SIGCHLD:
484       self._CollectChildren(True)
485
486   def _CollectChildren(self, quick):
487     """Checks whether any child processes are done
488
489     @type quick: bool
490     @param quick: Whether to only use non-blocking functions
491
492     """
493     if not quick:
494       # Don't wait for other processes if it should be a quick check
495       while len(self._children) > self.MAX_CHILDREN:
496         try:
497           # Waiting without a timeout brings us into a potential DoS situation.
498           # As soon as too many children run, we'll not respond to new
499           # requests. The real solution would be to add a timeout for children
500           # and killing them after some time.
501           pid, status = os.waitpid(0, 0)
502         except os.error:
503           pid = None
504         if pid and pid in self._children:
505           self._children.remove(pid)
506
507     for child in self._children:
508       try:
509         pid, status = os.waitpid(child, os.WNOHANG)
510       except os.error:
511         pid = None
512       if pid and pid in self._children:
513         self._children.remove(pid)
514
515   def _IncomingConnection(self):
516     """Called for each incoming connection
517
518     """
519     (connection, client_addr) = self.socket.accept()
520
521     self._CollectChildren(False)
522
523     pid = os.fork()
524     if pid == 0:
525       # Child process
526       try:
527         self.request_executor(self, connection, client_addr)
528       except Exception:
529         logging.exception("Error while handling request from %s:%s",
530                           client_addr[0], client_addr[1])
531         os._exit(1)
532       os._exit(0)
533     else:
534       self._children.append(pid)
535
536   def PreHandleRequest(self, req):
537     """Called before handling a request.
538
539     Can be overriden by a subclass.
540
541     """
542
543   def HandleRequest(self, req):
544     """Handles a request.
545
546     Must be overriden by subclass.
547
548     """
549     raise NotImplementedError()