Revision 42242313 lib/http.py

b/lib/http.py
19 19

  
20 20
"""
21 21

  
22
import socket
23 22
import BaseHTTPServer
23
import cgi
24
import logging
25
import mimetools
24 26
import OpenSSL
27
import os
28
import select
29
import socket
30
import sys
25 31
import time
26
import logging
32
import signal
27 33

  
34
from ganeti import constants
28 35
from ganeti import logger
29 36
from ganeti import serializer
30 37

  
31 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
<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
"""
57

  
58
HTTP_OK = 200
59
HTTP_NO_CONTENT = 204
60
HTTP_NOT_MODIFIED = 304
61

  
62
HTTP_0_9 = "HTTP/0.9"
63
HTTP_1_0 = "HTTP/1.0"
64
HTTP_1_1 = "HTTP/1.1"
65

  
66
HTTP_GET = "GET"
67
HTTP_HEAD = "HEAD"
68

  
69

  
70
class SocketClosed(socket.error):
71
  pass
72

  
73

  
32 74
class HTTPException(Exception):
33 75
  code = None
34 76
  message = None
35 77

  
36 78
  def __init__(self, message=None):
79
    Exception.__init__(self)
37 80
    if message is not None:
38 81
      self.message = message
39 82

  
......
70 113
  code = 503
71 114

  
72 115

  
116
class HTTPVersionNotSupported(HTTPException):
117
  code = 505
118

  
119

  
73 120
class ApacheLogfile:
74 121
  """Utility class to write HTTP server log files.
75 122

  
......
77 124
  http://httpd.apache.org/docs/2.2/mod/mod_log_config.html#examples
78 125

  
79 126
  """
80
  MONTHNAME = [None,
81
               'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
82
               'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
83

  
84 127
  def __init__(self, fd):
85 128
    """Constructor for ApacheLogfile class.
86 129

  
......
125 168

  
126 169
    """
127 170
    (_, month, _, _, _, _, _, _, _) = tm = time.gmtime(seconds)
128
    format = "%d/" + self.MONTHNAME[month] + "/%Y:%H:%M:%S +0000"
171
    format = "%d/" + MONTHNAME[month] + "/%Y:%H:%M:%S +0000"
129 172
    return time.strftime(format, tm)
130 173

  
131 174

  
......
275 318
    logging.debug("Handled request: %s", format % args)
276 319
    if self.server.httplog:
277 320
      self.server.httplog.LogRequest(self, format, *args)
321

  
322

  
323
class _HttpConnectionHandler(object):
324
  """Implements server side of HTTP
325

  
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.
329

  
330
  """
331
  # String for "Server" header
332
  server_version = "Ganeti %s" % constants.RELEASE_VERSION
333

  
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
339

  
340
  # Error message settings
341
  error_message_format = DEFAULT_ERROR_MESSAGE
342
  error_content_type = DEFAULT_ERROR_CONTENT_TYPE
343

  
344
  responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
345

  
346
  def __init__(self, server, conn, client_addr, fileio_class):
347
    """Initializes this class.
348

  
349
    Part of the initialization is reading the request and eventual POST/PUT
350
    data sent by the client.
351

  
352
    """
353
    self._server = server
354

  
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
360
    # aren't.
361
    self.rfile = fileio_class(conn, mode="rb", bufsize=-1)
362
    self.wfile = fileio_class(conn, mode="wb", bufsize=0)
363

  
364
    self.client_addr = client_addr
365

  
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
371

  
372
    self.response_body = None
373
    self.response_code = HTTP_OK
374
    self.response_content_type = None
375

  
376
    self.should_fork = False
377

  
378
    try:
379
      self._ReadRequest()
380
      self._ReadPostData()
381

  
382
      self.should_fork = self._server.ForkForRequest(self)
383
    except HTTPException, err:
384
      self._SetErrorStatus(err)
385

  
386
  def Close(self):
387
    if not self.wfile.closed:
388
      self.wfile.flush()
389
    self.wfile.close()
390
    self.rfile.close()
391

  
392
  def _DateTimeHeader(self):
393
    """Return the current date and time formatted for a message header.
394

  
395
    """
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))
399

  
400
  def _SetErrorStatus(self, err):
