| # |
| # |
| |
| # Copyright (C) 2007, 2008, 2010, 2012 Google Inc. |
| # |
| # This program is free software; you can redistribute it and/or modify |
| # it under the terms of the GNU General Public License as published by |
| # the Free Software Foundation; either version 2 of the License, or |
| # (at your option) any later version. |
| # |
| # This program is distributed in the hope that it will be useful, but |
| # WITHOUT ANY WARRANTY; without even the implied warranty of |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| # General Public License for more details. |
| # |
| # You should have received a copy of the GNU General Public License |
| # along with this program; if not, write to the Free Software |
| # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
| # 02110-1301, USA. |
| |
| """HTTP server module. |
| |
| """ |
| |
| import BaseHTTPServer |
| import cgi |
| import logging |
| import os |
| import socket |
| import time |
| import signal |
| import asyncore |
| |
| from ganeti import http |
| from ganeti import utils |
| from ganeti import netutils |
| from ganeti import compat |
| from ganeti import errors |
| |
| |
| WEEKDAYNAME = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] |
| MONTHNAME = [None, |
| "Jan", "Feb", "Mar", "Apr", "May", "Jun", |
| "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] |
| |
| # Default error message |
| DEFAULT_ERROR_CONTENT_TYPE = "text/html" |
| DEFAULT_ERROR_MESSAGE = """\ |
| <html> |
| <head> |
| <title>Error response</title> |
| </head> |
| <body> |
| <h1>Error response</h1> |
| <p>Error code %(code)d. |
| <p>Message: %(message)s. |
| <p>Error code explanation: %(code)s = %(explain)s. |
| </body> |
| </html> |
| """ |
| |
| |
| def _DateTimeHeader(gmnow=None): |
| """Return the current date and time formatted for a message header. |
| |
| The time MUST be in the GMT timezone. |
| |
| """ |
| if gmnow is None: |
| gmnow = time.gmtime() |
| (year, month, day, hh, mm, ss, wd, _, _) = gmnow |
| return ("%s, %02d %3s %4d %02d:%02d:%02d GMT" % |
| (WEEKDAYNAME[wd], day, MONTHNAME[month], year, hh, mm, ss)) |
| |
| |
| class _HttpServerRequest(object): |
| """Data structure for HTTP request on server side. |
| |
| """ |
| def __init__(self, method, path, headers, body): |
| # Request attributes |
| self.request_method = method |
| self.request_path = path |
| self.request_headers = headers |
| self.request_body = body |
| |
| # Response attributes |
| self.resp_headers = {} |
| |
| # Private data for request handler (useful in combination with |
| # authentication) |
| self.private = None |
| |
| def __repr__(self): |
| status = ["%s.%s" % (self.__class__.__module__, self.__class__.__name__), |
| self.request_method, self.request_path, |
| "headers=%r" % str(self.request_headers), |
| "body=%r" % (self.request_body, )] |
| |
| return "<%s at %#x>" % (" ".join(status), id(self)) |
| |
| |
| class _HttpServerToClientMessageWriter(http.HttpMessageWriter): |
| """Writes an HTTP response to client. |
| |
| """ |
| def __init__(self, sock, request_msg, response_msg, write_timeout): |
| """Writes the response to the client. |
| |
| @type sock: socket |
| @param sock: Target socket |
| @type request_msg: http.HttpMessage |
| @param request_msg: Request message, required to determine whether |
| response may have a message body |
| @type response_msg: http.HttpMessage |
| @param response_msg: Response message |
| @type write_timeout: float |
| @param write_timeout: Write timeout for socket |
| |
| """ |
| self._request_msg = request_msg |
| self._response_msg = response_msg |
| http.HttpMessageWriter.__init__(self, sock, response_msg, write_timeout) |
| |
| def HasMessageBody(self): |
| """Logic to detect whether response should contain a message body. |
| |
| """ |
| if self._request_msg.start_line: |
| request_method = self._request_msg.start_line.method |
| else: |
| request_method = None |
| |
| response_code = self._response_msg.start_line.code |
| |
| # RFC2616, section 4.3: "A message-body MUST NOT be included in a request |
| # if the specification of the request method (section 5.1.1) does not allow |
| # sending an entity-body in requests" |
| # |
| # RFC2616, section 9.4: "The HEAD method is identical to GET except that |
| # the server MUST NOT return a message-body in the response." |
| # |
| # RFC2616, section 10.2.5: "The 204 response MUST NOT include a |
| # message-body [...]" |
| # |
| # RFC2616, section 10.3.5: "The 304 response MUST NOT contain a |
| # message-body, [...]" |
| |
| return (http.HttpMessageWriter.HasMessageBody(self) and |
| (request_method is not None and |
| request_method != http.HTTP_HEAD) and |
| response_code >= http.HTTP_OK and |
| response_code not in (http.HTTP_NO_CONTENT, |
| http.HTTP_NOT_MODIFIED)) |
| |
| |
| class _HttpClientToServerMessageReader(http.HttpMessageReader): |
| """Reads an HTTP request sent by client. |
| |
| """ |
| # Length limits |
| START_LINE_LENGTH_MAX = 8192 |
| HEADER_LENGTH_MAX = 4096 |
| |
| def ParseStartLine(self, start_line): |
| """Parses the start line sent by client. |
| |
| Example: "GET /index.html HTTP/1.1" |
| |
| @type start_line: string |
| @param start_line: Start line |
| |
| """ |
| # Empty lines are skipped when reading |
| assert start_line |
| |
| logging.debug("HTTP request: %s", start_line) |
| |
| words = start_line.split() |
| |
| if len(words) == 3: |
| [method, path, version] = words |
| if version[:5] != "HTTP/": |
| raise http.HttpBadRequest("Bad request version (%r)" % version) |
| |
| try: |
| base_version_number = version.split("/", 1)[1] |
| version_number = base_version_number.split(".") |
| |
| # RFC 2145 section 3.1 says there can be only one "." and |
| # - major and minor numbers MUST be treated as |
| # separate integers; |
| # - HTTP/2.4 is a lower version than HTTP/2.13, which in |
| # turn is lower than HTTP/12.3; |
| # - Leading zeros MUST be ignored by recipients. |
| if len(version_number) != 2: |
| raise http.HttpBadRequest("Bad request version (%r)" % version) |
| |
| version_number = (int(version_number[0]), int(version_number[1])) |
| except (ValueError, IndexError): |
| raise http.HttpBadRequest("Bad request version (%r)" % version) |
| |
| if version_number >= (2, 0): |
| raise http.HttpVersionNotSupported("Invalid HTTP Version (%s)" % |
| base_version_number) |
| |
| elif len(words) == 2: |
| version = http.HTTP_0_9 |
| [method, path] = words |
| if method != http.HTTP_GET: |
| raise http.HttpBadRequest("Bad HTTP/0.9 request type (%r)" % method) |
| |
| else: |
| raise http.HttpBadRequest("Bad request syntax (%r)" % start_line) |
| |
| return http.HttpClientToServerStartLine(method, path, version) |
| |
| |
| def _HandleServerRequestInner(handler, req_msg): |
| """Calls the handler function for the current request. |
| |
| """ |
| handler_context = _HttpServerRequest(req_msg.start_line.method, |
| req_msg.start_line.path, |
| req_msg.headers, |
| req_msg.body) |
| |
| logging.debug("Handling request %r", handler_context) |
| |
| try: |
| try: |
| # Authentication, etc. |
| handler.PreHandleRequest(handler_context) |
| |
| # Call actual request handler |
| result = handler.HandleRequest(handler_context) |
| except (http.HttpException, errors.RapiTestResult, |
| KeyboardInterrupt, SystemExit): |
| raise |
| except Exception, err: |
| logging.exception("Caught exception") |
| raise http.HttpInternalServerError(message=str(err)) |
| except: |
| logging.exception("Unknown exception") |
| raise http.HttpInternalServerError(message="Unknown error") |
| |
| if not isinstance(result, basestring): |
| raise http.HttpError("Handler function didn't return string type") |
| |
| return (http.HTTP_OK, handler_context.resp_headers, result) |
| finally: |
| # No reason to keep this any longer, even for exceptions |
| handler_context.private = None |
| |
| |
| class HttpResponder(object): |
| # The default request version. This only affects responses up until |
| # the point where the request line is parsed, so it mainly decides what |
| # the client gets back when sending a malformed request line. |
| # Most web servers default to HTTP 0.9, i.e. don't send a status line. |
| default_request_version = http.HTTP_0_9 |
| |
| responses = BaseHTTPServer.BaseHTTPRequestHandler.responses |
| |
| def __init__(self, handler): |
| """Initializes this class. |
| |
| """ |
| self._handler = handler |
| |
| def __call__(self, fn): |
| """Handles a request. |
| |
| @type fn: callable |
| @param fn: Callback for retrieving HTTP request, must return a tuple |
| containing request message (L{http.HttpMessage}) and C{None} or the |
| message reader (L{_HttpClientToServerMessageReader}) |
| |
| """ |
| response_msg = http.HttpMessage() |
| response_msg.start_line = \ |
| http.HttpServerToClientStartLine(version=self.default_request_version, |
| code=None, reason=None) |
| |
| force_close = True |
| |
| try: |
| (request_msg, req_msg_reader) = fn() |
| |
| response_msg.start_line.version = request_msg.start_line.version |
| |
| # RFC2616, 14.23: All Internet-based HTTP/1.1 servers MUST respond |
| # with a 400 (Bad Request) status code to any HTTP/1.1 request |
| # message which lacks a Host header field. |
| if (request_msg.start_line.version == http.HTTP_1_1 and |
| not (request_msg.headers and |
| http.HTTP_HOST in request_msg.headers)): |
| raise http.HttpBadRequest(message="Missing Host header") |
| |
| (response_msg.start_line.code, response_msg.headers, |
| response_msg.body) = \ |
| _HandleServerRequestInner(self._handler, request_msg) |
| except http.HttpException, err: |
| self._SetError(self.responses, self._handler, response_msg, err) |
| else: |
| # Only wait for client to close if we didn't have any exception. |
| force_close = False |
| |
| return (request_msg, req_msg_reader, force_close, |
| self._Finalize(self.responses, response_msg)) |
| |
| @staticmethod |
| def _SetError(responses, handler, response_msg, err): |
| """Sets the response code and body from a HttpException. |
| |
| @type err: HttpException |
| @param err: Exception instance |
| |
| """ |
| try: |
| (shortmsg, longmsg) = responses[err.code] |
| except KeyError: |
| shortmsg = longmsg = "Unknown" |
| |
| if err.message: |
| message = err.message |
| else: |
| message = shortmsg |
| |
| values = { |
| "code": err.code, |
| "message": cgi.escape(message), |
| "explain": longmsg, |
| } |
| |
| (content_type, body) = handler.FormatErrorMessage(values) |
| |
| headers = { |
| http.HTTP_CONTENT_TYPE: content_type, |
| } |
| |
| if err.headers: |
| headers.update(err.headers) |
| |
| response_msg.start_line.code = err.code |
| response_msg.headers = headers |
| response_msg.body = body |
| |
| @staticmethod |
| def _Finalize(responses, msg): |
| assert msg.start_line.reason is None |
| |
| if not msg.headers: |
| msg.headers = {} |
| |
| msg.headers.update({ |
| # TODO: Keep-alive is not supported |
| http.HTTP_CONNECTION: "close", |
| http.HTTP_DATE: _DateTimeHeader(), |
| http.HTTP_SERVER: http.HTTP_GANETI_VERSION, |
| }) |
| |
| # Get response reason based on code |
| try: |
| code_desc = responses[msg.start_line.code] |
| except KeyError: |
| reason = "" |
| else: |
| (reason, _) = code_desc |
| |
| msg.start_line.reason = reason |
| |
| return msg |
| |
| |
| class HttpServerRequestExecutor(object): |
| """Implements server side of HTTP. |
| |
| This class implements the server side of HTTP. It's based on code of |
| Python's BaseHTTPServer, from both version 2.4 and 3k. It does not |
| support non-ASCII character encodings. Keep-alive connections are |
| not supported. |
| |
| """ |
| # Timeouts in seconds for socket layer |
| WRITE_TIMEOUT = 10 |
| READ_TIMEOUT = 10 |
| CLOSE_TIMEOUT = 1 |
| |
| def __init__(self, server, handler, sock, client_addr): |
| """Initializes this class. |
| |
| """ |
| responder = HttpResponder(handler) |
| |
| # Disable Python's timeout |
| sock.settimeout(None) |
| |
| # Operate in non-blocking mode |
| sock.setblocking(0) |
| |
| request_msg_reader = None |
| force_close = True |
| |
| logging.debug("Connection from %s:%s", client_addr[0], client_addr[1]) |
| try: |
| # Block for closing connection |
| try: |
| # Do the secret SSL handshake |
| if server.using_ssl: |
| sock.set_accept_state() |
| try: |
| http.Handshake(sock, self.WRITE_TIMEOUT) |
| except http.HttpSessionHandshakeUnexpectedEOF: |
| # Ignore rest |
| return |
| |
| (request_msg, request_msg_reader, force_close, response_msg) = \ |
| responder(compat.partial(self._ReadRequest, sock, self.READ_TIMEOUT)) |
| if response_msg: |
| # HttpMessage.start_line can be of different types |
| # Instance of 'HttpClientToServerStartLine' has no 'code' member |
| # pylint: disable=E1103,E1101 |
| logging.info("%s:%s %s %s", client_addr[0], client_addr[1], |
| request_msg.start_line, response_msg.start_line.code) |
| self._SendResponse(sock, request_msg, response_msg, |
| self.WRITE_TIMEOUT) |
| finally: |
| http.ShutdownConnection(sock, self.CLOSE_TIMEOUT, self.WRITE_TIMEOUT, |
| request_msg_reader, force_close) |
| |
| sock.close() |
| finally: |
| logging.debug("Disconnected %s:%s", client_addr[0], client_addr[1]) |
| |
| @staticmethod |
| def _ReadRequest(sock, timeout): |
| """Reads a request sent by client. |
| |
| """ |
| msg = http.HttpMessage() |
| |
| try: |
| reader = _HttpClientToServerMessageReader(sock, msg, timeout) |
| except http.HttpSocketTimeout: |
| raise http.HttpError("Timeout while reading request") |
| except socket.error, err: |
| raise http.HttpError("Error reading request: %s" % err) |
| |
| return (msg, reader) |
| |
| @staticmethod |
| def _SendResponse(sock, req_msg, msg, timeout): |
| """Sends the response to the client. |
| |
| """ |
| try: |
| _HttpServerToClientMessageWriter(sock, req_msg, msg, timeout) |
| except http.HttpSocketTimeout: |
| raise http.HttpError("Timeout while sending response") |
| except socket.error, err: |
| raise http.HttpError("Error sending response: %s" % err) |
| |
| |
| class HttpServer(http.HttpBase, asyncore.dispatcher): |
| """Generic HTTP server class |
| |
| """ |
| MAX_CHILDREN = 20 |
| |
| def __init__(self, mainloop, local_address, port, handler, |
| ssl_params=None, ssl_verify_peer=False, |
| request_executor_class=None): |
| """Initializes the HTTP server |
| |
| @type mainloop: ganeti.daemon.Mainloop |
| @param mainloop: Mainloop used to poll for I/O events |
| @type local_address: string |
| @param local_address: Local IP address to bind to |
| @type port: int |
| @param port: TCP port to listen on |
| @type ssl_params: HttpSslParams |
| @param ssl_params: SSL key and certificate |
| @type ssl_verify_peer: bool |
| @param ssl_verify_peer: Whether to require client certificate |
| and compare it with our certificate |
| @type request_executor_class: class |
| @param request_executor_class: an class derived from the |
| HttpServerRequestExecutor class |
| |
| """ |
| http.HttpBase.__init__(self) |
| asyncore.dispatcher.__init__(self) |
| |
| if request_executor_class is None: |
| self.request_executor = HttpServerRequestExecutor |
| else: |
| self.request_executor = request_executor_class |
| |
| self.mainloop = mainloop |
| self.local_address = local_address |
| self.port = port |
| self.handler = handler |
| family = netutils.IPAddress.GetAddressFamily(local_address) |
| self.socket = self._CreateSocket(ssl_params, ssl_verify_peer, family) |
| |
| # Allow port to be reused |
| self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
| |
| self._children = [] |
| self.set_socket(self.socket) |
| self.accepting = True |
| mainloop.RegisterSignal(self) |
| |
| def Start(self): |
| self.socket.bind((self.local_address, self.port)) |
| self.socket.listen(1024) |
| |
| def Stop(self): |
| self.socket.close() |
| |
| def handle_accept(self): |
| self._IncomingConnection() |
| |
| def OnSignal(self, signum): |
| if signum == signal.SIGCHLD: |
| self._CollectChildren(True) |
| |
| def _CollectChildren(self, quick): |
| """Checks whether any child processes are done |
| |
| @type quick: bool |
| @param quick: Whether to only use non-blocking functions |
| |
| """ |
| if not quick: |
| # Don't wait for other processes if it should be a quick check |
| while len(self._children) > self.MAX_CHILDREN: |
| try: |
| # Waiting without a timeout brings us into a potential DoS situation. |
| # As soon as too many children run, we'll not respond to new |
| # requests. The real solution would be to add a timeout for children |
| # and killing them after some time. |
| pid, _ = os.waitpid(0, 0) |
| except os.error: |
| pid = None |
| if pid and pid in self._children: |
| self._children.remove(pid) |
| |
| for child in self._children: |
| try: |
| pid, _ = os.waitpid(child, os.WNOHANG) |
| except os.error: |
| pid = None |
| if pid and pid in self._children: |
| self._children.remove(pid) |
| |
| def _IncomingConnection(self): |
| """Called for each incoming connection |
| |
| """ |
| # pylint: disable=W0212 |
| (connection, client_addr) = self.socket.accept() |
| |
| self._CollectChildren(False) |
| |
| pid = os.fork() |
| if pid == 0: |
| # Child process |
| try: |
| # The client shouldn't keep the listening socket open. If the parent |
| # process is restarted, it would fail when there's already something |
| # listening (in this case its own child from a previous run) on the |
| # same port. |
| try: |
| self.socket.close() |
| except socket.error: |
| pass |
| self.socket = None |
| |
| # In case the handler code uses temporary files |
| utils.ResetTempfileModule() |
| |
| self.request_executor(self, self.handler, connection, client_addr) |
| except Exception: # pylint: disable=W0703 |
| logging.exception("Error while handling request from %s:%s", |
| client_addr[0], client_addr[1]) |
| os._exit(1) |
| os._exit(0) |
| else: |
| self._children.append(pid) |
| |
| |
| class HttpServerHandler(object): |
| """Base class for handling HTTP server requests. |
| |
| Users of this class must subclass it and override the L{HandleRequest} |
| function. |
| |
| """ |
| def PreHandleRequest(self, req): |
| """Called before handling a request. |
| |
| Can be overridden by a subclass. |
| |
| """ |
| |
| def HandleRequest(self, req): |
| """Handles a request. |
| |
| Must be overridden by subclass. |
| |
| """ |
| raise NotImplementedError() |
| |
| @staticmethod |
| def FormatErrorMessage(values): |
| """Formats the body of an error message. |
| |
| @type values: dict |
| @param values: dictionary with keys C{code}, C{message} and C{explain}. |
| @rtype: tuple; (string, string) |
| @return: Content-type and response body |
| |
| """ |
| return (DEFAULT_ERROR_CONTENT_TYPE, DEFAULT_ERROR_MESSAGE % values) |