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.
34 from ganeti import constants
35 from ganeti import logger
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
70 class SocketClosed(socket.error):
74 class HTTPException(Exception):
78 def __init__(self, message=None):
79 Exception.__init__(self)
80 if message is not None:
81 self.message = message
84 class HTTPBadRequest(HTTPException):
88 class HTTPForbidden(HTTPException):
92 class HTTPNotFound(HTTPException):
96 class HTTPGone(HTTPException):
100 class HTTPLengthRequired(HTTPException):
104 class HTTPInternalError(HTTPException):
108 class HTTPNotImplemented(HTTPException):
112 class HTTPServiceUnavailable(HTTPException):
116 class HTTPVersionNotSupported(HTTPException):
121 """Utility class to write HTTP server log files.
123 The written format is the "Common Log Format" as defined by Apache:
124 http://httpd.apache.org/docs/2.2/mod/mod_log_config.html#examples
127 def __init__(self, fd):
128 """Constructor for ApacheLogfile class.
131 - fd: Open file object
136 def LogRequest(self, request, format, *args):
137 self._fd.write("%s %s %s [%s] %s\n" % (
138 # Remote host address
139 request.address_string(),
141 # RFC1413 identity (identd)
148 self._FormatCurrentTime(),
155 def _FormatCurrentTime(self):
156 """Formats current time in Common Log Format.
159 return self._FormatLogTime(time.time())
161 def _FormatLogTime(self, seconds):
162 """Formats time for Common Log Format.
164 All timestamps are logged in the UTC timezone.
167 - seconds: Time in seconds since the epoch
170 (_, month, _, _, _, _, _, _, _) = tm = time.gmtime(seconds)
171 format = "%d/" + MONTHNAME[month] + "/%Y:%H:%M:%S +0000"
172 return time.strftime(format, tm)
175 class HTTPServer(BaseHTTPServer.HTTPServer, object):
176 """Class to provide an HTTP/HTTPS server.
179 allow_reuse_address = True
181 def __init__(self, server_address, HandlerClass, httplog=None,
182 enable_ssl=False, ssl_key=None, ssl_cert=None):
183 """Server constructor.
186 server_address: a touple containing:
187 ip: a string with IP address, localhost if empty string
188 port: port number, integer
189 HandlerClass: HTTPRequestHandler object
190 httplog: Access log object
191 enable_ssl: Whether to enable SSL
192 ssl_key: SSL key file
193 ssl_cert: SSL certificate key
196 BaseHTTPServer.HTTPServer.__init__(self, server_address, HandlerClass)
198 self.httplog = httplog
202 context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
203 context.use_privatekey_file(ssl_key)
204 context.use_certificate_file(ssl_cert)
205 self.socket = OpenSSL.SSL.Connection(context,
206 socket.socket(self.address_family,
209 self.socket = socket.socket(self.address_family, self.socket_type)
212 self.server_activate()
215 class HTTPJsonConverter:
216 CONTENT_TYPE = "application/json"
218 def Encode(self, data):
219 return serializer.DumpJson(data)
221 def Decode(self, data):
222 return serializer.LoadJson(data)
225 class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler, object):
226 """Request handler class.
230 """Setup secure read and write file objects.
233 self.connection = self.request
234 self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
235 self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
237 def handle_one_request(self):
238 """Parses a request and calls the handler function.
241 self.raw_requestline = None
243 self.raw_requestline = self.rfile.readline()
244 except OpenSSL.SSL.Error, ex:
245 logger.Error("Error in SSL: %s" % str(ex))
246 if not self.raw_requestline:
247 self.close_connection = 1
249 if not self.parse_request(): # An error code has been sent, just exit
251 logging.debug("HTTP request: %s", self.raw_requestline.rstrip("\r\n"))
256 result = self.HandleRequest()
259 encoder = HTTPJsonConverter()
260 encoded_result = encoder.Encode(result)
262 self.send_response(200)
263 self.send_header("Content-Type", encoder.CONTENT_TYPE)
264 self.send_header("Content-Length", str(len(encoded_result)))
267 self.wfile.write(encoded_result)
269 except HTTPException, err:
270 self.send_error(err.code, message=err.message)
272 except Exception, err:
273 self.send_error(HTTPInternalError.code, message=str(err))
276 self.send_error(HTTPInternalError.code, message="Unknown error")
278 def _ReadPostData(self):
279 if self.command.upper() not in ("POST", "PUT"):
280 self.post_data = None
283 # TODO: Decide what to do when Content-Length header was not sent
285 content_length = int(self.headers.get('Content-Length', 0))
287 raise HTTPBadRequest("No Content-Length header or invalid format")
290 data = self.rfile.read(content_length)
291 except socket.error, err:
292 logger.Error("Socket error while reading: %s" % str(err))
295 # TODO: Content-type, error handling
296 self.post_data = HTTPJsonConverter().Decode(data)
298 logging.debug("HTTP POST data: %s", self.post_data)
300 def HandleRequest(self):
301 """Handles a request.
304 raise NotImplementedError()
306 def log_message(self, format, *args):
307 """Log an arbitrary message.
309 This is used by all other logging functions.
311 The first argument, FORMAT, is a format string for the
312 message to be logged. If the format string contains
313 any % escapes requiring parameters, they should be
314 specified as subsequent arguments (it's just like
318 logging.debug("Handled request: %s", format % args)
319 if self.server.httplog:
320 self.server.httplog.LogRequest(self, format, *args)
323 class _HttpConnectionHandler(object):
324 """Implements server side of HTTP
326 This class implements the server side of HTTP. It's based on code of Python's
327 BaseHTTPServer, from both version 2.4 and 3k. It does not support non-ASCII
328 character encodings. Keep-alive connections are not supported.
331 # String for "Server" header
332 server_version = "Ganeti %s" % constants.RELEASE_VERSION
334 # The default request version. This only affects responses up until
335 # the point where the request line is parsed, so it mainly decides what
336 # the client gets back when sending a malformed request line.
337 # Most web servers default to HTTP 0.9, i.e. don't send a status line.
338 default_request_version = HTTP_0_9
340 # Error message settings
341 error_message_format = DEFAULT_ERROR_MESSAGE
342 error_content_type = DEFAULT_ERROR_CONTENT_TYPE
344 responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
346 def __init__(self, server, conn, client_addr, fileio_class):
347 """Initializes this class.
349 Part of the initialization is reading the request and eventual POST/PUT
350 data sent by the client.
353 self._server = server
355 # We default rfile to buffered because otherwise it could be
356 # really slow for large data (a getc() call per byte); we make
357 # wfile unbuffered because (a) often after a write() we want to
358 # read and we need to flush the line; (b) big writes to unbuffered
359 # files are typically optimized by stdio even when big reads
361 self.rfile = fileio_class(conn, mode="rb", bufsize=-1)
362 self.wfile = fileio_class(conn, mode="wb", bufsize=0)
364 self.client_addr = client_addr
366 self.request_headers = None
367 self.request_method = None
368 self.request_path = None
369 self.request_requestline = None
370 self.request_version = self.default_request_version
372 self.response_body = None
373 self.response_code = HTTP_OK
374 self.response_content_type = None
376 self.should_fork = False
382 self.should_fork = self._server.ForkForRequest(self)
383 except HTTPException, err:
384 self._SetErrorStatus(err)
387 if not self.wfile.closed:
392 def _DateTimeHeader(self):
393 """Return the current date and time formatted for a message header.
396 (year, month, day, hh, mm, ss, wd, _, _) = time.gmtime()
397 return ("%s, %02d %3s %4d %02d:%02d:%02d GMT" %
398 (WEEKDAYNAME[wd], day, MONTHNAME[month], year, hh, mm, ss))
400 def _SetErrorStatus(self, err):
401 """Sets the response code and body from a HTTPException.
403 @type err: HTTPException
404 @param err: Exception instance
408 (shortmsg, longmsg) = self.responses[err.code]
410 shortmsg = longmsg = "Unknown"
413 message = err.message
419 "message": cgi.escape(message),
423 self.response_code = err.code
424 self.response_content_type = self.error_content_type
425 self.response_body = self.error_message_format % values
427 def HandleRequest(self):
428 """Handle the actual request.
430 Calls the actual handler function and converts exceptions into HTTP errors.
433 # Don't do anything if there's already been a problem
434 if self.response_code != HTTP_OK:
437 assert self.request_method, "Status code %s requires a method" % HTTP_OK
439 # Check whether client is still there
444 result = self._server.HandleRequest(self)
447 encoder = HTTPJsonConverter()
448 body = encoder.Encode(result)
450 self.response_content_type = encoder.CONTENT_TYPE
451 self.response_body = body
452 except (HTTPException, KeyboardInterrupt, SystemExit):
454 except Exception, err:
455 logging.exception("Caught exception")
456 raise HTTPInternalError(message=str(err))
458 logging.exception("Unknown exception")
459 raise HTTPInternalError(message="Unknown error")
461 except HTTPException, err:
462 self._SetErrorStatus(err)
464 def SendResponse(self):
465 """Sends response to the client.
468 # Check whether client is still there
471 logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1],
472 self.request_requestline, self.response_code)
474 if self.response_code in self.responses:
475 response_message = self.responses[self.response_code][0]
477 response_message = ""
479 if self.request_version != HTTP_0_9:
480 self.wfile.write("%s %d %s\r\n" %
481 (self.request_version, self.response_code,
483 self._SendHeader("Server", self.server_version)
484 self._SendHeader("Date", self._DateTimeHeader())
485 self._SendHeader("Content-Type", self.response_content_type)
486 self._SendHeader("Content-Length", str(len(self.response_body)))
487 # We don't support keep-alive at this time
488 self._SendHeader("Connection", "close")
489 self.wfile.write("\r\n")
491 if (self.request_method != HTTP_HEAD and
492 self.response_code >= HTTP_OK and
493 self.response_code not in (HTTP_NO_CONTENT, HTTP_NOT_MODIFIED)):
494 self.wfile.write(self.response_body)
496 def _SendHeader(self, name, value):
497 if self.request_version != HTTP_0_9:
498 self.wfile.write("%s: %s\r\n" % (name, value))
500 def _ReadRequest(self):
501 """Reads and parses request line
504 raw_requestline = self.rfile.readline()
506 requestline = raw_requestline
507 if requestline[-2:] == '\r\n':
508 requestline = requestline[:-2]
509 elif requestline[-1:] == '\n':
510 requestline = requestline[:-1]
513 raise HTTPBadRequest("Empty request line")
515 self.request_requestline = requestline
517 logging.debug("HTTP request: %s", raw_requestline.rstrip("\r\n"))
519 words = requestline.split()
522 [method, path, version] = words
523 if version[:5] != 'HTTP/':
524 raise HTTPBadRequest("Bad request version (%r)" % version)
527 base_version_number = version.split('/', 1)[1]
528 version_number = base_version_number.split(".")
530 # RFC 2145 section 3.1 says there can be only one "." and
531 # - major and minor numbers MUST be treated as
533 # - HTTP/2.4 is a lower version than HTTP/2.13, which in
534 # turn is lower than HTTP/12.3;
535 # - Leading zeros MUST be ignored by recipients.
536 if len(version_number) != 2:
537 raise HTTPBadRequest("Bad request version (%r)" % version)
539 version_number = int(version_number[0]), int(version_number[1])
540 except (ValueError, IndexError):
541 raise HTTPBadRequest("Bad request version (%r)" % version)
543 if version_number >= (2, 0):
544 raise HTTPVersionNotSupported("Invalid HTTP Version (%s)" %
547 elif len(words) == 2:
549 [method, path] = words
550 if method != HTTP_GET:
551 raise HTTPBadRequest("Bad HTTP/0.9 request type (%r)" % method)
554 raise HTTPBadRequest("Bad request syntax (%r)" % requestline)
556 # Examine the headers and look for a Connection directive
557 headers = mimetools.Message(self.rfile, 0)
559 self.request_method = method
560 self.request_path = path
561 self.request_version = version
562 self.request_headers = headers
564 def _ReadPostData(self):
565 """Reads POST/PUT data
568 if not self.request_method or self.request_method.upper() not in ("POST", "PUT"):
569 self.request_post_data = None
572 # TODO: Decide what to do when Content-Length header was not sent
574 content_length = int(self.request_headers.get('Content-Length', 0))
576 raise HTTPBadRequest("No Content-Length header or invalid format")
578 data = self.rfile.read(content_length)
580 # TODO: Content-type, error handling
581 self.request_post_data = HTTPJsonConverter().Decode(data)
583 logging.debug("HTTP POST data: %s", self.request_post_data)
586 class HttpServer(object):
587 """Generic HTTP server class
589 Users of this class must subclass it and override the HandleRequest function.
590 Optionally, the ForkForRequest function can be overriden.
595 def __init__(self, mainloop, server_address):
596 self.mainloop = mainloop
597 self.server_address = server_address
601 self.ssl_key = self.ssl_cert
603 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
605 if self.ssl_cert and self.ssl_key:
606 ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
607 ctx.set_options(OpenSSL.SSL.OP_NO_SSLv2)
609 ctx.use_certificate_file(self.ssl_cert)
610 ctx.use_privatekey_file(self.ssl_key)
612 self.socket = OpenSSL.SSL.Connection(ctx, sock)
613 self._fileio_class = _SSLFileObject
616 self._fileio_class = socket._fileobject
618 # Allow port to be reused
619 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
623 mainloop.RegisterIO(self, self.socket.fileno(), select.POLLIN)
624 mainloop.RegisterSignal(self)
627 self.socket.bind(self.server_address)
628 self.socket.listen(5)
633 def OnIO(self, fd, condition):
634 if condition & select.POLLIN:
635 self._IncomingConnection()
637 def OnSignal(self, signum):
638 if signum == signal.SIGCHLD:
639 self._CollectChildren(True)
641 def _CollectChildren(self, quick):
642 """Checks whether any child processes are done
645 @param quick: Whether to only use non-blocking functions
649 # Don't wait for other processes if it should be a quick check
650 while len(self._children) > self.MAX_CHILDREN:
652 pid, status = os.waitpid(0, 0)
655 if pid and pid in self._children:
656 self._children.remove(pid)
658 for child in self._children:
660 pid, status = os.waitpid(child, os.WNOHANG)
663 if pid and pid in self._children:
664 self._children.remove(pid)
666 def _IncomingConnection(self):
667 connection, client_addr = self.socket.accept()
668 logging.info("Connection from %s:%s", client_addr[0], client_addr[1])
670 handler = _HttpConnectionHandler(self, connection, client_addr, self._fileio_class)
671 except (socket.error, SocketClosed):
678 handler.HandleRequest()
680 # Try to send a response
681 handler.SendResponse()
686 logging.info("Disconnected %s:%s", client_addr[0], client_addr[1])
688 # Check whether we should fork or not
689 if not handler.should_fork:
693 self._CollectChildren(False)
701 logging.exception("Error while handling request from %s:%s",
702 client_addr[0], client_addr[1])
706 self._children.append(pid)
708 def HandleRequest(self, req):
709 raise NotImplementedError()
711 def ForkForRequest(self, req):
715 class _SSLFileObject(object):
716 """Wrapper around socket._fileobject
718 This wrapper is required to handle OpenSSL exceptions.
721 def _RequireOpenSocket(fn):
722 def wrapper(self, *args, **kwargs):
724 raise SocketClosed("Socket is closed")
725 return fn(self, *args, **kwargs)
728 def __init__(self, sock, mode='rb', bufsize=-1):
729 self._base = socket._fileobject(sock, mode=mode, bufsize=bufsize)
731 def _ConnectionLost(self):
734 def _getclosed(self):
735 return self._base is None or self._base.closed
736 closed = property(_getclosed, doc="True if the file is closed")
740 return self._base.close()
744 return self._base.flush()
748 return self._base.fileno()
751 def read(self, size=-1):
752 return self._ReadWrapper(self._base.read, size=size)
755 def readline(self, size=-1):
756 return self._ReadWrapper(self._base.readline, size=size)
758 def _ReadWrapper(self, fn, *args, **kwargs):
761 return fn(*args, **kwargs)
763 except OpenSSL.SSL.ZeroReturnError, err:
764 self._ConnectionLost()
767 except OpenSSL.SSL.WantReadError:
770 #except OpenSSL.SSL.WantWriteError:
773 except OpenSSL.SSL.SysCallError, (retval, desc):
774 if ((retval == -1 and desc == "Unexpected EOF")
776 self._ConnectionLost()
779 logging.exception("Error in OpenSSL")
780 self._ConnectionLost()
781 raise socket.error(err.args)
783 except OpenSSL.SSL.Error, err:
784 self._ConnectionLost()
785 raise socket.error(err.args)
788 def write(self, data):
789 return self._WriteWrapper(self._base.write, data)
791 def _WriteWrapper(self, fn, *args, **kwargs):
794 return fn(*args, **kwargs)
795 except OpenSSL.SSL.ZeroReturnError, err:
796 self._ConnectionLost()
799 except OpenSSL.SSL.WantWriteError:
802 #except OpenSSL.SSL.WantReadError:
805 except OpenSSL.SSL.SysCallError, err:
806 if err.args[0] == -1 and data == "":
807 # errors when writing empty strings are expected
811 self._ConnectionLost()
812 raise socket.error(err.args)
814 except OpenSSL.SSL.Error, err:
815 self._ConnectionLost()
816 raise socket.error(err.args)