401
    """Sets the response code and body from a HTTPException.
402

  
403
    @type err: HTTPException
404
    @param err: Exception instance
405

  
406
    """
407
    try:
408
      (shortmsg, longmsg) = self.responses[err.code]
409
    except KeyError:
410
      shortmsg = longmsg = "Unknown"
411

  
412
    if err.message:
413
      message = err.message
414
    else:
415
      message = shortmsg
416

  
417
    values = {
418
      "code": err.code,
419
      "message": cgi.escape(message),
420
      "explain": longmsg,
421
      }
422

  
423
    self.response_code = err.code
424
    self.response_content_type = self.error_content_type
425
    self.response_body = self.error_message_format % values
426

  
427
  def HandleRequest(self):
428
    """Handle the actual request.
429

  
430
    Calls the actual handler function and converts exceptions into HTTP errors.
431

  
432
    """
433
    # Don't do anything if there's already been a problem
434
    if self.response_code != HTTP_OK:
435
      return
436

  
437
    assert self.request_method, "Status code %s requires a method" % HTTP_OK
438

  
439
    # Check whether client is still there
440
    self.rfile.read(0)
441

  
442
    try:
443
      try:
444
        result = self._server.HandleRequest(self)
445

  
446
        # TODO: Content-type
447
        encoder = HTTPJsonConverter()
448
        body = encoder.Encode(result)
449

  
450
        self.response_content_type = encoder.CONTENT_TYPE
451
        self.response_body = body
452
      except (HTTPException, KeyboardInterrupt, SystemExit):
453
        raise
454
      except Exception, err:
455
        logging.exception("Caught exception")
456
        raise HTTPInternalError(message=str(err))
457
      except:
458
        logging.exception("Unknown exception")
459
        raise HTTPInternalError(message="Unknown error")
460

  
461
    except HTTPException, err:
462
      self._SetErrorStatus(err)
463

  
464
  def SendResponse(self):
465
    """Sends response to the client.
466

  
467
    """
468
    # Check whether client is still there
469
    self.rfile.read(0)
470

  
471
    logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1],
472
                 self.request_requestline, self.response_code)
473

  
474
    if self.response_code in self.responses:
475
      response_message = self.responses[self.response_code][0]
476
    else:
477
      response_message = ""
478

  
479
    if self.request_version != HTTP_0_9:
480
      self.wfile.write("%s %d %s\r\n" %
481
                       (self.request_version, self.response_code,
482
                        response_message))
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")
490

  
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)
495

  
496
  def _SendHeader(self, name, value):
497
    if self.request_version != HTTP_0_9:
498
      self.wfile.write("%s: %s\r\n" % (name, value))
499

  
500
  def _ReadRequest(self):
501
    """Reads and parses request line
502

  
503
    """
504
    raw_requestline = self.rfile.readline()
505

  
506
    requestline = raw_requestline
507
    if requestline[-2:] == '\r\n':
508
      requestline = requestline[:-2]
509
    elif requestline[-1:] == '\n':
510
      requestline = requestline[:-1]
511

  
512
    if not requestline:
513
      raise HTTPBadRequest("Empty request line")
514

  
515
    self.request_requestline = requestline
516

  
517
    logging.debug("HTTP request: %s", raw_requestline.rstrip("\r\n"))
518

  
519
    words = requestline.split()
520

  
521
    if len(words) == 3:
522
      [method, path, version] = words
523
      if version[:5] != 'HTTP/':
524
        raise HTTPBadRequest("Bad request version (%r)" % version)
525

  
526
      try:
527
        base_version_number = version.split('/', 1)[1]
528
        version_number = base_version_number.split(".")
529

  
530
        # RFC 2145 section 3.1 says there can be only one "." and
531
        #   - major and minor numbers MUST be treated as
532
        #      separate integers;
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)
538

  
539
        version_number = int(version_number[0]), int(version_number[1])
540
      except (ValueError, IndexError):
541
        raise HTTPBadRequest("Bad request version (%r)" % version)
542

  
543
      if version_number >= (2, 0):
544
        raise HTTPVersionNotSupported("Invalid HTTP Version (%s)" %
545
                                      base_version_number)
546

  
547
    elif len(words) == 2:
