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
37 WEEKDAYNAME = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
39 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
40 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
42 # Default error message
43 DEFAULT_ERROR_CONTENT_TYPE = "text/html"
44 DEFAULT_ERROR_MESSAGE = """\
47 <title>Error response</title>
50 <h1>Error response</h1>
51 <p>Error code %(code)d.
52 <p>Message: %(message)s.
53 <p>Error code explanation: %(code)s = %(explain)s.
59 def _DateTimeHeader(gmnow=None):
60 """Return the current date and time formatted for a message header.
62 The time MUST be in the GMT timezone.
67 (year, month, day, hh, mm, ss, wd, _, _) = gmnow
68 return ("%s, %02d %3s %4d %02d:%02d:%02d GMT" %
69 (WEEKDAYNAME[wd], day, MONTHNAME[month], year, hh, mm, ss))
72 class _HttpServerRequest(object):
73 """Data structure for HTTP request on server side.
76 def __init__(self, request_msg):
78 self.request_method = request_msg.start_line.method
79 self.request_path = request_msg.start_line.path
80 self.request_headers = request_msg.headers
81 self.request_body = request_msg.decoded_body
84 self.resp_headers = {}
86 # Private data for request handler (useful in combination with
91 class _HttpServerToClientMessageWriter(http.HttpMessageWriter):
92 """Writes an HTTP response to client.
95 def __init__(self, sock, request_msg, response_msg, write_timeout):
96 """Writes the response to the client.
99 @param sock: Target socket
100 @type request_msg: http.HttpMessage
101 @param request_msg: Request message, required to determine whether
102 response may have a message body
103 @type response_msg: http.HttpMessage
104 @param response_msg: Response message
105 @type write_timeout: float
106 @param write_timeout: Write timeout for socket
109 self._request_msg = request_msg
110 self._response_msg = response_msg
111 http.HttpMessageWriter.__init__(self, sock, response_msg, write_timeout)
113 def HasMessageBody(self):
114 """Logic to detect whether response should contain a message body.
117 if self._request_msg.start_line:
118 request_method = self._request_msg.start_line.method
120 request_method = None
122 response_code = self._response_msg.start_line.code
124 # RFC2616, section 4.3: "A message-body MUST NOT be included in a request
125 # if the specification of the request method (section 5.1.1) does not allow
126 # sending an entity-body in requests"
128 # RFC2616, section 9.4: "The HEAD method is identical to GET except that
129 # the server MUST NOT return a message-body in the response."
131 # RFC2616, section 10.2.5: "The 204 response MUST NOT include a
132 # message-body [...]"
134 # RFC2616, section 10.3.5: "The 304 response MUST NOT contain a
135 # message-body, [...]"
137 return (http.HttpMessageWriter.HasMessageBody(self) and
138 (request_method is not None and
139 request_method != http.HTTP_HEAD) and
140 response_code >= http.HTTP_OK and
141 response_code not in (http.HTTP_NO_CONTENT,
142 http.HTTP_NOT_MODIFIED))
145 class _HttpClientToServerMessageReader(http.HttpMessageReader):
146 """Reads an HTTP request sent by client.
150 START_LINE_LENGTH_MAX = 4096
151 HEADER_LENGTH_MAX = 4096
153 def ParseStartLine(self, start_line):
154 """Parses the start line sent by client.
156 Example: "GET /index.html HTTP/1.1"
158 @type start_line: string
159 @param start_line: Start line
162 # Empty lines are skipped when reading
165 logging.debug("HTTP request: %s", start_line)
167 words = start_line.split()
170 [method, path, version] = words
171 if version[:5] != 'HTTP/':
172 raise http.HttpBadRequest("Bad request version (%r)" % version)
175 base_version_number = version.split("/", 1)[1]
176 version_number = base_version_number.split(".")
178 # RFC 2145 section 3.1 says there can be only one "." and
179 # - major and minor numbers MUST be treated as
181 # - HTTP/2.4 is a lower version than HTTP/2.13, which in
182 # turn is lower than HTTP/12.3;
183 # - Leading zeros MUST be ignored by recipients.
184 if len(version_number) != 2:
185 raise http.HttpBadRequest("Bad request version (%r)" % version)
187 version_number = (int(version_number[0]), int(version_number[1]))
188 except (ValueError, IndexError):
189 raise http.HttpBadRequest("Bad request version (%r)" % version)
191 if version_number >= (2, 0):
192 raise http.HttpVersionNotSupported("Invalid HTTP Version (%s)" %
195 elif len(words) == 2:
196 version = http.HTTP_0_9
197 [method, path] = words
198 if method != http.HTTP_GET:
199 raise http.HttpBadRequest("Bad HTTP/0.9 request type (%r)" % method)
202 raise http.HttpBadRequest("Bad request syntax (%r)" % start_line)
204 return http.HttpClientToServerStartLine(method, path, version)
207 class HttpServerRequestExecutor(object):
208 """Implements server side of HTTP.
210 This class implements the server side of HTTP. It's based on code of
211 Python's BaseHTTPServer, from both version 2.4 and 3k. It does not
212 support non-ASCII character encodings. Keep-alive connections are
216 # The default request version. This only affects responses up until
217 # the point where the request line is parsed, so it mainly decides what
218 # the client gets back when sending a malformed request line.
219 # Most web servers default to HTTP 0.9, i.e. don't send a status line.
220 default_request_version = http.HTTP_0_9
222 # Error message settings
223 error_message_format = DEFAULT_ERROR_MESSAGE
224 error_content_type = DEFAULT_ERROR_CONTENT_TYPE
226 responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
228 # Timeouts in seconds for socket layer
233 def __init__(self, server, sock, client_addr):
234 """Initializes this class.
239 self.client_addr = client_addr
241 self.request_msg = http.HttpMessage()
242 self.response_msg = http.HttpMessage()
244 self.response_msg.start_line = \
245 http.HttpServerToClientStartLine(version=self.default_request_version,
246 code=None, reason=None)
248 # Disable Python's timeout
249 self.sock.settimeout(None)
251 # Operate in non-blocking mode
252 self.sock.setblocking(0)
254 logging.debug("Connection from %s:%s", client_addr[0], client_addr[1])
256 request_msg_reader = None
259 # Do the secret SSL handshake
260 if self.server.using_ssl:
261 self.sock.set_accept_state()
263 http.Handshake(self.sock, self.WRITE_TIMEOUT)
264 except http.HttpSessionHandshakeUnexpectedEOF:
270 request_msg_reader = self._ReadRequest()
271 self._HandleRequest()
273 # Only wait for client to close if we didn't have any exception.
275 except http.HttpException, err:
276 self._SetErrorStatus(err)
278 # Try to send a response
281 http.ShutdownConnection(sock, self.CLOSE_TIMEOUT, self.WRITE_TIMEOUT,
282 request_msg_reader, force_close)
287 logging.debug("Disconnected %s:%s", client_addr[0], client_addr[1])
289 def _ReadRequest(self):
290 """Reads a request sent by client.
294 request_msg_reader = \
295 _HttpClientToServerMessageReader(self.sock, self.request_msg,
297 except http.HttpSocketTimeout:
298 raise http.HttpError("Timeout while reading request")
299 except socket.error, err:
300 raise http.HttpError("Error reading request: %s" % err)
302 self.response_msg.start_line.version = self.request_msg.start_line.version
304 return request_msg_reader
306 def _HandleRequest(self):
307 """Calls the handler function for the current request.
310 handler_context = _HttpServerRequest(self.request_msg)
314 # Authentication, etc.
315 self.server.PreHandleRequest(handler_context)
317 # Call actual request handler
318 result = self.server.HandleRequest(handler_context)
319 except (http.HttpException, KeyboardInterrupt, SystemExit):
321 except Exception, err:
322 logging.exception("Caught exception")
323 raise http.HttpInternalServerError(message=str(err))
325 logging.exception("Unknown exception")
326 raise http.HttpInternalServerError(message="Unknown error")
329 encoder = http.HttpJsonConverter()
330 self.response_msg.start_line.code = http.HTTP_OK
331 self.response_msg.body = encoder.Encode(result)
332 self.response_msg.headers = handler_context.resp_headers
333 self.response_msg.headers[http.HTTP_CONTENT_TYPE] = encoder.CONTENT_TYPE
335 # No reason to keep this any longer, even for exceptions
336 handler_context.private = None
338 def _SendResponse(self):
339 """Sends the response to the client.
342 if self.response_msg.start_line.code is None:
345 if not self.response_msg.headers:
346 self.response_msg.headers = {}
348 self.response_msg.headers.update({
349 # TODO: Keep-alive is not supported
350 http.HTTP_CONNECTION: "close",
351 http.HTTP_DATE: _DateTimeHeader(),
352 http.HTTP_SERVER: http.HTTP_GANETI_VERSION,
355 # Get response reason based on code
356 response_code = self.response_msg.start_line.code
357 if response_code in self.responses:
358 response_reason = self.responses[response_code][0]
361 self.response_msg.start_line.reason = response_reason
363 logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1],
364 self.request_msg.start_line, response_code)
367 _HttpServerToClientMessageWriter(self.sock, self.request_msg,
368 self.response_msg, self.WRITE_TIMEOUT)
369 except http.HttpSocketTimeout:
370 raise http.HttpError("Timeout while sending response")
371 except socket.error, err:
372 raise http.HttpError("Error sending response: %s" % err)
374 def _SetErrorStatus(self, err):
375 """Sets the response code and body from a HttpException.
377 @type err: HttpException
378 @param err: Exception instance
382 (shortmsg, longmsg) = self.responses[err.code]
384 shortmsg = longmsg = "Unknown"
387 message = err.message
393 "message": cgi.escape(message),
397 self.response_msg.start_line.code = err.code
401 headers.update(err.headers)
402 headers[http.HTTP_CONTENT_TYPE] = self.error_content_type
403 self.response_msg.headers = headers
405 self.response_msg.body = self._FormatErrorMessage(values)
407 def _FormatErrorMessage(self, values):
408 """Formats the body of an error message.
411 @param values: dictionary with keys code, message and explain.
413 @return: the body of the message
416 return self.error_message_format % values
418 class HttpServer(http.HttpBase):
419 """Generic HTTP server class
421 Users of this class must subclass it and override the HandleRequest function.
426 def __init__(self, mainloop, local_address, port,
427 ssl_params=None, ssl_verify_peer=False,
428 request_executor_class=None):
429 """Initializes the HTTP server
431 @type mainloop: ganeti.daemon.Mainloop
432 @param mainloop: Mainloop used to poll for I/O events
433 @type local_address: string
434 @param local_address: Local IP address to bind to
436 @param port: TCP port to listen on
437 @type ssl_params: HttpSslParams
438 @param ssl_params: SSL key and certificate
439 @type ssl_verify_peer: bool
440 @param ssl_verify_peer: Whether to require client certificate
441 and compare it with our certificate
442 @type request_executor_class: class
443 @param request_executor_class: an class derived from the
444 HttpServerRequestExecutor class
447 http.HttpBase.__init__(self)
449 if request_executor_class is None:
450 self.request_executor = HttpServerRequestExecutor
452 self.request_executor = request_executor_class
454 self.mainloop = mainloop
455 self.local_address = local_address
458 self.socket = self._CreateSocket(ssl_params, ssl_verify_peer)
460 # Allow port to be reused
461 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
465 mainloop.RegisterIO(self, self.socket.fileno(), select.POLLIN)
466 mainloop.RegisterSignal(self)
469 self.socket.bind((self.local_address, self.port))
470 self.socket.listen(1024)
475 def OnIO(self, fd, condition):
476 if condition & select.POLLIN:
477 self._IncomingConnection()
479 def OnSignal(self, signum):
480 if signum == signal.SIGCHLD:
481 self._CollectChildren(True)
483 def _CollectChildren(self, quick):
484 """Checks whether any child processes are done
487 @param quick: Whether to only use non-blocking functions
491 # Don't wait for other processes if it should be a quick check
492 while len(self._children) > self.MAX_CHILDREN:
494 # Waiting without a timeout brings us into a potential DoS situation.
495 # As soon as too many children run, we'll not respond to new
496 # requests. The real solution would be to add a timeout for children
497 # and killing them after some time.
498 pid, _ = os.waitpid(0, 0)
501 if pid and pid in self._children:
502 self._children.remove(pid)
504 for child in self._children:
506 pid, status = os.waitpid(child, os.WNOHANG)
509 if pid and pid in self._children:
510 self._children.remove(pid)
512 def _IncomingConnection(self):
513 """Called for each incoming connection
516 (connection, client_addr) = self.socket.accept()
518 self._CollectChildren(False)
524 self.request_executor(self, connection, client_addr)
526 logging.exception("Error while handling request from %s:%s",
527 client_addr[0], client_addr[1])
531 self._children.append(pid)
533 def PreHandleRequest(self, req):
534 """Called before handling a request.
536 Can be overridden by a subclass.
540 def HandleRequest(self, req):
541 """Handles a request.
543 Must be overridden by subclass.
546 raise NotImplementedError()