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, request_msg):
79 self.request_method = request_msg.start_line.method
80 self.request_path = request_msg.start_line.path
81 self.request_headers = request_msg.headers
82 self.request_body = request_msg.decoded_body
85 self.resp_headers = {}
87 # Private data for request handler (useful in combination with
92 class _HttpServerToClientMessageWriter(http.HttpMessageWriter):
93 """Writes an HTTP response to client.
96 def __init__(self, sock, request_msg, response_msg, write_timeout):
97 """Writes the response to the client.
100 @param sock: Target socket
101 @type request_msg: http.HttpMessage
102 @param request_msg: Request message, required to determine whether
103 response may have a message body
104 @type response_msg: http.HttpMessage
105 @param response_msg: Response message
106 @type write_timeout: float
107 @param write_timeout: Write timeout for socket
110 self._request_msg = request_msg
111 self._response_msg = response_msg
112 http.HttpMessageWriter.__init__(self, sock, response_msg, write_timeout)
114 def HasMessageBody(self):
115 """Logic to detect whether response should contain a message body.
118 if self._request_msg.start_line:
119 request_method = self._request_msg.start_line.method
121 request_method = None
123 response_code = self._response_msg.start_line.code
125 # RFC2616, section 4.3: "A message-body MUST NOT be included in a request
126 # if the specification of the request method (section 5.1.1) does not allow
127 # sending an entity-body in requests"
129 # RFC2616, section 9.4: "The HEAD method is identical to GET except that
130 # the server MUST NOT return a message-body in the response."
132 # RFC2616, section 10.2.5: "The 204 response MUST NOT include a
133 # message-body [...]"
135 # RFC2616, section 10.3.5: "The 304 response MUST NOT contain a
136 # message-body, [...]"
138 return (http.HttpMessageWriter.HasMessageBody(self) and
139 (request_method is not None and
140 request_method != http.HTTP_HEAD) and
141 response_code >= http.HTTP_OK and
142 response_code not in (http.HTTP_NO_CONTENT,
143 http.HTTP_NOT_MODIFIED))
146 class _HttpClientToServerMessageReader(http.HttpMessageReader):
147 """Reads an HTTP request sent by client.
151 START_LINE_LENGTH_MAX = 4096
152 HEADER_LENGTH_MAX = 4096
154 def ParseStartLine(self, start_line):
155 """Parses the start line sent by client.
157 Example: "GET /index.html HTTP/1.1"
159 @type start_line: string
160 @param start_line: Start line
163 # Empty lines are skipped when reading
166 logging.debug("HTTP request: %s", start_line)
168 words = start_line.split()
171 [method, path, version] = words
172 if version[:5] != 'HTTP/':
173 raise http.HttpBadRequest("Bad request version (%r)" % version)
176 base_version_number = version.split("/", 1)[1]
177 version_number = base_version_number.split(".")
179 # RFC 2145 section 3.1 says there can be only one "." and
180 # - major and minor numbers MUST be treated as
182 # - HTTP/2.4 is a lower version than HTTP/2.13, which in
183 # turn is lower than HTTP/12.3;
184 # - Leading zeros MUST be ignored by recipients.
185 if len(version_number) != 2:
186 raise http.HttpBadRequest("Bad request version (%r)" % version)
188 version_number = (int(version_number[0]), int(version_number[1]))
189 except (ValueError, IndexError):
190 raise http.HttpBadRequest("Bad request version (%r)" % version)
192 if version_number >= (2, 0):
193 raise http.HttpVersionNotSupported("Invalid HTTP Version (%s)" %
196 elif len(words) == 2:
197 version = http.HTTP_0_9
198 [method, path] = words
199 if method != http.HTTP_GET:
200 raise http.HttpBadRequest("Bad HTTP/0.9 request type (%r)" % method)
203 raise http.HttpBadRequest("Bad request syntax (%r)" % start_line)
205 return http.HttpClientToServerStartLine(method, path, version)
208 class HttpServerRequestExecutor(object):
209 """Implements server side of HTTP.
211 This class implements the server side of HTTP. It's based on code of
212 Python's BaseHTTPServer, from both version 2.4 and 3k. It does not
213 support non-ASCII character encodings. Keep-alive connections are
217 # The default request version. This only affects responses up until
218 # the point where the request line is parsed, so it mainly decides what
219 # the client gets back when sending a malformed request line.
220 # Most web servers default to HTTP 0.9, i.e. don't send a status line.
221 default_request_version = http.HTTP_0_9
223 # Error message settings
224 error_message_format = DEFAULT_ERROR_MESSAGE
225 error_content_type = DEFAULT_ERROR_CONTENT_TYPE
227 responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
229 # Timeouts in seconds for socket layer
234 def __init__(self, server, sock, client_addr):
235 """Initializes this class.
240 self.client_addr = client_addr
242 self.request_msg = http.HttpMessage()
243 self.response_msg = http.HttpMessage()
245 self.response_msg.start_line = \
246 http.HttpServerToClientStartLine(version=self.default_request_version,
247 code=None, reason=None)
249 # Disable Python's timeout
250 self.sock.settimeout(None)
252 # Operate in non-blocking mode
253 self.sock.setblocking(0)
255 logging.debug("Connection from %s:%s", client_addr[0], client_addr[1])
257 request_msg_reader = None
260 # Do the secret SSL handshake
261 if self.server.using_ssl:
262 self.sock.set_accept_state()
264 http.Handshake(self.sock, self.WRITE_TIMEOUT)
265 except http.HttpSessionHandshakeUnexpectedEOF:
271 request_msg_reader = self._ReadRequest()
272 self._HandleRequest()
274 # Only wait for client to close if we didn't have any exception.
276 except http.HttpException, err:
277 self._SetErrorStatus(err)
279 # Try to send a response
282 http.ShutdownConnection(sock, self.CLOSE_TIMEOUT, self.WRITE_TIMEOUT,
283 request_msg_reader, force_close)
288 logging.debug("Disconnected %s:%s", client_addr[0], client_addr[1])
290 def _ReadRequest(self):
291 """Reads a request sent by client.
295 request_msg_reader = \
296 _HttpClientToServerMessageReader(self.sock, self.request_msg,
298 except http.HttpSocketTimeout:
299 raise http.HttpError("Timeout while reading request")
300 except socket.error, err:
301 raise http.HttpError("Error reading request: %s" % err)
303 self.response_msg.start_line.version = self.request_msg.start_line.version
305 return request_msg_reader
307 def _HandleRequest(self):
308 """Calls the handler function for the current request.
311 handler_context = _HttpServerRequest(self.request_msg)
315 # Authentication, etc.
316 self.server.PreHandleRequest(handler_context)
318 # Call actual request handler
319 result = self.server.HandleRequest(handler_context)
320 except (http.HttpException, KeyboardInterrupt, SystemExit):
322 except Exception, err:
323 logging.exception("Caught exception")
324 raise http.HttpInternalServerError(message=str(err))
326 logging.exception("Unknown exception")
327 raise http.HttpInternalServerError(message="Unknown error")
330 encoder = http.HttpJsonConverter()
331 self.response_msg.start_line.code = http.HTTP_OK
332 self.response_msg.body = encoder.Encode(result)
333 self.response_msg.headers = handler_context.resp_headers
334 self.response_msg.headers[http.HTTP_CONTENT_TYPE] = encoder.CONTENT_TYPE
336 # No reason to keep this any longer, even for exceptions
337 handler_context.private = None
339 def _SendResponse(self):
340 """Sends the response to the client.
343 if self.response_msg.start_line.code is None:
346 if not self.response_msg.headers:
347 self.response_msg.headers = {}
349 self.response_msg.headers.update({
350 # TODO: Keep-alive is not supported
351 http.HTTP_CONNECTION: "close",
352 http.HTTP_DATE: _DateTimeHeader(),
353 http.HTTP_SERVER: http.HTTP_GANETI_VERSION,
356 # Get response reason based on code
357 response_code = self.response_msg.start_line.code
358 if response_code in self.responses:
359 response_reason = self.responses[response_code][0]
362 self.response_msg.start_line.reason = response_reason
364 logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1],
365 self.request_msg.start_line, response_code)
368 _HttpServerToClientMessageWriter(self.sock, self.request_msg,
369 self.response_msg, self.WRITE_TIMEOUT)
370 except http.HttpSocketTimeout:
371 raise http.HttpError("Timeout while sending response")
372 except socket.error, err:
373 raise http.HttpError("Error sending response: %s" % err)
375 def _SetErrorStatus(self, err):
376 """Sets the response code and body from a HttpException.
378 @type err: HttpException
379 @param err: Exception instance
383 (shortmsg, longmsg) = self.responses[err.code]
385 shortmsg = longmsg = "Unknown"
388 message = err.message
394 "message": cgi.escape(message),
398 self.response_msg.start_line.code = err.code
402 headers.update(err.headers)
403 headers[http.HTTP_CONTENT_TYPE] = self.error_content_type
404 self.response_msg.headers = headers
406 self.response_msg.body = self._FormatErrorMessage(values)
408 def _FormatErrorMessage(self, values):
409 """Formats the body of an error message.
412 @param values: dictionary with keys code, message and explain.
414 @return: the body of the message
417 return self.error_message_format % values
419 class HttpServer(http.HttpBase, asyncore.dispatcher):
420 """Generic HTTP server class
422 Users of this class must subclass it and override the HandleRequest function.
427 def __init__(self, mainloop, local_address, port,
428 ssl_params=None, ssl_verify_peer=False,
429 request_executor_class=None):
430 """Initializes the HTTP server
432 @type mainloop: ganeti.daemon.Mainloop
433 @param mainloop: Mainloop used to poll for I/O events
434 @type local_address: string
435 @param local_address: Local IP address to bind to
437 @param port: TCP port to listen on
438 @type ssl_params: HttpSslParams
439 @param ssl_params: SSL key and certificate
440 @type ssl_verify_peer: bool
441 @param ssl_verify_peer: Whether to require client certificate
442 and compare it with our certificate
443 @type request_executor_class: class
444 @param request_executor_class: an class derived from the
445 HttpServerRequestExecutor class
448 http.HttpBase.__init__(self)
449 asyncore.dispatcher.__init__(self)
451 if request_executor_class is None:
452 self.request_executor = HttpServerRequestExecutor
454 self.request_executor = request_executor_class
456 self.mainloop = mainloop
457 self.local_address = local_address
460 self.socket = self._CreateSocket(ssl_params, ssl_verify_peer)
462 # Allow port to be reused
463 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
466 self.set_socket(self.socket)
467 self.accepting = True
468 mainloop.RegisterSignal(self)
471 self.socket.bind((self.local_address, self.port))
472 self.socket.listen(1024)
477 def handle_accept(self):
478 self._IncomingConnection()
480 def OnSignal(self, signum):
481 if signum == signal.SIGCHLD:
482 self._CollectChildren(True)
484 def _CollectChildren(self, quick):
485 """Checks whether any child processes are done
488 @param quick: Whether to only use non-blocking functions
492 # Don't wait for other processes if it should be a quick check
493 while len(self._children) > self.MAX_CHILDREN:
495 # Waiting without a timeout brings us into a potential DoS situation.
496 # As soon as too many children run, we'll not respond to new
497 # requests. The real solution would be to add a timeout for children
498 # and killing them after some time.
499 pid, _ = os.waitpid(0, 0)
502 if pid and pid in self._children:
503 self._children.remove(pid)
505 for child in self._children:
507 pid, _ = os.waitpid(child, os.WNOHANG)
510 if pid and pid in self._children:
511 self._children.remove(pid)
513 def _IncomingConnection(self):
514 """Called for each incoming connection
517 # pylint: disable-msg=W0212
518 (connection, client_addr) = self.socket.accept()
520 self._CollectChildren(False)
526 # The client shouldn't keep the listening socket open. If the parent
527 # process is restarted, it would fail when there's already something
528 # listening (in this case its own child from a previous run) on the
536 # In case the handler code uses temporary files
537 utils.ResetTempfileModule()
539 self.request_executor(self, connection, client_addr)
540 except Exception: # pylint: disable-msg=W0703
541 logging.exception("Error while handling request from %s:%s",
542 client_addr[0], client_addr[1])
546 self._children.append(pid)
548 def PreHandleRequest(self, req):
549 """Called before handling a request.
551 Can be overridden by a subclass.
555 def HandleRequest(self, req):
556 """Handles a request.
558 Must be overridden by subclass.
561 raise NotImplementedError()