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 self._HandleRequest() 281 282 # Only wait for client to close if we didn't have any exception. 283 force_close = False 284 except http.HttpException, err: 285 self._SetErrorStatus(err) 286 finally: 287 # Try to send a response 288 self._SendResponse() 289 finally: 290 http.ShutdownConnection(sock, self.CLOSE_TIMEOUT, self.WRITE_TIMEOUT, 291 request_msg_reader, force_close) 292 293 self.sock.close() 294 self.sock = None 295 finally: 296 logging.debug("Disconnected %s:%s", client_addr[0], client_addr[1])
297
298 - def _ReadRequest(self):
299 """Reads a request sent by client. 300 301 """ 302 try: 303 request_msg_reader = \ 304 _HttpClientToServerMessageReader(self.sock, self.request_msg, 305 self.READ_TIMEOUT) 306 except http.HttpSocketTimeout: 307 raise http.HttpError("Timeout while reading request") 308 except socket.error, err: 309 raise http.HttpError("Error reading request: %s" % err) 310 311 self.response_msg.start_line.version = self.request_msg.start_line.version 312 313 return request_msg_reader
314
315 - def _HandleRequest(self):
316 """Calls the handler function for the current request. 317 318 """ 319 handler_context = _HttpServerRequest(self.request_msg.start_line.method, 320 self.request_msg.start_line.path, 321 self.request_msg.headers, 322 self.request_msg.decoded_body) 323 324 logging.debug("Handling request %r", handler_context) 325 326 try: 327 try: 328 # Authentication, etc. 329 self.server.PreHandleRequest(handler_context) 330 331 # Call actual request handler 332 result = self.server.HandleRequest(handler_context) 333 except (http.HttpException, KeyboardInterrupt, SystemExit): 334 raise 335 except Exception, err: 336 logging.exception("Caught exception") 337 raise http.HttpInternalServerError(message=str(err)) 338 except: 339 logging.exception("Unknown exception") 340 raise http.HttpInternalServerError(message="Unknown error") 341 342 # TODO: Content-type 343 encoder = http.HttpJsonConverter() 344 self.response_msg.start_line.code = http.HTTP_OK 345 self.response_msg.body = encoder.Encode(result) 346 self.response_msg.headers = handler_context.resp_headers 347 self.response_msg.headers[http.HTTP_CONTENT_TYPE] = encoder.CONTENT_TYPE 348 finally: 349 # No reason to keep this any longer, even for exceptions 350 handler_context.private = None
351
352 - def _SendResponse(self):
353 """Sends the response to the client. 354 355 """ 356 if self.response_msg.start_line.code is None: 357 return 358 359 if not self.response_msg.headers: 360 self.response_msg.headers = {} 361 362 self.response_msg.headers.update({ 363 # TODO: Keep-alive is not supported 364 http.HTTP_CONNECTION: "close", 365 http.HTTP_DATE: _DateTimeHeader(), 366 http.HTTP_SERVER: http.HTTP_GANETI_VERSION, 367 }) 368 369 # Get response reason based on code 370 response_code = self.response_msg.start_line.code 371 if response_code in self.responses: 372 response_reason = self.responses[response_code][0] 373 else: 374 response_reason = "" 375 self.response_msg.start_line.reason = response_reason 376 377 logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1], 378 self.request_msg.start_line, response_code) 379 380 try: 381 _HttpServerToClientMessageWriter(self.sock, self.request_msg, 382 self.response_msg, self.WRITE_TIMEOUT) 383 except http.HttpSocketTimeout: 384 raise http.HttpError("Timeout while sending response") 385 except socket.error, err: 386 raise http.HttpError("Error sending response: %s" % err)
387
388 - def _SetErrorStatus(self, err):
389 """Sets the response code and body from a HttpException. 390 391 @type err: HttpException 392 @param err: Exception instance 393 394 """ 395 try: 396 (shortmsg, longmsg) = self.responses[err.code] 397 except KeyError: 398 shortmsg = longmsg = "Unknown" 399 400 if err.message: 401 message = err.message 402 else: 403 message = shortmsg 404 405 values = { 406 "code": err.code, 407 "message": cgi.escape(message), 408 "explain": longmsg, 409 } 410 411 self.response_msg.start_line.code = err.code 412 413 headers = {} 414 if err.headers: 415 headers.update(err.headers) 416 headers[http.HTTP_CONTENT_TYPE] = self.error_content_type 417 self.response_msg.headers = headers 418 419 self.response_msg.body = self._FormatErrorMessage(values)
420
421 - def _FormatErrorMessage(self, values):
422 """Formats the body of an error message. 423 424 @type values: dict 425 @param values: dictionary with keys code, message and explain. 426 @rtype: string 427 @return: the body of the message 428 429 """ 430 return self.error_message_format % values
431
432 -class HttpServer(http.HttpBase, asyncore.dispatcher):
433 """Generic HTTP server class 434 435 Users of this class must subclass it and override the HandleRequest function. 436 437 """ 438 MAX_CHILDREN = 20 439
440 - def __init__(self, mainloop, local_address, port, 441 ssl_params=None, ssl_verify_peer=False, 442 request_executor_class=None):
443 """Initializes the HTTP server 444 445 @type mainloop: ganeti.daemon.Mainloop 446 @param mainloop: Mainloop used to poll for I/O events 447 @type local_address: string 448 @param local_address: Local IP address to bind to 449 @type port: int 450 @param port: TCP port to listen on 451 @type ssl_params: HttpSslParams 452 @param ssl_params: SSL key and certificate 453 @type ssl_verify_peer: bool 454 @param ssl_verify_peer: Whether to require client certificate 455 and compare it with our certificate 456 @type request_executor_class: class 457 @param request_executor_class: an class derived from the 458 HttpServerRequestExecutor class 459 460 """ 461 http.HttpBase.__init__(self) 462 asyncore.dispatcher.__init__(self) 463 464 if request_executor_class is None: 465 self.request_executor = HttpServerRequestExecutor 466 else: 467 self.request_executor = request_executor_class 468 469 self.mainloop = mainloop 470 self.local_address = local_address 471 self.port = port 472 473 self.socket = self._CreateSocket(ssl_params, ssl_verify_peer) 474 475 # Allow port to be reused 476 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 477 478 self._children = [] 479 self.set_socket(self.socket) 480 self.accepting = True 481 mainloop.RegisterSignal(self)
482
483 - def Start(self):
484 self.socket.bind((self.local_address, self.port)) 485 self.socket.listen(1024)
486
487 - def Stop(self):
488 self.socket.close()
489
490 - def handle_accept(self):
492
493 - def OnSignal(self, signum):
494 if signum == signal.SIGCHLD: 495 self._CollectChildren(True)
496
497 - def _CollectChildren(self, quick):
498 """Checks whether any child processes are done 499 500 @type quick: bool 501 @param quick: Whether to only use non-blocking functions 502 503 """ 504 if not quick: 505 # Don't wait for other processes if it should be a quick check 506 while len(self._children) > self.MAX_CHILDREN: 507 try: 508 # Waiting without a timeout brings us into a potential DoS situation. 509 # As soon as too many children run, we'll not respond to new 510 # requests. The real solution would be to add a timeout for children 511 # and killing them after some time. 512 pid, _ = os.waitpid(0, 0) 513 except os.error: 514 pid = None 515 if pid and pid in self._children: 516 self._children.remove(pid) 517 518 for child in self._children: 519 try: 520 pid, _ = os.waitpid(child, os.WNOHANG) 521 except os.error: 522 pid = None 523 if pid and pid in self._children: 524 self._children.remove(pid)
525
526 - def _IncomingConnection(self):
527 """Called for each incoming connection 528 529 """ 530 # pylint: disable-msg=W0212 531 (connection, client_addr) = self.socket.accept() 532 533 self._CollectChildren(False) 534 535 pid = os.fork() 536 if pid == 0: 537 # Child process 538 try: 539 # The client shouldn't keep the listening socket open. If the parent 540 # process is restarted, it would fail when there's already something 541 # listening (in this case its own child from a previous run) on the 542 # same port. 543 try: 544 self.socket.close() 545 except socket.error: 546 pass 547 self.socket = None 548 549 # In case the handler code uses temporary files 550 utils.ResetTempfileModule() 551 552 self.request_executor(self, connection, client_addr) 553 except Exception: # pylint: disable-msg=W0703 554 logging.exception("Error while handling request from %s:%s", 555 client_addr[0], client_addr[1]) 556 os._exit(1) 557 os._exit(0) 558 else: 559 self._children.append(pid)
560
561 - def PreHandleRequest(self, req):
562 """Called before handling a request. 563 564 Can be overridden by a subclass. 565 566 """
567
568 - def HandleRequest(self, req):
569 """Handles a request. 570 571 Must be overridden by subclass. 572 573 """ 574 raise NotImplementedError()
575