Package ganeti :: Package http :: Module server
[hide private]
[frames] | no frames]

Source Code for Module ganeti.http.server

  1  # 
  2  # 
  3   
  4  # Copyright (C) 2007, 2008, 2010 Google Inc. 
  5  # 
  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. 
 10  # 
 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. 
 15  # 
 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 
 19  # 02110-1301, USA. 
 20   
 21  """HTTP server module. 
 22   
 23  """ 
 24   
 25  import BaseHTTPServer 
 26  import cgi 
 27  import logging 
 28  import os 
 29  import socket 
 30  import time 
 31  import signal 
 32  import asyncore 
 33   
 34  from ganeti import http 
 35  from ganeti import utils 
 36  from ganeti import netutils 
 37   
 38   
 39  WEEKDAYNAME = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] 
 40  MONTHNAME = [None, 
 41               'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 
 42               'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] 
 43   
 44  # Default error message 
 45  DEFAULT_ERROR_CONTENT_TYPE = "text/html" 
 46  DEFAULT_ERROR_MESSAGE = """\ 
 47  <html> 
 48  <head> 
 49  <title>Error response</title> 
 50  </head> 
 51  <body> 
 52  <h1>Error response</h1> 
 53  <p>Error code %(code)d. 
 54  <p>Message: %(message)s. 
 55  <p>Error code explanation: %(code)s = %(explain)s. 
 56  </body> 
 57  </html> 
 58  """ 
 59   
 60   
61 -def _DateTimeHeader(gmnow=None):
62 """Return the current date and time formatted for a message header. 63 64 The time MUST be in the GMT timezone. 65 66 """ 67 if gmnow is None: 68 gmnow = time.gmtime() 69 (year, month, day, hh, mm, ss, wd, _, _) = gmnow 70 return ("%s, %02d %3s %4d %02d:%02d:%02d GMT" % 71 (WEEKDAYNAME[wd], day, MONTHNAME[month], year, hh, mm, ss))
72 73
74 -class _HttpServerRequest(object):
75 """Data structure for HTTP request on server side. 76 77 """
78 - def __init__(self, method, path, headers, body):
79 # Request attributes 80 self.request_method = method 81 self.request_path = path 82 self.request_headers = headers 83 self.request_body = body 84 85 # Response attributes 86 self.resp_headers = {} 87 88 # Private data for request handler (useful in combination with 89 # authentication) 90 self.private = None
91
92 - def __repr__(self):
93 status = ["%s.%s" % (self.__class__.__module__, self.__class__.__name__), 94 self.request_method, self.request_path, 95 "headers=%r" % str(self.request_headers), 96 "body=%r" % (self.request_body, )] 97 98 return "<%s at %#x>" % (" ".join(status), id(self))
99 100
101 -class _HttpServerToClientMessageWriter(http.HttpMessageWriter):
102 """Writes an HTTP response to client. 103 104 """
105 - def __init__(self, sock, request_msg, response_msg, write_timeout):
106 """Writes the response to the client. 107 108 @type sock: socket 109 @param sock: Target socket 110 @type request_msg: http.HttpMessage 111 @param request_msg: Request message, required to determine whether 112 response may have a message body 113 @type response_msg: http.HttpMessage 114 @param response_msg: Response message 115 @type write_timeout: float 116 @param write_timeout: Write timeout for socket 117 118 """ 119 self._request_msg = request_msg 120 self._response_msg = response_msg 121 http.HttpMessageWriter.__init__(self, sock, response_msg, write_timeout)
122
123 - def HasMessageBody(self):
124 """Logic to detect whether response should contain a message body. 125 126 """ 127 if self._request_msg.start_line: 128 request_method = self._request_msg.start_line.method 129 else: 130 request_method = None 131 132 response_code = self._response_msg.start_line.code 133 134 # RFC2616, section 4.3: "A message-body MUST NOT be included in a request 135 # if the specification of the request method (section 5.1.1) does not allow 136 # sending an entity-body in requests" 137 # 138 # RFC2616, section 9.4: "The HEAD method is identical to GET except that 139 # the server MUST NOT return a message-body in the response." 140 # 141 # RFC2616, section 10.2.5: "The 204 response MUST NOT include a 142 # message-body [...]" 143 # 144 # RFC2616, section 10.3.5: "The 304 response MUST NOT contain a 145 # message-body, [...]" 146 147 return (http.HttpMessageWriter.HasMessageBody(self) and 148 (request_method is not None and 149 request_method != http.HTTP_HEAD) and 150 response_code >= http.HTTP_OK and 151 response_code not in (http.HTTP_NO_CONTENT, 152 http.HTTP_NOT_MODIFIED))
153 154
155 -class _HttpClientToServerMessageReader(http.HttpMessageReader):
156 """Reads an HTTP request sent by client. 157 158 """ 159 # Length limits 160 START_LINE_LENGTH_MAX = 4096 161 HEADER_LENGTH_MAX = 4096 162
163 - def ParseStartLine(self, start_line):
164 """Parses the start line sent by client. 165 166 Example: "GET /index.html HTTP/1.1" 167 168 @type start_line: string 169 @param start_line: Start line 170 171 """ 172 # Empty lines are skipped when reading 173 assert start_line 174 175 logging.debug("HTTP request: %s", start_line) 176 177 words = start_line.split() 178 179 if len(words) == 3: 180 [method, path, version] = words 181 if version[:5] != 'HTTP/': 182 raise http.HttpBadRequest("Bad request version (%r)" % version) 183 184 try: 185 base_version_number = version.split("/", 1)[1] 186 version_number = base_version_number.split(".") 187 188 # RFC 2145 section 3.1 says there can be only one "." and 189 # - major and minor numbers MUST be treated as 190 # separate integers; 191 # - HTTP/2.4 is a lower version than HTTP/2.13, which in 192 # turn is lower than HTTP/12.3; 193 # - Leading zeros MUST be ignored by recipients. 194 if len(version_number) != 2: 195 raise http.HttpBadRequest("Bad request version (%r)" % version) 196 197 version_number = (int(version_number[0]), int(version_number[1])) 198 except (ValueError, IndexError): 199 raise http.HttpBadRequest("Bad request version (%r)" % version) 200 201 if version_number >= (2, 0): 202 raise http.HttpVersionNotSupported("Invalid HTTP Version (%s)" % 203 base_version_number) 204 205 elif len(words) == 2: 206 version = http.HTTP_0_9 207 [method, path] = words 208 if method != http.HTTP_GET: 209 raise http.HttpBadRequest("Bad HTTP/0.9 request type (%r)" % method) 210 211 else: 212 raise http.HttpBadRequest("Bad request syntax (%r)" % start_line) 213 214 return http.HttpClientToServerStartLine(method, path, version)
215 216
217 -class HttpServerRequestExecutor(object):
218 """Implements server side of HTTP. 219 220 This class implements the server side of HTTP. It's based on code of 221 Python's BaseHTTPServer, from both version 2.4 and 3k. It does not 222 support non-ASCII character encodings. Keep-alive connections are 223 not supported. 224 225 """ 226 # The default request version. This only affects responses up until 227 # the point where the request line is parsed, so it mainly decides what 228 # the client gets back when sending a malformed request line. 229 # Most web servers default to HTTP 0.9, i.e. don't send a status line. 230 default_request_version = http.HTTP_0_9 231 232 # Error message settings 233 error_message_format = DEFAULT_ERROR_MESSAGE 234 error_content_type = DEFAULT_ERROR_CONTENT_TYPE 235 236 responses = BaseHTTPServer.BaseHTTPRequestHandler.responses 237 238 # Timeouts in seconds for socket layer 239 WRITE_TIMEOUT = 10 240 READ_TIMEOUT = 10 241 CLOSE_TIMEOUT = 1 242
243 - def __init__(self, server, sock, client_addr):
244 """Initializes this class. 245 246 """ 247 self.server = server 248 self.sock = sock 249 self.client_addr = client_addr 250 251 self.request_msg = http.HttpMessage() 252 self.response_msg = http.HttpMessage() 253 254 self.response_msg.start_line = \ 255 http.HttpServerToClientStartLine(version=self.default_request_version, 256 code=None, reason=None) 257 258 # Disable Python's timeout 259 self.sock.settimeout(None) 260 261 # Operate in non-blocking mode 262 self.sock.setblocking(0) 263 264 logging.debug("Connection from %s:%s", client_addr[0], client_addr[1]) 265 try: 266 request_msg_reader = None 267 force_close = True 268 try: 269 # Do the secret SSL handshake 270 if self.server.using_ssl: 271 self.sock.set_accept_state() 272 try: 273 http.Handshake(self.sock, self.WRITE_TIMEOUT) 274 except http.HttpSessionHandshakeUnexpectedEOF: 275 # Ignore rest 276 return 277 278 try: 279 try: 280 request_msg_reader = self._ReadRequest() 281 282 # RFC2616, 14.23: All Internet-based HTTP/1.1 servers MUST respond 283 # with a 400 (Bad Request) status code to any HTTP/1.1 request 284 # message which lacks a Host header field. 285 if (self.request_msg.start_line.version == http.HTTP_1_1 and 286 http.HTTP_HOST not in self.request_msg.headers): 287 raise http.HttpBadRequest(message="Missing Host header") 288 289 self._HandleRequest() 290 291 # Only wait for client to close if we didn't have any exception. 292 force_close = False 293 except http.HttpException, err: 294 self._SetErrorStatus(err) 295 finally: 296 # Try to send a response 297 self._SendResponse() 298 finally: 299 http.ShutdownConnection(sock, self.CLOSE_TIMEOUT, self.WRITE_TIMEOUT, 300 request_msg_reader, force_close) 301 302 self.sock.close() 303 self.sock = None 304 finally: 305 logging.debug("Disconnected %s:%s", client_addr[0], client_addr[1])
306
307 - def _ReadRequest(self):
308 """Reads a request sent by client. 309 310 """ 311 try: 312 request_msg_reader = \ 313 _HttpClientToServerMessageReader(self.sock, self.request_msg, 314 self.READ_TIMEOUT) 315 except http.HttpSocketTimeout: 316 raise http.HttpError("Timeout while reading request") 317 except socket.error, err: 318 raise http.HttpError("Error reading request: %s" % err) 319 320 self.response_msg.start_line.version = self.request_msg.start_line.version 321 322 return request_msg_reader
323
324 - def _HandleRequest(self):
325 """Calls the handler function for the current request. 326 327 """ 328 handler_context = _HttpServerRequest(self.request_msg.start_line.method, 329 self.request_msg.start_line.path, 330 self.request_msg.headers, 331 self.request_msg.body) 332 333 logging.debug("Handling request %r", handler_context) 334 335 try: 336 try: 337 # Authentication, etc. 338 self.server.PreHandleRequest(handler_context) 339 340 # Call actual request handler 341 result = self.server.HandleRequest(handler_context) 342 except (http.HttpException, KeyboardInterrupt, SystemExit): 343 raise 344 except Exception, err: 345 logging.exception("Caught exception") 346 raise http.HttpInternalServerError(message=str(err)) 347 except: 348 logging.exception("Unknown exception") 349 raise http.HttpInternalServerError(message="Unknown error") 350 351 if not isinstance(result, basestring): 352 raise http.HttpError("Handler function didn't return string type") 353 354 self.response_msg.start_line.code = http.HTTP_OK 355 self.response_msg.headers = handler_context.resp_headers 356 self.response_msg.body = result 357 finally: 358 # No reason to keep this any longer, even for exceptions 359 handler_context.private = None
360
361 - def _SendResponse(self):
362 """Sends the response to the client. 363 364 """ 365 if self.response_msg.start_line.code is None: 366 return 367 368 if not self.response_msg.headers: 369 self.response_msg.headers = {} 370 371 self.response_msg.headers.update({ 372 # TODO: Keep-alive is not supported 373 http.HTTP_CONNECTION: "close", 374 http.HTTP_DATE: _DateTimeHeader(), 375 http.HTTP_SERVER: http.HTTP_GANETI_VERSION, 376 }) 377 378 # Get response reason based on code 379 response_code = self.response_msg.start_line.code 380 if response_code in self.responses: 381 response_reason = self.responses[response_code][0] 382 else: 383 response_reason = "" 384 self.response_msg.start_line.reason = response_reason 385 386 logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1], 387 self.request_msg.start_line, response_code) 388 389 try: 390 _HttpServerToClientMessageWriter(self.sock, self.request_msg, 391 self.response_msg, self.WRITE_TIMEOUT) 392 except http.HttpSocketTimeout: 393 raise http.HttpError("Timeout while sending response") 394 except socket.error, err: 395 raise http.HttpError("Error sending response: %s" % err)
396
397 - def _SetErrorStatus(self, err):
398 """Sets the response code and body from a HttpException. 399 400 @type err: HttpException 401 @param err: Exception instance 402 403 """ 404 try: 405 (shortmsg, longmsg) = self.responses[err.code] 406 except KeyError: 407 shortmsg = longmsg = "Unknown" 408 409 if err.message: 410 message = err.message 411 else: 412 message = shortmsg 413 414 values = { 415 "code": err.code, 416 "message": cgi.escape(message), 417 "explain": longmsg, 418 } 419 420 self.response_msg.start_line.code = err.code 421 422 headers = {} 423 if err.headers: 424 headers.update(err.headers) 425 headers[http.HTTP_CONTENT_TYPE] = self.error_content_type 426 self.response_msg.headers = headers 427 428 self.response_msg.body = self._FormatErrorMessage(values)
429
430 - def _FormatErrorMessage(self, values):
431 """Formats the body of an error message. 432 433 @type values: dict 434 @param values: dictionary with keys code, message and explain. 435 @rtype: string 436 @return: the body of the message 437 438 """ 439 return self.error_message_format % values
440 441
442 -class HttpServer(http.HttpBase, asyncore.dispatcher):
443 """Generic HTTP server class 444 445 Users of this class must subclass it and override the HandleRequest function. 446 447 """ 448 MAX_CHILDREN = 20 449
450 - def __init__(self, mainloop, local_address, port, 451 ssl_params=None, ssl_verify_peer=False, 452 request_executor_class=None):
453 """Initializes the HTTP server 454 455 @type mainloop: ganeti.daemon.Mainloop 456 @param mainloop: Mainloop used to poll for I/O events 457 @type local_address: string 458 @param local_address: Local IP address to bind to 459 @type port: int 460 @param port: TCP port to listen on 461 @type ssl_params: HttpSslParams 462 @param ssl_params: SSL key and certificate 463 @type ssl_verify_peer: bool 464 @param ssl_verify_peer: Whether to require client certificate 465 and compare it with our certificate 466 @type request_executor_class: class 467 @param request_executor_class: an class derived from the 468 HttpServerRequestExecutor class 469 470 """ 471 http.HttpBase.__init__(self) 472 asyncore.dispatcher.__init__(self) 473 474 if request_executor_class is None: 475 self.request_executor = HttpServerRequestExecutor 476 else: 477 self.request_executor = request_executor_class 478 479 self.mainloop = mainloop 480 self.local_address = local_address 481 self.port = port 482 family = netutils.IPAddress.GetAddressFamily(local_address) 483 self.socket = self._CreateSocket(ssl_params, ssl_verify_peer, family) 484 485 # Allow port to be reused 486 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 487 488 self._children = [] 489 self.set_socket(self.socket) 490 self.accepting = True 491 mainloop.RegisterSignal(self)
492
493 - def Start(self):
494 self.socket.bind((self.local_address, self.port)) 495 self.socket.listen(1024)
496
497 - def Stop(self):
498 self.socket.close()
499
500 - def handle_accept(self):
502
503 - def OnSignal(self, signum):
504 if signum == signal.SIGCHLD: 505 self._CollectChildren(True)
506
507 - def _CollectChildren(self, quick):
508 """Checks whether any child processes are done 509 510 @type quick: bool 511 @param quick: Whether to only use non-blocking functions 512 513 """ 514 if not quick: 515 # Don't wait for other processes if it should be a quick check 516 while len(self._children) > self.MAX_CHILDREN: 517 try: 518 # Waiting without a timeout brings us into a potential DoS situation. 519 # As soon as too many children run, we'll not respond to new 520 # requests. The real solution would be to add a timeout for children 521 # and killing them after some time. 522 pid, _ = os.waitpid(0, 0) 523 except os.error: 524 pid = None 525 if pid and pid in self._children: 526 self._children.remove(pid) 527 528 for child in self._children: 529 try: 530 pid, _ = os.waitpid(child, os.WNOHANG) 531 except os.error: 532 pid = None 533 if pid and pid in self._children: 534 self._children.remove(pid)
535
536 - def _IncomingConnection(self):
537 """Called for each incoming connection 538 539 """ 540 # pylint: disable-msg=W0212 541 (connection, client_addr) = self.socket.accept() 542 543 self._CollectChildren(False) 544 545 pid = os.fork() 546 if pid == 0: 547 # Child process 548 try: 549 # The client shouldn't keep the listening socket open. If the parent 550 # process is restarted, it would fail when there's already something 551 # listening (in this case its own child from a previous run) on the 552 # same port. 553 try: 554 self.socket.close() 555 except socket.error: 556 pass 557 self.socket = None 558 559 # In case the handler code uses temporary files 560 utils.ResetTempfileModule() 561 562 self.request_executor(self, connection, client_addr) 563 except Exception: # pylint: disable-msg=W0703 564 logging.exception("Error while handling request from %s:%s", 565 client_addr[0], client_addr[1]) 566 os._exit(1) 567 os._exit(0) 568 else: 569 self._children.append(pid)
570
571 - def PreHandleRequest(self, req):
572 """Called before handling a request. 573 574 Can be overridden by a subclass. 575 576 """
577
578 - def HandleRequest(self, req):
579 """Handles a request. 580 581 Must be overridden by subclass. 582 583 """ 584 raise NotImplementedError()
585