548
      version = HTTP_0_9
549
      [method, path] = words
550
      if method != HTTP_GET:
551
        raise HTTPBadRequest("Bad HTTP/0.9 request type (%r)" % method)
552

  
553
    else:
554
      raise HTTPBadRequest("Bad request syntax (%r)" % requestline)
555

  
556
    # Examine the headers and look for a Connection directive
557
    headers = mimetools.Message(self.rfile, 0)
558

  
559
    self.request_method = method
560
    self.request_path = path
561
    self.request_version = version
562
    self.request_headers = headers
563

  
564
  def _ReadPostData(self):
565
    """Reads POST/PUT data
566

  
567
    """
568
    if not self.request_method or self.request_method.upper() not in ("POST", "PUT"):
569
      self.request_post_data = None
570
      return
571

  
572
    # TODO: Decide what to do when Content-Length header was not sent
573
    try:
574
      content_length = int(self.request_headers.get('Content-Length', 0))
575
    except ValueError:
576
      raise HTTPBadRequest("No Content-Length header or invalid format")
577

  
578
    data = self.rfile.read(content_length)
579

  
580
    # TODO: Content-type, error handling
581
    self.request_post_data = HTTPJsonConverter().Decode(data)
582

  
583
    logging.debug("HTTP POST data: %s", self.request_post_data)
584

  
585

  
586
class HttpServer(object):
587
  """Generic HTTP server class
588

  
589
  Users of this class must subclass it and override the HandleRequest function.
590
  Optionally, the ForkForRequest function can be overriden.
591

  
592
  """
593
  MAX_CHILDREN = 20
594

  
595
  def __init__(self, mainloop, server_address):
596
    self.mainloop = mainloop
597
    self.server_address = server_address
598

  
599
    # TODO: SSL support
600
    self.ssl_cert = None
601
    self.ssl_key = self.ssl_cert
602

  
603
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
604

  
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)
608

  
609
      ctx.use_certificate_file(self.ssl_cert)
610
      ctx.use_privatekey_file(self.ssl_key)
611

  
612
      self.socket = OpenSSL.SSL.Connection(ctx, sock)
613
      self._fileio_class = _SSLFileObject
614
    else:
615
      self.socket = sock
616
      self._fileio_class = socket._fileobject
617

  
618
    # Allow port to be reused
619
    self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
620

  
621
    self._children = []
622

  
623
    mainloop.RegisterIO(self, self.socket.fileno(), select.POLLIN)
624
    mainloop.RegisterSignal(self)
625

  
626
  def Start(self):
627
    self.socket.bind(self.server_address)
628
    self.socket.listen(5)
629

  
630
  def Stop(self):
631
    self.socket.close()
632

  
633
  def OnIO(self, fd, condition):
634
    if condition & select.POLLIN:
635
      self._IncomingConnection()
636

  
637
  def OnSignal(self, signum):
638
    if signum == signal.SIGCHLD:
639
      self._CollectChildren(True)
640

  
641
  def _CollectChildren(self, quick):
642
    """Checks whether any child processes are done
643

  
644
    @type quick: bool
645
    @param quick: Whether to only use non-blocking functions
646

  
647
    """
648
    if not quick:
649
      # Don't wait for other processes if it should be a quick check
650
      while len(self._children) > self.MAX_CHILDREN:
651
        try:
652
          pid, status = os.waitpid(0, 0)
653
        except os.error:
654
          pid = None
655
        if pid and pid in self._children:
656
          self._children.remove(pid)
657

  
658
    for child in self._children:
659
      try:
660
        pid, status = os.waitpid(child, os.WNOHANG)
661
      except os.error:
662
        pid = None
663
      if pid and pid in self._children:
664
        self._children.remove(pid)
665

  
666
  def _IncomingConnection(self):
667
    connection, client_addr = self.socket.accept()
668
    logging.info("Connection from %s:%s", client_addr[0], client_addr[1])
669
    try:
670
      handler = _HttpConnectionHandler(self, connection, client_addr, self._fileio_class)
671
    except (socket.error, SocketClosed):
672
      return
673

  
674
    def FinishRequest():
675
      try:
676
        try:
677
          try:
678
            handler.HandleRequest()
679
          finally:
