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 = {}
90 class _HttpServerToClientMessageWriter(http.HttpMessageWriter):
91 """Writes an HTTP response to client.
94 def __init__(self, sock, request_msg, response_msg, write_timeout):
95 """Writes the response to the client.
98 @param sock: Target socket
99 @type request_msg: http.HttpMessage
100 @param request_msg: Request message, required to determine whether
101 response may have a message body
102 @type response_msg: http.HttpMessage
103 @param response_msg: Response message
104 @type write_timeout: float
105 @param write_timeout: Write timeout for socket
108 self._request_msg = request_msg
109 self._response_msg = response_msg
110 http.HttpMessageWriter.__init__(self, sock, response_msg, write_timeout)
112 def HasMessageBody(self):
113 """Logic to detect whether response should contain a message body.
116 if self._request_msg.start_line:
117 request_method = self._request_msg.start_line.method
119 request_method = None
121 response_code = self._response_msg.start_line.code
123 # RFC2616, section 4.3: "A message-body MUST NOT be included in a request
124 # if the specification of the request method (section 5.1.1) does not allow
125 # sending an entity-body in requests"
127 # RFC2616, section 9.4: "The HEAD method is identical to GET except that
128 # the server MUST NOT return a message-body in the response."
130 # RFC2616, section 10.2.5: "The 204 response MUST NOT include a
131 # message-body [...]"
133 # RFC2616, section 10.3.5: "The 304 response MUST NOT contain a
134 # message-body, [...]"
136 return (http.HttpMessageWriter.HasMessageBody(self) and
137 (request_method is not None and
138 request_method != http.HTTP_HEAD) and
139 response_code >= http.HTTP_OK and
140 response_code not in (http.HTTP_NO_CONTENT,
141 http.HTTP_NOT_MODIFIED))
144 class _HttpClientToServerMessageReader(http.HttpMessageReader):
145 """Reads an HTTP request sent by client.
149 START_LINE_LENGTH_MAX = 4096
150 HEADER_LENGTH_MAX = 4096
152 def ParseStartLine(self, start_line):
153 """Parses the start line sent by client.
155 Example: "GET /index.html HTTP/1.1"
157 @type start_line: string
158 @param start_line: Start line
161 # Empty lines are skipped when reading
164 logging.debug("HTTP request: %s", start_line)
166 words = start_line.split()
169 [method, path, version] = words
170 if version[:5] != 'HTTP/':
171 raise http.HttpBadRequest("Bad request version (%r)" % version)
174 base_version_number = version.split("/", 1)[1]
175 version_number = base_version_number.split(".")
177 # RFC 2145 section 3.1 says there can be only one "." and
178 # - major and minor numbers MUST be treated as
180 # - HTTP/2.4 is a lower version than HTTP/2.13, which in
181 # turn is lower than HTTP/12.3;
182 # - Leading zeros MUST be ignored by recipients.
183 if len(version_number) != 2:
184 raise http.HttpBadRequest("Bad request version (%r)" % version)
186 version_number = (int(version_number[0]), int(version_number[1]))
187 except (ValueError, IndexError):
188 raise http.HttpBadRequest("Bad request version (%r)" % version)
190 if version_number >= (2, 0):
191 raise http.HttpVersionNotSupported("Invalid HTTP Version (%s)" %
194 elif len(words) == 2:
195 version = http.HTTP_0_9
196 [method, path] = words
197 if method != http.HTTP_GET:
198 raise http.HttpBadRequest("Bad HTTP/0.9 request type (%r)" % method)
201 raise http.HttpBadRequest("Bad request syntax (%r)" % start_line)
203 return http.HttpClientToServerStartLine(method, path, version)
206 class _HttpServerRequestExecutor(object):
207 """Implements server side of HTTP.
209 This class implements the server side of HTTP. It's based on code of Python's
210 BaseHTTPServer, from both version 2.4 and 3k. It does not support non-ASCII
211 character encodings. Keep-alive connections are not supported.
214 # The default request version. This only affects responses up until
215 # the point where the request line is parsed, so it mainly decides what
216 # the client gets back when sending a malformed request line.
217 # Most web servers default to HTTP 0.9, i.e. don't send a status line.
218 default_request_version = http.HTTP_0_9
220 # Error message settings
221 error_message_format = DEFAULT_ERROR_MESSAGE
222 error_content_type = DEFAULT_ERROR_CONTENT_TYPE
224 responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
226 # Timeouts in seconds for socket layer
231 def __init__(self, server, sock, client_addr):
232 """Initializes this class.
237 self.client_addr = client_addr
239 self.request_msg = http.HttpMessage()
240 self.response_msg = http.HttpMessage()
242 self.response_msg.start_line = \
243 http.HttpServerToClientStartLine(version=self.default_request_version,
244 code=None, reason=None)
246 # Disable Python's timeout
247 self.sock.settimeout(None)
249 # Operate in non-blocking mode
250 self.sock.setblocking(0)
252 logging.info("Connection from %s:%s", client_addr[0], client_addr[1])
254 request_msg_reader = None
257 # Do the secret SSL handshake
258 if self.server.using_ssl:
259 self.sock.set_accept_state()
261 http.Handshake(self.sock, self.WRITE_TIMEOUT)
262 except http.HttpSessionHandshakeUnexpectedEOF:
268 request_msg_reader = self._ReadRequest()
269 self._HandleRequest()
271 # Only wait for client to close if we didn't have any exception.
273 except http.HttpException, err:
274 self._SetErrorStatus(err)
276 # Try to send a response
279 http.ShutdownConnection(sock, self.CLOSE_TIMEOUT, self.WRITE_TIMEOUT,
280 request_msg_reader, force_close)
285 logging.info("Disconnected %s:%s", client_addr[0], client_addr[1])
287 def _ReadRequest(self):
288 """Reads a request sent by client.
292 request_msg_reader = \
293 _HttpClientToServerMessageReader(self.sock, self.request_msg,
295 except http.HttpSocketTimeout:
296 raise http.HttpError("Timeout while reading request")
297 except socket.error, err:
298 raise http.HttpError("Error reading request: %s" % err)
300 self.response_msg.start_line.version = self.request_msg.start_line.version
302 return request_msg_reader
304 def _HandleRequest(self):
305 """Calls the handler function for the current request.
308 handler_context = _HttpServerRequest(self.request_msg)
311 # Authentication, etc.
312 self.server.PreHandleRequest(handler_context)
314 # Call actual request handler
315 result = self.server.HandleRequest(handler_context)
316 except (http.HttpException, KeyboardInterrupt, SystemExit):
318 except Exception, err:
319 logging.exception("Caught exception")
320 raise http.HttpInternalServerError(message=str(err))
322 logging.exception("Unknown exception")
323 raise http.HttpInternalServerError(message="Unknown error")
326 encoder = http.HttpJsonConverter()
327 self.response_msg.start_line.code = http.HTTP_OK
328 self.response_msg.body = encoder.Encode(result)
329 self.response_msg.headers = handler_context.resp_headers
330 self.response_msg.headers[http.HTTP_CONTENT_TYPE] = encoder.CONTENT_TYPE
332 def _SendResponse(self):
333 """Sends the response to the client.
336 if self.response_msg.start_line.code is None:
339 if not self.response_msg.headers:
340 self.response_msg.headers = {}
342 self.response_msg.headers.update({
343 # TODO: Keep-alive is not supported
344 http.HTTP_CONNECTION: "close",
345 http.HTTP_DATE: _DateTimeHeader(),
346 http.HTTP_SERVER: http.HTTP_GANETI_VERSION,
349 # Get response reason based on code
350 response_code = self.response_msg.start_line.code
351 if response_code in self.responses:
352 response_reason = self.responses[response_code][0]
355 self.response_msg.start_line.reason = response_reason
357 logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1],
358 self.request_msg.start_line, response_code)
361 _HttpServerToClientMessageWriter(self.sock, self.request_msg,
362 self.response_msg, self.WRITE_TIMEOUT)
363 except http.HttpSocketTimeout:
364 raise http.HttpError("Timeout while sending response")
365 except socket.error, err:
366 raise http.HttpError("Error sending response: %s" % err)
368 def _SetErrorStatus(self, err):
369 """Sets the response code and body from a HttpException.
371 @type err: HttpException
372 @param err: Exception instance
376 (shortmsg, longmsg) = self.responses[err.code]
378 shortmsg = longmsg = "Unknown"
381 message = err.message
387 "message": cgi.escape(message),
391 self.response_msg.start_line.code = err.code
395 headers.update(err.headers)
396 headers[http.HTTP_CONTENT_TYPE] = self.error_content_type
397 self.response_msg.headers = headers
399 self.response_msg.body = self.error_message_format % values
402 class HttpServer(http.HttpBase):
403 """Generic HTTP server class
405 Users of this class must subclass it and override the HandleRequest function.
410 def __init__(self, mainloop, local_address, port,
411 ssl_params=None, ssl_verify_peer=False):
412 """Initializes the HTTP server
414 @type mainloop: ganeti.daemon.Mainloop
415 @param mainloop: Mainloop used to poll for I/O events
416 @type local_address: string
417 @param local_address: Local IP address to bind to
419 @param port: TCP port to listen on
420 @type ssl_params: HttpSslParams
421 @param ssl_params: SSL key and certificate
422 @type ssl_verify_peer: bool
423 @param ssl_verify_peer: Whether to require client certificate and compare
424 it with our certificate
427 http.HttpBase.__init__(self)
429 self.mainloop = mainloop
430 self.local_address = local_address
433 self.socket = self._CreateSocket(ssl_params, ssl_verify_peer)
435 # Allow port to be reused
436 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
440 mainloop.RegisterIO(self, self.socket.fileno(), select.POLLIN)
441 mainloop.RegisterSignal(self)
444 self.socket.bind((self.local_address, self.port))
445 self.socket.listen(1024)
450 def OnIO(self, fd, condition):
451 if condition & select.POLLIN:
452 self._IncomingConnection()
454 def OnSignal(self, signum):
455 if signum == signal.SIGCHLD:
456 self._CollectChildren(True)
458 def _CollectChildren(self, quick):
459 """Checks whether any child processes are done
462 @param quick: Whether to only use non-blocking functions
466 # Don't wait for other processes if it should be a quick check
467 while len(self._children) > self.MAX_CHILDREN:
469 # Waiting without a timeout brings us into a potential DoS situation.
470 # As soon as too many children run, we'll not respond to new
471 # requests. The real solution would be to add a timeout for children
472 # and killing them after some time.
473 pid, status = os.waitpid(0, 0)
476 if pid and pid in self._children:
477 self._children.remove(pid)
479 for child in self._children:
481 pid, status = os.waitpid(child, os.WNOHANG)
484 if pid and pid in self._children:
485 self._children.remove(pid)
487 def _IncomingConnection(self):
488 """Called for each incoming connection
491 (connection, client_addr) = self.socket.accept()
493 self._CollectChildren(False)
499 _HttpServerRequestExecutor(self, connection, client_addr)
501 logging.exception("Error while handling request from %s:%s",
502 client_addr[0], client_addr[1])
506 self._children.append(pid)
508 def PreHandleRequest(self, req):
509 """Called before handling a request.
511 Can be overriden by a subclass.
515 def HandleRequest(self, req):
516 """Handles a request.
518 Must be overriden by subclass.
521 raise NotImplementedError()