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
214 Python's BaseHTTPServer, from both version 2.4 and 3k. It does not
215 support non-ASCII character encodings. Keep-alive connections are
219 # The default request version. This only affects responses up until
220 # the point where the request line is parsed, so it mainly decides what
221 # the client gets back when sending a malformed request line.
222 # Most web servers default to HTTP 0.9, i.e. don't send a status line.
223 default_request_version = http.HTTP_0_9
225 # Error message settings
226 error_message_format = DEFAULT_ERROR_MESSAGE
227 error_content_type = DEFAULT_ERROR_CONTENT_TYPE
229 responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
231 # Timeouts in seconds for socket layer
236 def __init__(self, server, sock, client_addr):
237 """Initializes this class.
242 self.client_addr = client_addr
244 self.request_msg = http.HttpMessage()
245 self.response_msg = http.HttpMessage()
247 self.response_msg.start_line = \
248 http.HttpServerToClientStartLine(version=self.default_request_version,
249 code=None, reason=None)
251 # Disable Python's timeout
252 self.sock.settimeout(None)
254 # Operate in non-blocking mode
255 self.sock.setblocking(0)
257 logging.debug("Connection from %s:%s", client_addr[0], client_addr[1])
259 request_msg_reader = None
262 # Do the secret SSL handshake
263 if self.server.using_ssl:
264 self.sock.set_accept_state()
266 http.Handshake(self.sock, self.WRITE_TIMEOUT)
267 except http.HttpSessionHandshakeUnexpectedEOF:
273 request_msg_reader = self._ReadRequest()
274 self._HandleRequest()
276 # Only wait for client to close if we didn't have any exception.
278 except http.HttpException, err:
279 self._SetErrorStatus(err)
281 # Try to send a response
284 http.ShutdownConnection(sock, self.CLOSE_TIMEOUT, self.WRITE_TIMEOUT,
285 request_msg_reader, force_close)
290 logging.debug("Disconnected %s:%s", client_addr[0], client_addr[1])
292 def _ReadRequest(self):
293 """Reads a request sent by client.
297 request_msg_reader = \
298 _HttpClientToServerMessageReader(self.sock, self.request_msg,
300 except http.HttpSocketTimeout:
301 raise http.HttpError("Timeout while reading request")
302 except socket.error, err:
303 raise http.HttpError("Error reading request: %s" % err)
305 self.response_msg.start_line.version = self.request_msg.start_line.version
307 return request_msg_reader
309 def _HandleRequest(self):
310 """Calls the handler function for the current request.
313 handler_context = _HttpServerRequest(self.request_msg)
317 # Authentication, etc.
318 self.server.PreHandleRequest(handler_context)
320 # Call actual request handler
321 result = self.server.HandleRequest(handler_context)
322 except (http.HttpException, KeyboardInterrupt, SystemExit):
324 except Exception, err:
325 logging.exception("Caught exception")
326 raise http.HttpInternalServerError(message=str(err))
328 logging.exception("Unknown exception")
329 raise http.HttpInternalServerError(message="Unknown error")
332 encoder = http.HttpJsonConverter()
333 self.response_msg.start_line.code = http.HTTP_OK
334 self.response_msg.body = encoder.Encode(result)
335 self.response_msg.headers = handler_context.resp_headers
336 self.response_msg.headers[http.HTTP_CONTENT_TYPE] = encoder.CONTENT_TYPE
338 # No reason to keep this any longer, even for exceptions
339 handler_context.private = None
341 def _SendResponse(self):
342 """Sends the response to the client.
345 if self.response_msg.start_line.code is None:
348 if not self.response_msg.headers:
349 self.response_msg.headers = {}
351 self.response_msg.headers.update({
352 # TODO: Keep-alive is not supported
353 http.HTTP_CONNECTION: "close",
354 http.HTTP_DATE: _DateTimeHeader(),
355 http.HTTP_SERVER: http.HTTP_GANETI_VERSION,
358 # Get response reason based on code
359 response_code = self.response_msg.start_line.code
360 if response_code in self.responses:
361 response_reason = self.responses[response_code][0]
364 self.response_msg.start_line.reason = response_reason
366 logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1],
367 self.request_msg.start_line, response_code)
370 _HttpServerToClientMessageWriter(self.sock, self.request_msg,
371 self.response_msg, self.WRITE_TIMEOUT)
372 except http.HttpSocketTimeout:
373 raise http.HttpError("Timeout while sending response")
374 except socket.error, err:
375 raise http.HttpError("Error sending response: %s" % err)
377 def _SetErrorStatus(self, err):
378 """Sets the response code and body from a HttpException.
380 @type err: HttpException
381 @param err: Exception instance
385 (shortmsg, longmsg) = self.responses[err.code]
387 shortmsg = longmsg = "Unknown"
390 message = err.message
396 "message": cgi.escape(message),
400 self.response_msg.start_line.code = err.code
404 headers.update(err.headers)
405 headers[http.HTTP_CONTENT_TYPE] = self.error_content_type
406 self.response_msg.headers = headers
408 self.response_msg.body = self._FormatErrorMessage(values)
410 def _FormatErrorMessage(self, values):
411 """Formats the body of an error message.
414 @param values: dictionary with keys code, message and explain.
416 @return: the body of the message
419 return self.error_message_format % values
421 class HttpServer(http.HttpBase):
422 """Generic HTTP server class
424 Users of this class must subclass it and override the HandleRequest function.
429 def __init__(self, mainloop, local_address, port,
430 ssl_params=None, ssl_verify_peer=False,
431 request_executor_class=None):
432 """Initializes the HTTP server
434 @type mainloop: ganeti.daemon.Mainloop
435 @param mainloop: Mainloop used to poll for I/O events
436 @type local_address: string
437 @param local_address: Local IP address to bind to
439 @param port: TCP port to listen on
440 @type ssl_params: HttpSslParams
441 @param ssl_params: SSL key and certificate
442 @type ssl_verify_peer: bool
443 @param ssl_verify_peer: Whether to require client certificate
444 and compare it with our certificate
445 @type request_executor_class: class
446 @param request_executor_class: an class derived from the
447 HttpServerRequestExecutor class
450 http.HttpBase.__init__(self)
452 if request_executor_class is None:
453 self.request_executor = HttpServerRequestExecutor
455 self.request_executor = request_executor_class
457 self.mainloop = mainloop
458 self.local_address = local_address
461 self.socket = self._CreateSocket(ssl_params, ssl_verify_peer)
463 # Allow port to be reused
464 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
468 mainloop.RegisterIO(self, self.socket.fileno(), select.POLLIN)
469 mainloop.RegisterSignal(self)
472 self.socket.bind((self.local_address, self.port))
473 self.socket.listen(1024)
478 def OnIO(self, fd, condition):
479 if condition & select.POLLIN:
480 self._IncomingConnection()
482 def OnSignal(self, signum):
483 if signum == signal.SIGCHLD:
484 self._CollectChildren(True)
486 def _CollectChildren(self, quick):
487 """Checks whether any child processes are done
490 @param quick: Whether to only use non-blocking functions
494 # Don't wait for other processes if it should be a quick check
495 while len(self._children) > self.MAX_CHILDREN:
497 # Waiting without a timeout brings us into a potential DoS situation.
498 # As soon as too many children run, we'll not respond to new
499 # requests. The real solution would be to add a timeout for children
500 # and killing them after some time.
501 pid, status = os.waitpid(0, 0)
504 if pid and pid in self._children:
505 self._children.remove(pid)
507 for child in self._children:
509 pid, status = os.waitpid(child, os.WNOHANG)
512 if pid and pid in self._children:
513 self._children.remove(pid)
515 def _IncomingConnection(self):
516 """Called for each incoming connection
519 (connection, client_addr) = self.socket.accept()
521 self._CollectChildren(False)
527 self.request_executor(self, connection, client_addr)
529 logging.exception("Error while handling request from %s:%s",
530 client_addr[0], client_addr[1])
534 self._children.append(pid)
536 def PreHandleRequest(self, req):
537 """Called before handling a request.
539 Can be overriden by a subclass.
543 def HandleRequest(self, req):
544 """Handles a request.
546 Must be overriden by subclass.
549 raise NotImplementedError()