680
            # Try to send a response
681
            handler.SendResponse()
682
            handler.Close()
683
        except SocketClosed:
684
          pass
685
      finally:
686
        logging.info("Disconnected %s:%s", client_addr[0], client_addr[1])
687

  
688
    # Check whether we should fork or not
689
    if not handler.should_fork:
690
      FinishRequest()
691
      return
692

  
693
    self._CollectChildren(False)
694

  
695
    pid = os.fork()
696
    if pid == 0:
697
      # Child process
698
      try:
699
        FinishRequest()
700
      except:
701
        logging.exception("Error while handling request from %s:%s",
702
                          client_addr[0], client_addr[1])
703
        os._exit(1)
704
      os._exit(0)
705
    else:
706
      self._children.append(pid)
707

  
708
  def HandleRequest(self, req):
709
    raise NotImplementedError()
710

  
711
  def ForkForRequest(self, req):
712
    return True
713

  
714

  
715
class _SSLFileObject(object):
716
  """Wrapper around socket._fileobject
717

  
718
  This wrapper is required to handle OpenSSL exceptions.
719

  
720
  """
721
  def _RequireOpenSocket(fn):
722
    def wrapper(self, *args, **kwargs):
723
      if self.closed:
724
        raise SocketClosed("Socket is closed")
725
      return fn(self, *args, **kwargs)
726
    return wrapper
727

  
728
  def __init__(self, sock, mode='rb', bufsize=-1):
729
    self._base = socket._fileobject(sock, mode=mode, bufsize=bufsize)
730

  
731
  def _ConnectionLost(self):
732
    self._base = None
733

  
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")
737

  
738
  @_RequireOpenSocket
739
  def close(self):
740
    return self._base.close()
741

  
742
  @_RequireOpenSocket
743
  def flush(self):
744
    return self._base.flush()
745

  
746
  @_RequireOpenSocket
747
  def fileno(self):
748
    return self._base.fileno()
749

  
750
  @_RequireOpenSocket
751
  def read(self, size=-1):
752
    return self._ReadWrapper(self._base.read, size=size)
753

  
754
  @_RequireOpenSocket
755
  def readline(self, size=-1):
756
    return self._ReadWrapper(self._base.readline, size=size)
757

  
758
  def _ReadWrapper(self, fn, *args, **kwargs):
759
    while True:
760
      try:
761
        return fn(*args, **kwargs)
762

  
763
      except OpenSSL.SSL.ZeroReturnError, err:
764
        self._ConnectionLost()
765
        return ""
766

  
767
      except OpenSSL.SSL.WantReadError:
768
        continue
769

  
770
      #except OpenSSL.SSL.WantWriteError:
771
      # TODO
772

  
773
      except OpenSSL.SSL.SysCallError, (retval, desc):
774
        if ((retval == -1 and desc == "Unexpected EOF")
775
            or retval > 0):
776
          self._ConnectionLost()
777
          return ""
778

  
779
        logging.exception("Error in OpenSSL")
780
        self._ConnectionLost()
781
        raise socket.error(err.args)
782

  
783
      except OpenSSL.SSL.Error, err:
784
        self._ConnectionLost()
785
        raise socket.error(err.args)
786

  
787
  @_RequireOpenSocket
788
  def write(self, data):
789
    return self._WriteWrapper(self._base.write, data)
790

  
791
  def _WriteWrapper(self, fn, *args, **kwargs):
792
    while True:
793
      try:
794
        return fn(*args, **kwargs)
795
      except OpenSSL.SSL.ZeroReturnError, err:
796
        self._ConnectionLost()
797
        return 0
798

  
799
      except OpenSSL.SSL.WantWriteError:
800
        continue
801

  
802
      #except OpenSSL.SSL.WantReadError:
803
      # TODO
804

  
805
      except OpenSSL.SSL.SysCallError, err:
806
        if err.args[0] == -1 and data == "":
807
          # errors when writing empty strings are expected
808
          # and can be ignored
809
          return 0
810

  
811
        self._ConnectionLost()
812
        raise socket.error(err.args)
813

  
814
      except OpenSSL.SSL.Error, err:
815
        self._ConnectionLost()
816
        raise socket.error(err.args)

Also available in: Unified diff