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