3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful, but
9 # WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 # General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 """HTTP server module.
35 from ganeti import constants
36 from ganeti import serializer
39 WEEKDAYNAME = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
41 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
42 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
44 # Default error message
45 DEFAULT_ERROR_CONTENT_TYPE = "text/html"
46 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 HTTP_NOT_MODIFIED = 304
71 class SocketClosed(socket.error):
75 class HTTPException(Exception):
79 def __init__(self, message=None):
80 Exception.__init__(self)
81 if message is not None:
82 self.message = message
85 class HTTPBadRequest(HTTPException):
89 class HTTPForbidden(HTTPException):
93 class HTTPNotFound(HTTPException):
97 class HTTPGone(HTTPException):
101 class HTTPLengthRequired(HTTPException):
105 class HTTPInternalError(HTTPException):
109 class HTTPNotImplemented(HTTPException):
113 class HTTPServiceUnavailable(HTTPException):
117 class HTTPVersionNotSupported(HTTPException):
122 """Utility class to write HTTP server log files.
124 The written format is the "Common Log Format" as defined by Apache:
125 http://httpd.apache.org/docs/2.2/mod/mod_log_config.html#examples
128 def __init__(self, fd):
129 """Constructor for ApacheLogfile class.
132 - fd: Open file object
137 def LogRequest(self, request, format, *args):
138 self._fd.write("%s %s %s [%s] %s\n" % (
139 # Remote host address
140 request.address_string(),
142 # RFC1413 identity (identd)
149 self._FormatCurrentTime(),
156 def _FormatCurrentTime(self):
157 """Formats current time in Common Log Format.
160 return self._FormatLogTime(time.time())
162 def _FormatLogTime(self, seconds):
163 """Formats time for Common Log Format.
165 All timestamps are logged in the UTC timezone.
168 - seconds: Time in seconds since the epoch
171 (_, month, _, _, _, _, _, _, _) = tm = time.gmtime(seconds)
172 format = "%d/" + MONTHNAME[month] + "/%Y:%H:%M:%S +0000"
173 return time.strftime(format, tm)
176 class HTTPJsonConverter:
177 CONTENT_TYPE = "application/json"
179 def Encode(self, data):
180 return serializer.DumpJson(data)
182 def Decode(self, data):
183 return serializer.LoadJson(data)
186 class _HttpConnectionHandler(object):
187 """Implements server side of HTTP
189 This class implements the server side of HTTP. It's based on code of Python's
190 BaseHTTPServer, from both version 2.4 and 3k. It does not support non-ASCII
191 character encodings. Keep-alive connections are not supported.
194 # String for "Server" header
195 server_version = "Ganeti %s" % constants.RELEASE_VERSION
197 # The default request version. This only affects responses up until
198 # the point where the request line is parsed, so it mainly decides what
199 # the client gets back when sending a malformed request line.
200 # Most web servers default to HTTP 0.9, i.e. don't send a status line.
201 default_request_version = HTTP_0_9
203 # Error message settings
204 error_message_format = DEFAULT_ERROR_MESSAGE
205 error_content_type = DEFAULT_ERROR_CONTENT_TYPE
207 responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
209 def __init__(self, server, conn, client_addr, fileio_class):
210 """Initializes this class.
212 Part of the initialization is reading the request and eventual POST/PUT
213 data sent by the client.
216 self._server = server
218 # We default rfile to buffered because otherwise it could be
219 # really slow for large data (a getc() call per byte); we make
220 # wfile unbuffered because (a) often after a write() we want to
221 # read and we need to flush the line; (b) big writes to unbuffered
222 # files are typically optimized by stdio even when big reads
224 self.rfile = fileio_class(conn, mode="rb", bufsize=-1)
225 self.wfile = fileio_class(conn, mode="wb", bufsize=0)
227 self.client_addr = client_addr
229 self.request_headers = None
230 self.request_method = None
231 self.request_path = None
232 self.request_requestline = None
233 self.request_version = self.default_request_version
235 self.response_body = None
236 self.response_code = HTTP_OK
237 self.response_content_type = None
238 self.response_headers = {}
240 self.should_fork = False
246 self.should_fork = self._server.ForkForRequest(self)
247 except HTTPException, err:
248 self._SetErrorStatus(err)
251 if not self.wfile.closed:
256 def _DateTimeHeader(self):
257 """Return the current date and time formatted for a message header.
260 (year, month, day, hh, mm, ss, wd, _, _) = time.gmtime()
261 return ("%s, %02d %3s %4d %02d:%02d:%02d GMT" %
262 (WEEKDAYNAME[wd], day, MONTHNAME[month], year, hh, mm, ss))
264 def _SetErrorStatus(self, err):
265 """Sets the response code and body from a HTTPException.
267 @type err: HTTPException
268 @param err: Exception instance
272 (shortmsg, longmsg) = self.responses[err.code]
274 shortmsg = longmsg = "Unknown"
277 message = err.message
283 "message": cgi.escape(message),
287 self.response_code = err.code
288 self.response_content_type = self.error_content_type
289 self.response_body = self.error_message_format % values
291 def HandleRequest(self):
292 """Handle the actual request.
294 Calls the actual handler function and converts exceptions into HTTP errors.
297 # Don't do anything if there's already been a problem
298 if self.response_code != HTTP_OK:
301 assert self.request_method, "Status code %s requires a method" % HTTP_OK
303 # Check whether client is still there
308 result = self._server.HandleRequest(self)
311 encoder = HTTPJsonConverter()
312 body = encoder.Encode(result)
314 self.response_content_type = encoder.CONTENT_TYPE
315 self.response_body = body
316 except (HTTPException, KeyboardInterrupt, SystemExit):
318 except Exception, err:
319 logging.exception("Caught exception")
320 raise HTTPInternalError(message=str(err))
322 logging.exception("Unknown exception")
323 raise HTTPInternalError(message="Unknown error")
325 except HTTPException, err:
326 self._SetErrorStatus(err)
328 def SendResponse(self):
329 """Sends response to the client.
332 # Check whether client is still there
335 logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1],
336 self.request_requestline, self.response_code)
338 if self.response_code in self.responses:
339 response_message = self.responses[self.response_code][0]
341 response_message = ""
343 if self.request_version != HTTP_0_9:
344 self.wfile.write("%s %d %s\r\n" %
345 (self.request_version, self.response_code,
347 self._SendHeader("Server", self.server_version)
348 self._SendHeader("Date", self._DateTimeHeader())
349 self._SendHeader("Content-Type", self.response_content_type)
350 self._SendHeader("Content-Length", str(len(self.response_body)))
351 for key, val in self.response_headers.iteritems():
352 self._SendHeader(key, val)
354 # We don't support keep-alive at this time
355 self._SendHeader("Connection", "close")
356 self.wfile.write("\r\n")
358 if (self.request_method != HTTP_HEAD and
359 self.response_code >= HTTP_OK and
360 self.response_code not in (HTTP_NO_CONTENT, HTTP_NOT_MODIFIED)):
361 self.wfile.write(self.response_body)
363 def _SendHeader(self, name, value):
364 if self.request_version != HTTP_0_9:
365 self.wfile.write("%s: %s\r\n" % (name, value))
367 def _ReadRequest(self):
368 """Reads and parses request line
371 raw_requestline = self.rfile.readline()
373 requestline = raw_requestline
374 if requestline[-2:] == '\r\n':
375 requestline = requestline[:-2]
376 elif requestline[-1:] == '\n':
377 requestline = requestline[:-1]
380 raise HTTPBadRequest("Empty request line")
382 self.request_requestline = requestline
384 logging.debug("HTTP request: %s", raw_requestline.rstrip("\r\n"))
386 words = requestline.split()
389 [method, path, version] = words
390 if version[:5] != 'HTTP/':
391 raise HTTPBadRequest("Bad request version (%r)" % version)
394 base_version_number = version.split('/', 1)[1]
395 version_number = base_version_number.split(".")
397 # RFC 2145 section 3.1 says there can be only one "." and
398 # - major and minor numbers MUST be treated as
400 # - HTTP/2.4 is a lower version than HTTP/2.13, which in
401 # turn is lower than HTTP/12.3;
402 # - Leading zeros MUST be ignored by recipients.
403 if len(version_number) != 2:
404 raise HTTPBadRequest("Bad request version (%r)" % version)
406 version_number = int(version_number[0]), int(version_number[1])
407 except (ValueError, IndexError):
408 raise HTTPBadRequest("Bad request version (%r)" % version)
410 if version_number >= (2, 0):
411 raise HTTPVersionNotSupported("Invalid HTTP Version (%s)" %
414 elif len(words) == 2:
416 [method, path] = words
417 if method != HTTP_GET:
418 raise HTTPBadRequest("Bad HTTP/0.9 request type (%r)" % method)
421 raise HTTPBadRequest("Bad request syntax (%r)" % requestline)
423 # Examine the headers and look for a Connection directive
424 headers = mimetools.Message(self.rfile, 0)
426 self.request_method = method
427 self.request_path = path
428 self.request_version = version
429 self.request_headers = headers
431 def _ReadPostData(self):
432 """Reads POST/PUT data
435 if not self.request_method or self.request_method.upper() not in ("POST", "PUT"):
436 self.request_post_data = None
439 # TODO: Decide what to do when Content-Length header was not sent
441 content_length = int(self.request_headers.get('Content-Length', 0))
443 raise HTTPBadRequest("No Content-Length header or invalid format")
445 data = self.rfile.read(content_length)
447 # TODO: Content-type, error handling
448 self.request_post_data = HTTPJsonConverter().Decode(data)
450 logging.debug("HTTP POST data: %s", self.request_post_data)
453 class HttpServer(object):
454 """Generic HTTP server class
456 Users of this class must subclass it and override the HandleRequest function.
457 Optionally, the ForkForRequest function can be overriden.
462 def __init__(self, mainloop, server_address):
463 self.mainloop = mainloop
464 self.server_address = server_address
468 self.ssl_key = self.ssl_cert
470 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
472 if self.ssl_cert and self.ssl_key:
473 ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
474 ctx.set_options(OpenSSL.SSL.OP_NO_SSLv2)
476 ctx.use_certificate_file(self.ssl_cert)
477 ctx.use_privatekey_file(self.ssl_key)
479 self.socket = OpenSSL.SSL.Connection(ctx, sock)
480 self._fileio_class = _SSLFileObject
483 self._fileio_class = socket._fileobject
485 # Allow port to be reused
486 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
490 mainloop.RegisterIO(self, self.socket.fileno(), select.POLLIN)
491 mainloop.RegisterSignal(self)
494 self.socket.bind(self.server_address)
495 self.socket.listen(5)
500 def OnIO(self, fd, condition):
501 if condition & select.POLLIN:
502 self._IncomingConnection()
504 def OnSignal(self, signum):
505 if signum == signal.SIGCHLD:
506 self._CollectChildren(True)
508 def _CollectChildren(self, quick):
509 """Checks whether any child processes are done
512 @param quick: Whether to only use non-blocking functions
516 # Don't wait for other processes if it should be a quick check
517 while len(self._children) > self.MAX_CHILDREN:
519 pid, status = os.waitpid(0, 0)
522 if pid and pid in self._children:
523 self._children.remove(pid)
525 for child in self._children:
527 pid, status = os.waitpid(child, os.WNOHANG)
530 if pid and pid in self._children:
531 self._children.remove(pid)
533 def _IncomingConnection(self):
534 connection, client_addr = self.socket.accept()
535 logging.info("Connection from %s:%s", client_addr[0], client_addr[1])
537 handler = _HttpConnectionHandler(self, connection, client_addr, self._fileio_class)
538 except (socket.error, SocketClosed):
545 handler.HandleRequest()
547 # Try to send a response
548 handler.SendResponse()
553 logging.info("Disconnected %s:%s", client_addr[0], client_addr[1])
555 # Check whether we should fork or not
556 if not handler.should_fork:
560 self._CollectChildren(False)
568 logging.exception("Error while handling request from %s:%s",
569 client_addr[0], client_addr[1])
573 self._children.append(pid)
575 def HandleRequest(self, req):
576 raise NotImplementedError()
578 def ForkForRequest(self, req):
582 class _SSLFileObject(object):
583 """Wrapper around socket._fileobject
585 This wrapper is required to handle OpenSSL exceptions.
588 def _RequireOpenSocket(fn):
589 def wrapper(self, *args, **kwargs):
591 raise SocketClosed("Socket is closed")
592 return fn(self, *args, **kwargs)
595 def __init__(self, sock, mode='rb', bufsize=-1):
596 self._base = socket._fileobject(sock, mode=mode, bufsize=bufsize)
598 def _ConnectionLost(self):
601 def _getclosed(self):
602 return self._base is None or self._base.closed
603 closed = property(_getclosed, doc="True if the file is closed")
607 return self._base.close()
611 return self._base.flush()
615 return self._base.fileno()
618 def read(self, size=-1):
619 return self._ReadWrapper(self._base.read, size=size)
622 def readline(self, size=-1):
623 return self._ReadWrapper(self._base.readline, size=size)
625 def _ReadWrapper(self, fn, *args, **kwargs):
628 return fn(*args, **kwargs)
630 except OpenSSL.SSL.ZeroReturnError, err:
631 self._ConnectionLost()
634 except OpenSSL.SSL.WantReadError:
637 #except OpenSSL.SSL.WantWriteError:
640 except OpenSSL.SSL.SysCallError, (retval, desc):
641 if ((retval == -1 and desc == "Unexpected EOF")
643 self._ConnectionLost()
646 logging.exception("Error in OpenSSL")
647 self._ConnectionLost()
648 raise socket.error(err.args)
650 except OpenSSL.SSL.Error, err:
651 self._ConnectionLost()
652 raise socket.error(err.args)
655 def write(self, data):
656 return self._WriteWrapper(self._base.write, data)
658 def _WriteWrapper(self, fn, *args, **kwargs):
661 return fn(*args, **kwargs)
662 except OpenSSL.SSL.ZeroReturnError, err:
663 self._ConnectionLost()
666 except OpenSSL.SSL.WantWriteError:
669 #except OpenSSL.SSL.WantReadError:
672 except OpenSSL.SSL.SysCallError, err:
673 if err.args[0] == -1 and data == "":
674 # errors when writing empty strings are expected
678 self._ConnectionLost()
679 raise socket.error(err.args)
681 except OpenSSL.SSL.Error, err:
682 self._ConnectionLost()
683 raise socket.error(err.args)