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.poller = select.poll()
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.info("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.poller, 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(self.poller, sock,
282 self.CLOSE_TIMEOUT, self.WRITE_TIMEOUT,
283 request_msg_reader, force_close)
288 logging.info("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)
314 result = self.server.HandleRequest(handler_context)
315 except (http.HttpException, KeyboardInterrupt, SystemExit):
317 except Exception, err:
318 logging.exception("Caught exception")
319 raise http.HttpInternalServerError(message=str(err))
321 logging.exception("Unknown exception")
322 raise http.HttpInternalServerError(message="Unknown error")
325 encoder = http.HttpJsonConverter()
326 self.response_msg.start_line.code = http.HTTP_OK
327 self.response_msg.body = encoder.Encode(result)
328 self.response_msg.headers = handler_context.resp_headers
329 self.response_msg.headers[http.HTTP_CONTENT_TYPE] = encoder.CONTENT_TYPE
331 def _SendResponse(self):
332 """Sends the response to the client.
335 if self.response_msg.start_line.code is None:
338 if not self.response_msg.headers:
339 self.response_msg.headers = {}
341 self.response_msg.headers.update({
342 # TODO: Keep-alive is not supported
343 http.HTTP_CONNECTION: "close",
344 http.HTTP_DATE: _DateTimeHeader(),
345 http.HTTP_SERVER: http.HTTP_GANETI_VERSION,
348 # Get response reason based on code
349 response_code = self.response_msg.start_line.code
350 if response_code in self.responses:
351 response_reason = self.responses[response_code][0]
354 self.response_msg.start_line.reason = response_reason
356 logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1],
357 self.request_msg.start_line, response_code)
360 _HttpServerToClientMessageWriter(self.sock, self.request_msg,
361 self.response_msg, self.WRITE_TIMEOUT)
362 except http.HttpSocketTimeout:
363 raise http.HttpError("Timeout while sending response")
364 except socket.error, err:
365 raise http.HttpError("Error sending response: %s" % err)
367 def _SetErrorStatus(self, err):
368 """Sets the response code and body from a HttpException.
370 @type err: HttpException
371 @param err: Exception instance
375 (shortmsg, longmsg) = self.responses[err.code]
377 shortmsg = longmsg = "Unknown"
380 message = err.message
386 "message": cgi.escape(message),
390 self.response_msg.start_line.code = err.code
394 headers.update(err.headers)
395 headers[http.HTTP_CONTENT_TYPE] = self.error_content_type
396 self.response_msg.headers = headers
398 self.response_msg.body = self.error_message_format % values
401 class HttpServer(http.HttpBase):
402 """Generic HTTP server class
404 Users of this class must subclass it and override the HandleRequest function.
409 def __init__(self, mainloop, local_address, port,
410 ssl_params=None, ssl_verify_peer=False):
411 """Initializes the HTTP server
413 @type mainloop: ganeti.daemon.Mainloop
414 @param mainloop: Mainloop used to poll for I/O events
415 @type local_address: string
416 @param local_address: Local IP address to bind to
418 @param port: TCP port to listen on
419 @type ssl_params: HttpSslParams
420 @param ssl_params: SSL key and certificate
421 @type ssl_verify_peer: bool
422 @param ssl_verify_peer: Whether to require client certificate and compare
423 it with our certificate
426 http.HttpBase.__init__(self)
428 self.mainloop = mainloop
429 self.local_address = local_address
432 self.socket = self._CreateSocket(ssl_params, ssl_verify_peer)
434 # Allow port to be reused
435 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
439 mainloop.RegisterIO(self, self.socket.fileno(), select.POLLIN)
440 mainloop.RegisterSignal(self)
443 self.socket.bind((self.local_address, self.port))
444 self.socket.listen(1024)
449 def OnIO(self, fd, condition):
450 if condition & select.POLLIN:
451 self._IncomingConnection()
453 def OnSignal(self, signum):
454 if signum == signal.SIGCHLD:
455 self._CollectChildren(True)
457 def _CollectChildren(self, quick):
458 """Checks whether any child processes are done
461 @param quick: Whether to only use non-blocking functions
465 # Don't wait for other processes if it should be a quick check
466 while len(self._children) > self.MAX_CHILDREN:
468 # Waiting without a timeout brings us into a potential DoS situation.
469 # As soon as too many children run, we'll not respond to new
470 # requests. The real solution would be to add a timeout for children
471 # and killing them after some time.
472 pid, status = os.waitpid(0, 0)
475 if pid and pid in self._children:
476 self._children.remove(pid)
478 for child in self._children:
480 pid, status = os.waitpid(child, os.WNOHANG)
483 if pid and pid in self._children:
484 self._children.remove(pid)
486 def _IncomingConnection(self):
487 """Called for each incoming connection
490 (connection, client_addr) = self.socket.accept()
492 self._CollectChildren(False)
498 _HttpServerRequestExecutor(self, connection, client_addr)
500 logging.exception("Error while handling request from %s:%s",
501 client_addr[0], client_addr[1])
505 self._children.append(pid)
507 def HandleRequest(self, req):
508 """Handles a request.
510 Must be overriden by subclass.
513 raise NotImplementedError()