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 constants
35 from ganeti import serializer
36 from ganeti import utils
37 from ganeti import http
40 WEEKDAYNAME = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
42 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
43 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
45 # Default error message
46 DEFAULT_ERROR_CONTENT_TYPE = "text/html"
47 DEFAULT_ERROR_MESSAGE = """\
50 <title>Error response</title>
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.
62 def _DateTimeHeader(gmnow=None):
63 """Return the current date and time formatted for a message header.
65 The time MUST be in the GMT timezone.
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))
75 class _HttpServerRequest(object):
76 """Data structure for HTTP request on server side.
79 def __init__(self, request_msg):
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
87 self.resp_headers = {}
89 # Private data for request handler (useful in combination with
94 class _HttpServerToClientMessageWriter(http.HttpMessageWriter):
95 """Writes an HTTP response to client.
98 def __init__(self, sock, request_msg, response_msg, write_timeout):
99 """Writes the response to the client.
102 @param sock: Target socket
103 @type request_msg: http.HttpMessage
104 @param request_msg: Request message, required to determine whether
105 response may have a message body
106 @type response_msg: http.HttpMessage
107 @param response_msg: Response message
108 @type write_timeout: float
109 @param write_timeout: Write timeout for socket
112 self._request_msg = request_msg
113 self._response_msg = response_msg
114 http.HttpMessageWriter.__init__(self, sock, response_msg, write_timeout)
116 def HasMessageBody(self):
117 """Logic to detect whether response should contain a message body.
120 if self._request_msg.start_line:
121 request_method = self._request_msg.start_line.method
123 request_method = None
125 response_code = self._response_msg.start_line.code
127 # RFC2616, section 4.3: "A message-body MUST NOT be included in a request
128 # if the specification of the request method (section 5.1.1) does not allow
129 # sending an entity-body in requests"
131 # RFC2616, section 9.4: "The HEAD method is identical to GET except that
132 # the server MUST NOT return a message-body in the response."
134 # RFC2616, section 10.2.5: "The 204 response MUST NOT include a
135 # message-body [...]"
137 # RFC2616, section 10.3.5: "The 304 response MUST NOT contain a
138 # message-body, [...]"
140 return (http.HttpMessageWriter.HasMessageBody(self) and
141 (request_method is not None and
142 request_method != http.HTTP_HEAD) and
143 response_code >= http.HTTP_OK and
144 response_code not in (http.HTTP_NO_CONTENT,
145 http.HTTP_NOT_MODIFIED))
148 class _HttpClientToServerMessageReader(http.HttpMessageReader):
149 """Reads an HTTP request sent by client.
153 START_LINE_LENGTH_MAX = 4096
154 HEADER_LENGTH_MAX = 4096
156 def ParseStartLine(self, start_line):
157 """Parses the start line sent by client.
159 Example: "GET /index.html HTTP/1.1"
161 @type start_line: string
162 @param start_line: Start line
165 # Empty lines are skipped when reading
168 logging.debug("HTTP request: %s", start_line)
170 words = start_line.split()
173 [method, path, version] = words
174 if version[:5] != 'HTTP/':
175 raise http.HttpBadRequest("Bad request version (%r)" % version)
178 base_version_number = version.split("/", 1)[1]
179 version_number = base_version_number.split(".")
181 # RFC 2145 section 3.1 says there can be only one "." and
182 # - major and minor numbers MUST be treated as
184 # - HTTP/2.4 is a lower version than HTTP/2.13, which in
185 # turn is lower than HTTP/12.3;
186 # - Leading zeros MUST be ignored by recipients.
187 if len(version_number) != 2:
188 raise http.HttpBadRequest("Bad request version (%r)" % version)
190 version_number = (int(version_number[0]), int(version_number[1]))
191 except (ValueError, IndexError):
192 raise http.HttpBadRequest("Bad request version (%r)" % version)
194 if version_number >= (2, 0):
195 raise http.HttpVersionNotSupported("Invalid HTTP Version (%s)" %
198 elif len(words) == 2:
199 version = http.HTTP_0_9
200 [method, path] = words
201 if method != http.HTTP_GET:
202 raise http.HttpBadRequest("Bad HTTP/0.9 request type (%r)" % method)
205 raise http.HttpBadRequest("Bad request syntax (%r)" % start_line)
207 return http.HttpClientToServerStartLine(method, path, version)
210 class _HttpServerRequestExecutor(object):
211 """Implements server side of HTTP.
213 This class implements the server side of HTTP. It's based on code of Python's
214 BaseHTTPServer, from both version 2.4 and 3k. It does not support non-ASCII
215 character encodings. Keep-alive connections are not supported.
218 # The default request version. This only affects responses up until
219 # the point where the request line is parsed, so it mainly decides what
220 # the client gets back when sending a malformed request line.
221 # Most web servers default to HTTP 0.9, i.e. don't send a status line.
222 default_request_version = http.HTTP_0_9
224 # Error message settings
225 error_message_format = DEFAULT_ERROR_MESSAGE
226 error_content_type = DEFAULT_ERROR_CONTENT_TYPE
228 responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
230 # Timeouts in seconds for socket layer
235 def __init__(self, server, sock, client_addr):
236 """Initializes this class.
241 self.client_addr = client_addr
243 self.request_msg = http.HttpMessage()
244 self.response_msg = http.HttpMessage()
246 self.response_msg.start_line = \
247 http.HttpServerToClientStartLine(version=self.default_request_version,
248 code=None, reason=None)
250 # Disable Python's timeout
251 self.sock.settimeout(None)
253 # Operate in non-blocking mode
254 self.sock.setblocking(0)
256 logging.debug("Connection from %s:%s", client_addr[0], client_addr[1])
258 request_msg_reader = None
261 # Do the secret SSL handshake
262 if self.server.using_ssl:
263 self.sock.set_accept_state()
265 http.Handshake(self.sock, self.WRITE_TIMEOUT)
266 except http.HttpSessionHandshakeUnexpectedEOF:
272 request_msg_reader = self._ReadRequest()
273 self._HandleRequest()
275 # Only wait for client to close if we didn't have any exception.
277 except http.HttpException, err:
278 self._SetErrorStatus(err)
280 # Try to send a response
283 http.ShutdownConnection(sock, self.CLOSE_TIMEOUT, self.WRITE_TIMEOUT,
284 request_msg_reader, force_close)
289 logging.debug("Disconnected %s:%s", client_addr[0], client_addr[1])
291 def _ReadRequest(self):
292 """Reads a request sent by client.
296 request_msg_reader = \
297 _HttpClientToServerMessageReader(self.sock, self.request_msg,
299 except http.HttpSocketTimeout:
300 raise http.HttpError("Timeout while reading request")
301 except socket.error, err:
302 raise http.HttpError("Error reading request: %s" % err)
304 self.response_msg.start_line.version = self.request_msg.start_line.version
306 return request_msg_reader
308 def _HandleRequest(self):
309 """Calls the handler function for the current request.
312 handler_context = _HttpServerRequest(self.request_msg)
316 # Authentication, etc.
317 self.server.PreHandleRequest(handler_context)
319 # Call actual request handler
320 result = self.server.HandleRequest(handler_context)
321 except (http.HttpException, KeyboardInterrupt, SystemExit):
323 except Exception, err:
324 logging.exception("Caught exception")
325 raise http.HttpInternalServerError(message=str(err))
327 logging.exception("Unknown exception")
328 raise http.HttpInternalServerError(message="Unknown error")
331 encoder = http.HttpJsonConverter()
332 self.response_msg.start_line.code = http.HTTP_OK
333 self.response_msg.body = encoder.Encode(result)
334 self.response_msg.headers = handler_context.resp_headers
335 self.response_msg.headers[http.HTTP_CONTENT_TYPE] = encoder.CONTENT_TYPE
337 # No reason to keep this any longer, even for exceptions
338 handler_context.private = None
340 def _SendResponse(self):
341 """Sends the response to the client.
344 if self.response_msg.start_line.code is None:
347 if not self.response_msg.headers:
348 self.response_msg.headers = {}
350 self.response_msg.headers.update({
351 # TODO: Keep-alive is not supported
352 http.HTTP_CONNECTION: "close",
353 http.HTTP_DATE: _DateTimeHeader(),
354 http.HTTP_SERVER: http.HTTP_GANETI_VERSION,
357 # Get response reason based on code
358 response_code = self.response_msg.start_line.code
359 if response_code in self.responses:
360 response_reason = self.responses[response_code][0]
363 self.response_msg.start_line.reason = response_reason
365 logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1],
366 self.request_msg.start_line, response_code)
369 _HttpServerToClientMessageWriter(self.sock, self.request_msg,
370 self.response_msg, self.WRITE_TIMEOUT)
371 except http.HttpSocketTimeout:
372 raise http.HttpError("Timeout while sending response")
373 except socket.error, err:
374 raise http.HttpError("Error sending response: %s" % err)
376 def _SetErrorStatus(self, err):
377 """Sets the response code and body from a HttpException.
379 @type err: HttpException
380 @param err: Exception instance
384 (shortmsg, longmsg) = self.responses[err.code]
386 shortmsg = longmsg = "Unknown"
389 message = err.message
395 "message": cgi.escape(message),
399 self.response_msg.start_line.code = err.code
403 headers.update(err.headers)
404 headers[http.HTTP_CONTENT_TYPE] = self.error_content_type
405 self.response_msg.headers = headers
407 self.response_msg.body = self.error_message_format % values
410 class HttpServer(http.HttpBase):
411 """Generic HTTP server class
413 Users of this class must subclass it and override the HandleRequest function.
418 def __init__(self, mainloop, local_address, port,
419 ssl_params=None, ssl_verify_peer=False):
420 """Initializes the HTTP server
422 @type mainloop: ganeti.daemon.Mainloop
423 @param mainloop: Mainloop used to poll for I/O events
424 @type local_address: string
425 @param local_address: Local IP address to bind to
427 @param port: TCP port to listen on
428 @type ssl_params: HttpSslParams
429 @param ssl_params: SSL key and certificate
430 @type ssl_verify_peer: bool
431 @param ssl_verify_peer: Whether to require client certificate and compare
432 it with our certificate
435 http.HttpBase.__init__(self)
437 self.mainloop = mainloop
438 self.local_address = local_address
441 self.socket = self._CreateSocket(ssl_params, ssl_verify_peer)
443 # Allow port to be reused
444 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
448 mainloop.RegisterIO(self, self.socket.fileno(), select.POLLIN)
449 mainloop.RegisterSignal(self)
452 self.socket.bind((self.local_address, self.port))
453 self.socket.listen(1024)
458 def OnIO(self, fd, condition):
459 if condition & select.POLLIN:
460 self._IncomingConnection()
462 def OnSignal(self, signum):
463 if signum == signal.SIGCHLD:
464 self._CollectChildren(True)
466 def _CollectChildren(self, quick):
467 """Checks whether any child processes are done
470 @param quick: Whether to only use non-blocking functions
474 # Don't wait for other processes if it should be a quick check
475 while len(self._children) > self.MAX_CHILDREN:
477 # Waiting without a timeout brings us into a potential DoS situation.
478 # As soon as too many children run, we'll not respond to new
479 # requests. The real solution would be to add a timeout for children
480 # and killing them after some time.
481 pid, status = os.waitpid(0, 0)
484 if pid and pid in self._children:
485 self._children.remove(pid)
487 for child in self._children:
489 pid, status = os.waitpid(child, os.WNOHANG)
492 if pid and pid in self._children:
493 self._children.remove(pid)
495 def _IncomingConnection(self):
496 """Called for each incoming connection
499 (connection, client_addr) = self.socket.accept()
501 self._CollectChildren(False)
507 _HttpServerRequestExecutor(self, connection, client_addr)
509 logging.exception("Error while handling request from %s:%s",
510 client_addr[0], client_addr[1])
514 self._children.append(pid)
516 def PreHandleRequest(self, req):
517 """Called before handling a request.
519 Can be overriden by a subclass.
523 def HandleRequest(self, req):
524 """Handles a request.
526 Must be overriden by subclass.
529 raise NotImplementedError()