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