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