Rename http.HttpInternalError to HttpInternalServerError
[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
90 class _HttpServerToClientMessageWriter(http.HttpMessageWriter):
91   """Writes an HTTP response to client.
92
93   """
94   def __init__(self, sock, request_msg, response_msg, write_timeout):
95     """Writes the response to the client.
96
97     @type sock: socket
98     @param sock: Target socket
99     @type request_msg: http.HttpMessage
100     @param request_msg: Request message, required to determine whether
101                         response may have a message body
102     @type response_msg: http.HttpMessage
103     @param response_msg: Response message
104     @type write_timeout: float
105     @param write_timeout: Write timeout for socket
106
107     """
108     self._request_msg = request_msg
109     self._response_msg = response_msg
110     http.HttpMessageWriter.__init__(self, sock, response_msg, write_timeout)
111
112   def HasMessageBody(self):
113     """Logic to detect whether response should contain a message body.
114
115     """
116     if self._request_msg.start_line:
117       request_method = self._request_msg.start_line.method
118     else:
119       request_method = None
120
121     response_code = self._response_msg.start_line.code
122
123     # RFC2616, section 4.3: "A message-body MUST NOT be included in a request
124     # if the specification of the request method (section 5.1.1) does not allow
125     # sending an entity-body in requests"
126     #
127     # RFC2616, section 9.4: "The HEAD method is identical to GET except that
128     # the server MUST NOT return a message-body in the response."
129     #
130     # RFC2616, section 10.2.5: "The 204 response MUST NOT include a
131     # message-body [...]"
132     #
133     # RFC2616, section 10.3.5: "The 304 response MUST NOT contain a
134     # message-body, [...]"
135
136     return (http.HttpMessageWriter.HasMessageBody(self) and
137             (request_method is not None and
138              request_method != http.HTTP_HEAD) and
139             response_code >= http.HTTP_OK and
140             response_code not in (http.HTTP_NO_CONTENT,
141                                   http.HTTP_NOT_MODIFIED))
142
143
144 class _HttpClientToServerMessageReader(http.HttpMessageReader):
145   """Reads an HTTP request sent by client.
146
147   """
148   # Length limits
149   START_LINE_LENGTH_MAX = 4096
150   HEADER_LENGTH_MAX = 4096
151
152   def ParseStartLine(self, start_line):
153     """Parses the start line sent by client.
154
155     Example: "GET /index.html HTTP/1.1"
156
157     @type start_line: string
158     @param start_line: Start line
159
160     """
161     # Empty lines are skipped when reading
162     assert start_line
163
164     logging.debug("HTTP request: %s", start_line)
165
166     words = start_line.split()
167
168     if len(words) == 3:
169       [method, path, version] = words
170       if version[:5] != 'HTTP/':
171         raise http.HttpBadRequest("Bad request version (%r)" % version)
172
173       try:
174         base_version_number = version.split("/", 1)[1]
175         version_number = base_version_number.split(".")
176
177         # RFC 2145 section 3.1 says there can be only one "." and
178         #   - major and minor numbers MUST be treated as
179         #      separate integers;
180         #   - HTTP/2.4 is a lower version than HTTP/2.13, which in
181         #      turn is lower than HTTP/12.3;
182         #   - Leading zeros MUST be ignored by recipients.
183         if len(version_number) != 2:
184           raise http.HttpBadRequest("Bad request version (%r)" % version)
185
186         version_number = (int(version_number[0]), int(version_number[1]))
187       except (ValueError, IndexError):
188         raise http.HttpBadRequest("Bad request version (%r)" % version)
189
190       if version_number >= (2, 0):
191         raise http.HttpVersionNotSupported("Invalid HTTP Version (%s)" %
192                                       base_version_number)
193
194     elif len(words) == 2:
195       version = http.HTTP_0_9
196       [method, path] = words
197       if method != http.HTTP_GET:
198         raise http.HttpBadRequest("Bad HTTP/0.9 request type (%r)" % method)
199
200     else:
201       raise http.HttpBadRequest("Bad request syntax (%r)" % start_line)
202
203     return http.HttpClientToServerStartLine(method, path, version)
204
205
206 class _HttpServerRequestExecutor(object):
207   """Implements server side of HTTP.
208
209   This class implements the server side of HTTP. It's based on code of Python's
210   BaseHTTPServer, from both version 2.4 and 3k. It does not support non-ASCII
211   character encodings. Keep-alive connections are not supported.
212
213   """
214   # The default request version.  This only affects responses up until
215   # the point where the request line is parsed, so it mainly decides what
216   # the client gets back when sending a malformed request line.
217   # Most web servers default to HTTP 0.9, i.e. don't send a status line.
218   default_request_version = http.HTTP_0_9
219
220   # Error message settings
221   error_message_format = DEFAULT_ERROR_MESSAGE
222   error_content_type = DEFAULT_ERROR_CONTENT_TYPE
223
224   responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
225
226   # Timeouts in seconds for socket layer
227   WRITE_TIMEOUT = 10
228   READ_TIMEOUT = 10
229   CLOSE_TIMEOUT = 1
230
231   def __init__(self, server, sock, client_addr):
232     """Initializes this class.
233
234     """
235     self.server = server
236     self.sock = sock
237     self.client_addr = client_addr
238
239     self.poller = select.poll()
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.info("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.poller, 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(self.poller, sock,
282                                 self.CLOSE_TIMEOUT, self.WRITE_TIMEOUT,
283                                 request_msg_reader, force_close)
284
285       self.sock.close()
286       self.sock = None
287     finally:
288       logging.info("Disconnected %s:%s", client_addr[0], client_addr[1])
289
290   def _ReadRequest(self):
291     """Reads a request sent by client.
292
293     """
294     try:
295       request_msg_reader = \
296         _HttpClientToServerMessageReader(self.sock, self.request_msg,
297                                          self.READ_TIMEOUT)
298     except http.HttpSocketTimeout:
299       raise http.HttpError("Timeout while reading request")
300     except socket.error, err:
301       raise http.HttpError("Error reading request: %s" % err)
302
303     self.response_msg.start_line.version = self.request_msg.start_line.version
304
305     return request_msg_reader
306
307   def _HandleRequest(self):
308     """Calls the handler function for the current request.
309
310     """
311     handler_context = _HttpServerRequest(self.request_msg)
312
313     try:
314       result = self.server.HandleRequest(handler_context)
315     except (http.HttpException, KeyboardInterrupt, SystemExit):
316       raise
317     except Exception, err:
318       logging.exception("Caught exception")
319       raise http.HttpInternalServerError(message=str(err))
320     except:
321       logging.exception("Unknown exception")
322       raise http.HttpInternalServerError(message="Unknown error")
323
324     # TODO: Content-type
325     encoder = http.HttpJsonConverter()
326     self.response_msg.start_line.code = http.HTTP_OK
327     self.response_msg.body = encoder.Encode(result)
328     self.response_msg.headers = handler_context.resp_headers
329     self.response_msg.headers[http.HTTP_CONTENT_TYPE] = encoder.CONTENT_TYPE
330
331   def _SendResponse(self):
332     """Sends the response to the client.
333
334     """
335     if self.response_msg.start_line.code is None:
336       return
337
338     if not self.response_msg.headers:
339       self.response_msg.headers = {}
340
341     self.response_msg.headers.update({
342       # TODO: Keep-alive is not supported
343       http.HTTP_CONNECTION: "close",
344       http.HTTP_DATE: _DateTimeHeader(),
345       http.HTTP_SERVER: http.HTTP_GANETI_VERSION,
346       })
347
348     # Get response reason based on code
349     response_code = self.response_msg.start_line.code
350     if response_code in self.responses:
351       response_reason = self.responses[response_code][0]
352     else:
353       response_reason = ""
354     self.response_msg.start_line.reason = response_reason
355
356     logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1],
357                  self.request_msg.start_line, response_code)
358
359     try:
360       _HttpServerToClientMessageWriter(self.sock, self.request_msg,
361                                        self.response_msg, self.WRITE_TIMEOUT)
362     except http.HttpSocketTimeout:
363       raise http.HttpError("Timeout while sending response")
364     except socket.error, err:
365       raise http.HttpError("Error sending response: %s" % err)
366
367   def _SetErrorStatus(self, err):
368     """Sets the response code and body from a HttpException.
369
370     @type err: HttpException
371     @param err: Exception instance
372
373     """
374     try:
375       (shortmsg, longmsg) = self.responses[err.code]
376     except KeyError:
377       shortmsg = longmsg = "Unknown"
378
379     if err.message:
380       message = err.message
381     else:
382       message = shortmsg
383
384     values = {
385       "code": err.code,
386       "message": cgi.escape(message),
387       "explain": longmsg,
388       }
389
390     self.response_msg.start_line.code = err.code
391
392     headers = {}
393     if err.headers:
394       headers.update(err.headers)
395     headers[http.HTTP_CONTENT_TYPE] = self.error_content_type
396     self.response_msg.headers = headers
397
398     self.response_msg.body = self.error_message_format % values
399
400
401 class HttpServer(http.HttpBase):
402   """Generic HTTP server class
403
404   Users of this class must subclass it and override the HandleRequest function.
405
406   """
407   MAX_CHILDREN = 20
408
409   def __init__(self, mainloop, local_address, port,
410                ssl_params=None, ssl_verify_peer=False):
411     """Initializes the HTTP server
412
413     @type mainloop: ganeti.daemon.Mainloop
414     @param mainloop: Mainloop used to poll for I/O events
415     @type local_address: string
416     @param local_address: Local IP address to bind to
417     @type port: int
418     @param port: TCP port to listen on
419     @type ssl_params: HttpSslParams
420     @param ssl_params: SSL key and certificate
421     @type ssl_verify_peer: bool
422     @param ssl_verify_peer: Whether to require client certificate and compare
423                             it with our certificate
424
425     """
426     http.HttpBase.__init__(self)
427
428     self.mainloop = mainloop
429     self.local_address = local_address
430     self.port = port
431
432     self.socket = self._CreateSocket(ssl_params, ssl_verify_peer)
433
434     # Allow port to be reused
435     self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
436
437     self._children = []
438
439     mainloop.RegisterIO(self, self.socket.fileno(), select.POLLIN)
440     mainloop.RegisterSignal(self)
441
442   def Start(self):
443     self.socket.bind((self.local_address, self.port))
444     self.socket.listen(1024)
445
446   def Stop(self):
447     self.socket.close()
448
449   def OnIO(self, fd, condition):
450     if condition & select.POLLIN:
451       self._IncomingConnection()
452
453   def OnSignal(self, signum):
454     if signum == signal.SIGCHLD:
455       self._CollectChildren(True)
456
457   def _CollectChildren(self, quick):
458     """Checks whether any child processes are done
459
460     @type quick: bool
461     @param quick: Whether to only use non-blocking functions
462
463     """
464     if not quick:
465       # Don't wait for other processes if it should be a quick check
466       while len(self._children) > self.MAX_CHILDREN:
467         try:
468           # Waiting without a timeout brings us into a potential DoS situation.
469           # As soon as too many children run, we'll not respond to new
470           # requests. The real solution would be to add a timeout for children
471           # and killing them after some time.
472           pid, status = os.waitpid(0, 0)
473         except os.error:
474           pid = None
475         if pid and pid in self._children:
476           self._children.remove(pid)
477
478     for child in self._children:
479       try:
480         pid, status = os.waitpid(child, os.WNOHANG)
481       except os.error:
482         pid = None
483       if pid and pid in self._children:
484         self._children.remove(pid)
485
486   def _IncomingConnection(self):
487     """Called for each incoming connection
488
489     """
490     (connection, client_addr) = self.socket.accept()
491
492     self._CollectChildren(False)
493
494     pid = os.fork()
495     if pid == 0:
496       # Child process
497       try:
498         _HttpServerRequestExecutor(self, connection, client_addr)
499       except Exception:
500         logging.exception("Error while handling request from %s:%s",
501                           client_addr[0], client_addr[1])
502         os._exit(1)
503       os._exit(0)
504     else:
505       self._children.append(pid)
506
507   def HandleRequest(self, req):
508     """Handles a request.
509
510     Must be overriden by subclass.
511
512     """
513     raise NotImplementedError()