4 # Copyright (C) 2007, 2008 Google Inc.
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.
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.
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
21 """HTTP server module.
34 from ganeti import http
35 from ganeti import utils
38 WEEKDAYNAME = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
40 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
41 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
43 # Default error message
44 DEFAULT_ERROR_CONTENT_TYPE = "text/html"
45 DEFAULT_ERROR_MESSAGE = """\
48 <title>Error response</title>
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.
60 def _DateTimeHeader(gmnow=None):
61 """Return the current date and time formatted for a message header.
63 The time MUST be in the GMT timezone.
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))
73 class _HttpServerRequest(object):
74 """Data structure for HTTP request on server side.
77 def __init__(self, method, path, headers, body):
79 self.request_method = method
80 self.request_path = path
81 self.request_headers = headers
82 self.request_body = body
85 self.resp_headers = {}
87 # Private data for request handler (useful in combination with
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, )]
97 return "<%s at %#x>" % (" ".join(status), id(self))
100 class _HttpServerToClientMessageWriter(http.HttpMessageWriter):
101 """Writes an HTTP response to client.
104 def __init__(self, sock, request_msg, response_msg, write_timeout):
105 """Writes the response to the client.
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
118 self._request_msg = request_msg
119 self._response_msg = response_msg
120 http.HttpMessageWriter.__init__(self, sock, response_msg, write_timeout)
122 def HasMessageBody(self):
123 """Logic to detect whether response should contain a message body.
126 if self._request_msg.start_line:
127 request_method = self._request_msg.start_line.method
129 request_method = None
131 response_code = self._response_msg.start_line.code
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"
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."
140 # RFC2616, section 10.2.5: "The 204 response MUST NOT include a
141 # message-body [...]"
143 # RFC2616, section 10.3.5: "The 304 response MUST NOT contain a
144 # message-body, [...]"
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))
154 class _HttpClientToServerMessageReader(http.HttpMessageReader):
155 """Reads an HTTP request sent by client.
159 START_LINE_LENGTH_MAX = 4096
160 HEADER_LENGTH_MAX = 4096
162 def ParseStartLine(self, start_line):
163 """Parses the start line sent by client.
165 Example: "GET /index.html HTTP/1.1"
167 @type start_line: string
168 @param start_line: Start line
171 # Empty lines are skipped when reading
174 logging.debug("HTTP request: %s", start_line)
176 words = start_line.split()
179 [method, path, version] = words
180 if version[:5] != 'HTTP/':
181 raise http.HttpBadRequest("Bad request version (%r)" % version)
184 base_version_number = version.split("/", 1)[1]
185 version_number = base_version_number.split(".")
187 # RFC 2145 section 3.1 says there can be only one "." and
188 # - major and minor numbers MUST be treated as
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)
196 version_number = (int(version_number[0]), int(version_number[1]))
197 except (ValueError, IndexError):
198 raise http.HttpBadRequest("Bad request version (%r)" % version)
200 if version_number >= (2, 0):
201 raise http.HttpVersionNotSupported("Invalid HTTP Version (%s)" %
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)
211 raise http.HttpBadRequest("Bad request syntax (%r)" % start_line)
213 return http.HttpClientToServerStartLine(method, path, version)
216 class HttpServerRequestExecutor(object):
217 """Implements server side of HTTP.
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
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
231 # Error message settings
232 error_message_format = DEFAULT_ERROR_MESSAGE
233 error_content_type = DEFAULT_ERROR_CONTENT_TYPE
235 responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
237 # Timeouts in seconds for socket layer
242 def __init__(self, server, sock, client_addr):
243 """Initializes this class.
248 self.client_addr = client_addr
250 self.request_msg = http.HttpMessage()
251 self.response_msg = http.HttpMessage()
253 self.response_msg.start_line = \
254 http.HttpServerToClientStartLine(version=self.default_request_version,
255 code=None, reason=None)
257 # Disable Python's timeout
258 self.sock.settimeout(None)
260 # Operate in non-blocking mode
261 self.sock.setblocking(0)
263 logging.debug("Connection from %s:%s", client_addr[0], client_addr[1])
265 request_msg_reader = None
268 # Do the secret SSL handshake
269 if self.server.using_ssl:
270 self.sock.set_accept_state()
272 http.Handshake(self.sock, self.WRITE_TIMEOUT)
273 except http.HttpSessionHandshakeUnexpectedEOF:
279 request_msg_reader = self._ReadRequest()
280 self._HandleRequest()
282 # Only wait for client to close if we didn't have any exception.
284 except http.HttpException, err:
285 self._SetErrorStatus(err)
287 # Try to send a response
290 http.ShutdownConnection(sock, self.CLOSE_TIMEOUT, self.WRITE_TIMEOUT,
291 request_msg_reader, force_close)
296 logging.debug("Disconnected %s:%s", client_addr[0], client_addr[1])
298 def _ReadRequest(self):
299 """Reads a request sent by client.
303 request_msg_reader = \
304 _HttpClientToServerMessageReader(self.sock, self.request_msg,
306 except http.HttpSocketTimeout:
307 raise http.HttpError("Timeout while reading request")
308 except socket.error, err:
309 raise http.HttpError("Error reading request: %s" % err)
311 self.response_msg.start_line.version = self.request_msg.start_line.version
313 return request_msg_reader
315 def _HandleRequest(self):
316 """Calls the handler function for the current request.
319 handler_context = _HttpServerRequest(self.request_msg.start_line.method,
320 self.request_msg.start_line.path,
321 self.request_msg.headers,
322 self.request_msg.decoded_body)
324 logging.debug("Handling request %r", handler_context)
328 # Authentication, etc.
329 self.server.PreHandleRequest(handler_context)
331 # Call actual request handler
332 result = self.server.HandleRequest(handler_context)
333 except (http.HttpException, KeyboardInterrupt, SystemExit):
335 except Exception, err:
336 logging.exception("Caught exception")
337 raise http.HttpInternalServerError(message=str(err))
339 logging.exception("Unknown exception")
340 raise http.HttpInternalServerError(message="Unknown error")
343 encoder = http.HttpJsonConverter()
344 self.response_msg.start_line.code = http.HTTP_OK
345 self.response_msg.body = encoder.Encode(result)
346 self.response_msg.headers = handler_context.resp_headers
347 self.response_msg.headers[http.HTTP_CONTENT_TYPE] = encoder.CONTENT_TYPE
349 # No reason to keep this any longer, even for exceptions
350 handler_context.private = None
352 def _SendResponse(self):
353 """Sends the response to the client.
356 if self.response_msg.start_line.code is None:
359 if not self.response_msg.headers:
360 self.response_msg.headers = {}
362 self.response_msg.headers.update({
363 # TODO: Keep-alive is not supported
364 http.HTTP_CONNECTION: "close",
365 http.HTTP_DATE: _DateTimeHeader(),
366 http.HTTP_SERVER: http.HTTP_GANETI_VERSION,
369 # Get response reason based on code
370 response_code = self.response_msg.start_line.code
371 if response_code in self.responses:
372 response_reason = self.responses[response_code][0]
375 self.response_msg.start_line.reason = response_reason
377 logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1],
378 self.request_msg.start_line, response_code)
381 _HttpServerToClientMessageWriter(self.sock, self.request_msg,
382 self.response_msg, self.WRITE_TIMEOUT)
383 except http.HttpSocketTimeout:
384 raise http.HttpError("Timeout while sending response")
385 except socket.error, err:
386 raise http.HttpError("Error sending response: %s" % err)
388 def _SetErrorStatus(self, err):
389 """Sets the response code and body from a HttpException.
391 @type err: HttpException
392 @param err: Exception instance
396 (shortmsg, longmsg) = self.responses[err.code]
398 shortmsg = longmsg = "Unknown"
401 message = err.message
407 "message": cgi.escape(message),
411 self.response_msg.start_line.code = err.code
415 headers.update(err.headers)
416 headers[http.HTTP_CONTENT_TYPE] = self.error_content_type
417 self.response_msg.headers = headers
419 self.response_msg.body = self._FormatErrorMessage(values)
421 def _FormatErrorMessage(self, values):
422 """Formats the body of an error message.
425 @param values: dictionary with keys code, message and explain.
427 @return: the body of the message
430 return self.error_message_format % values
432 class HttpServer(http.HttpBase, asyncore.dispatcher):
433 """Generic HTTP server class
435 Users of this class must subclass it and override the HandleRequest function.
440 def __init__(self, mainloop, local_address, port,
441 ssl_params=None, ssl_verify_peer=False,
442 request_executor_class=None):
443 """Initializes the HTTP server
445 @type mainloop: ganeti.daemon.Mainloop
446 @param mainloop: Mainloop used to poll for I/O events
447 @type local_address: string
448 @param local_address: Local IP address to bind to
450 @param port: TCP port to listen on
451 @type ssl_params: HttpSslParams
452 @param ssl_params: SSL key and certificate
453 @type ssl_verify_peer: bool
454 @param ssl_verify_peer: Whether to require client certificate
455 and compare it with our certificate
456 @type request_executor_class: class
457 @param request_executor_class: an class derived from the
458 HttpServerRequestExecutor class
461 http.HttpBase.__init__(self)
462 asyncore.dispatcher.__init__(self)
464 if request_executor_class is None:
465 self.request_executor = HttpServerRequestExecutor
467 self.request_executor = request_executor_class
469 self.mainloop = mainloop
470 self.local_address = local_address
473 self.socket = self._CreateSocket(ssl_params, ssl_verify_peer)
475 # Allow port to be reused
476 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
479 self.set_socket(self.socket)
480 self.accepting = True
481 mainloop.RegisterSignal(self)
484 self.socket.bind((self.local_address, self.port))
485 self.socket.listen(1024)
490 def handle_accept(self):
491 self._IncomingConnection()
493 def OnSignal(self, signum):
494 if signum == signal.SIGCHLD:
495 self._CollectChildren(True)
497 def _CollectChildren(self, quick):
498 """Checks whether any child processes are done
501 @param quick: Whether to only use non-blocking functions
505 # Don't wait for other processes if it should be a quick check
506 while len(self._children) > self.MAX_CHILDREN:
508 # Waiting without a timeout brings us into a potential DoS situation.
509 # As soon as too many children run, we'll not respond to new
510 # requests. The real solution would be to add a timeout for children
511 # and killing them after some time.
512 pid, _ = os.waitpid(0, 0)
515 if pid and pid in self._children:
516 self._children.remove(pid)
518 for child in self._children:
520 pid, _ = os.waitpid(child, os.WNOHANG)
523 if pid and pid in self._children:
524 self._children.remove(pid)
526 def _IncomingConnection(self):
527 """Called for each incoming connection
530 # pylint: disable-msg=W0212
531 (connection, client_addr) = self.socket.accept()
533 self._CollectChildren(False)
539 # The client shouldn't keep the listening socket open. If the parent
540 # process is restarted, it would fail when there's already something
541 # listening (in this case its own child from a previous run) on the
549 # In case the handler code uses temporary files
550 utils.ResetTempfileModule()
552 self.request_executor(self, connection, client_addr)
553 except Exception: # pylint: disable-msg=W0703
554 logging.exception("Error while handling request from %s:%s",
555 client_addr[0], client_addr[1])
559 self._children.append(pid)
561 def PreHandleRequest(self, req):
562 """Called before handling a request.
564 Can be overridden by a subclass.
568 def HandleRequest(self, req):
569 """Handles a request.
571 Must be overridden by subclass.
574 raise NotImplementedError()