Revision 02cab3e7

b/Makefile.am
101 101
	lib/rapi/rlib2.py
102 102

  
103 103
http_PYTHON = \
104
	lib/http/__init__.py
104
	lib/http/__init__.py \
105
	lib/http/client.py \
106
	lib/http/server.py
105 107

  
106 108

  
107 109
docsgml = \
b/lib/http/__init__.py
18 18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 19
# 02110-1301, USA.
20 20

  
21
"""HTTP server module.
21
"""HTTP module.
22 22

  
23 23
"""
24 24

  
......
30 30
import os
31 31
import select
32 32
import socket
33
import sys
34 33
import time
35 34
import signal
36
import logging
37 35
import errno
38 36
import threading
39 37

  
......
103 101
  pass
104 102

  
105 103

  
104
class HttpError(Exception):
105
  """Internal exception for HTTP errors.
106

  
107
  This should only be used for internal error reporting.
108

  
109
  """
110

  
111

  
106 112
class _HttpClientError(Exception):
107 113
  """Internal exception for HTTP client errors.
108 114

  
......
330 336
      raise
331 337

  
332 338

  
339
def ShutdownConnection(poller, sock, close_timeout, write_timeout, msgreader,
340
                       force):
341
  """Closes the connection.
342

  
343
  """
344
  poller = select.poll()
345

  
346
  #print msgreader.peer_will_close, force
347
  if msgreader and msgreader.peer_will_close and not force:
348
    # Wait for peer to close
349
    try:
350
      # Check whether it's actually closed
351
      if not SocketOperation(poller, sock, SOCKOP_RECV, 1, close_timeout):
352
        return
353
    except (socket.error, HttpError, HttpSocketTimeout):
354
      # Ignore errors at this stage
355
      pass
356

  
357
  # Close the connection from our side
358
  try:
359
    SocketOperation(poller, sock, SOCKOP_SHUTDOWN, socket.SHUT_RDWR,
360
                    write_timeout)
361
  except HttpSocketTimeout:
362
    raise HttpError("Timeout while shutting down connection")
363
  except socket.error, err:
364
    raise HttpError("Error while shutting down connection: %s" % err)
365

  
366

  
333 367
class HttpSslParams(object):
334 368
  """Data class for SSL key and certificate.
335 369

  
......
1475 1509
      except OpenSSL.SSL.Error, err:
1476 1510
        self._ConnectionLost()
1477 1511
        raise socket.error(err.args)
1512

  
1513

  
1514
class HttpMessage(object):
1515
  """Data structure for HTTP message.
1516

  
1517
  """
1518
  def __init__(self):
1519
    self.start_line = None
1520
    self.headers = None
1521
    self.body = None
1522
    self.decoded_body = None
1523

  
1524

  
1525
class HttpClientToServerStartLine(object):
1526
  """Data structure for HTTP request start line.
1527

  
1528
  """
1529
  def __init__(self, method, path, version):
1530
    self.method = method
1531
    self.path = path
1532
    self.version = version
1533

  
1534
  def __str__(self):
1535
    return "%s %s %s" % (self.method, self.path, self.version)
1536

  
1537

  
1538
class HttpServerToClientStartLine(object):
1539
  """Data structure for HTTP response start line.
1540

  
1541
  """
1542
  def __init__(self, version, code, reason):
1543
    self.version = version
1544
    self.code = code
1545
    self.reason = reason
1546

  
1547
  def __str__(self):
1548
    return "%s %s %s" % (self.version, self.code, self.reason)
1549

  
1550

  
1551
class HttpMessageWriter(object):
1552
  """Writes an HTTP message to a socket.
1553

  
1554
  """
1555
  def __init__(self, sock, msg, write_timeout):
1556
    self._msg = msg
1557

  
1558
    self._PrepareMessage()
1559

  
1560
    buf = self._FormatMessage()
1561

  
1562
    poller = select.poll()
1563
    while buf:
1564
      # Send only 4 KB at a time
1565
      data = buf[:4096]
1566

  
1567
      sent = SocketOperation(poller, sock, SOCKOP_SEND, data,
1568
                             write_timeout)
1569

  
1570
      # Remove sent bytes
1571
      buf = buf[sent:]
1572

  
1573
    assert not buf, "Message wasn't sent completely"
1574

  
1575
  def _PrepareMessage(self):
1576
    """Prepares the HTTP message by setting mandatory headers.
1577

  
1578
    """
1579
    # RFC2616, section 4.3: "The presence of a message-body in a request is
1580
    # signaled by the inclusion of a Content-Length or Transfer-Encoding header
1581
    # field in the request's message-headers."
1582
    if self._msg.body:
1583
      self._msg.headers[HTTP_CONTENT_LENGTH] = len(self._msg.body)
1584

  
1585
  def _FormatMessage(self):
1586
    """Serializes the HTTP message into a string.
1587

  
1588
    """
1589
    buf = StringIO()
1590

  
1591
    # Add start line
1592
    buf.write(str(self._msg.start_line))
1593
    buf.write("\r\n")
1594

  
1595
    # Add headers
1596
    if self._msg.start_line.version != HTTP_0_9:
1597
      for name, value in self._msg.headers.iteritems():
1598
        buf.write("%s: %s\r\n" % (name, value))
1599

  
1600
    buf.write("\r\n")
1601

  
1602
    # Add message body if needed
1603
    if self.HasMessageBody():
1604
      buf.write(self._msg.body)
1605

  
1606
    elif self._msg.body:
1607
      logging.warning("Ignoring message body")
1608

  
1609
    return buf.getvalue()
1610

  
1611
  def HasMessageBody(self):
1612
    """Checks whether the HTTP message contains a body.
1613

  
1614
    Can be overriden by subclasses.
1615

  
1616
    """
1617
    return bool(self._msg.body)
1618

  
1619

  
1620
class HttpMessageReader(object):
1621
  """Reads HTTP message from socket.
1622

  
1623
  """
1624
  # Length limits
1625
  START_LINE_LENGTH_MAX = None
1626
  HEADER_LENGTH_MAX = None
1627

  
1628
  # Parser state machine
1629
  PS_START_LINE = "start-line"
1630
  PS_HEADERS = "headers"
1631
  PS_BODY = "entity-body"
1632
  PS_COMPLETE = "complete"
1633

  
1634
  def __init__(self, sock, msg, read_timeout):
1635
    self.sock = sock
1636
    self.msg = msg
1637

  
1638
    self.poller = select.poll()
1639
    self.start_line_buffer = None
1640
    self.header_buffer = StringIO()
1641
    self.body_buffer = StringIO()
1642
    self.parser_status = self.PS_START_LINE
1643
    self.content_length = None
1644
    self.peer_will_close = None
1645

  
1646
    buf = ""
1647
    eof = False
1648
    while self.parser_status != self.PS_COMPLETE:
1649
      data = SocketOperation(self.poller, sock, SOCKOP_RECV, 4096,
1650
                             read_timeout)
1651

  
1652
      if data:
1653
        buf += data
1654
      else:
1655
        eof = True
1656

  
1657
      # Do some parsing and error checking while more data arrives
1658
      buf = self._ContinueParsing(buf, eof)
1659

  
1660
      # Must be done only after the buffer has been evaluated
1661
      # TODO: Connection-length < len(data read) and connection closed
1662
      if (eof and
1663
          self.parser_status in (self.PS_START_LINE,
1664
                                 self.PS_HEADERS)):
1665
        raise HttpError("Connection closed prematurely")
1666

  
1667
    # Parse rest
1668
    buf = self._ContinueParsing(buf, True)
1669

  
1670
    assert self.parser_status == self.PS_COMPLETE
1671
    assert not buf, "Parser didn't read full response"
1672

  
1673
    msg.body = self.body_buffer.getvalue()
1674

  
1675
    # TODO: Content-type, error handling
1676
    if msg.body:
1677
      msg.decoded_body = HttpJsonConverter().Decode(msg.body)
1678
    else:
1679
      msg.decoded_body = None
1680

  
1681
    if msg.decoded_body:
1682
      logging.debug("Message body: %s", msg.decoded_body)
1683

  
1684
  def _ContinueParsing(self, buf, eof):
1685
    """Main function for HTTP message state machine.
1686

  
1687
    @type buf: string
1688
    @param buf: Receive buffer
1689
    @type eof: bool
1690
    @param eof: Whether we've reached EOF on the socket
1691
    @rtype: string
1692
    @return: Updated receive buffer
1693

  
1694
    """
1695
    if self.parser_status == self.PS_START_LINE:
1696
      # Expect start line
1697
      while True:
1698
        idx = buf.find("\r\n")
1699

  
1700
        # RFC2616, section 4.1: "In the interest of robustness, servers SHOULD
1701
        # ignore any empty line(s) received where a Request-Line is expected.
1702
        # In other words, if the server is reading the protocol stream at the
1703
        # beginning of a message and receives a CRLF first, it should ignore
1704
        # the CRLF."
1705
        if idx == 0:
1706
          # TODO: Limit number of CRLFs for safety?
1707
          buf = buf[:2]
1708
          continue
1709

  
1710
        if idx > 0:
1711
          self.start_line_buffer = buf[:idx]
1712

  
1713
          self._CheckStartLineLength(len(self.start_line_buffer))
1714

  
1715
          # Remove status line, including CRLF
1716
          buf = buf[idx + 2:]
1717

  
1718
          self.msg.start_line = self.ParseStartLine(self.start_line_buffer)
1719

  
1720
          self.parser_status = self.PS_HEADERS
1721
        else:
1722
          # Check whether incoming data is getting too large, otherwise we just
1723
          # fill our read buffer.
1724
          self._CheckStartLineLength(len(buf))
1725

  
1726
        break
1727

  
1728
    # TODO: Handle messages without headers
1729
    if self.parser_status == self.PS_HEADERS:
1730
      # Wait for header end
1731
      idx = buf.find("\r\n\r\n")
1732
      if idx >= 0:
1733
        self.header_buffer.write(buf[:idx + 2])
1734

  
1735
        self._CheckHeaderLength(self.header_buffer.tell())
1736

  
1737
        # Remove headers, including CRLF
1738
        buf = buf[idx + 4:]
1739

  
1740
        self._ParseHeaders()
1741

  
1742
        self.parser_status = self.PS_BODY
1743
      else:
1744
        # Check whether incoming data is getting too large, otherwise we just
1745
        # fill our read buffer.
1746
        self._CheckHeaderLength(len(buf))
1747

  
1748
    if self.parser_status == self.PS_BODY:
1749
      # TODO: Implement max size for body_buffer
1750
      self.body_buffer.write(buf)
1751
      buf = ""
1752

  
1753
      # Check whether we've read everything
1754
      #
1755
      # RFC2616, section 4.4: "When a message-body is included with a message,
1756
      # the transfer-length of that body is determined by one of the following
1757
      # [...] 5. By the server closing the connection. (Closing the connection
1758
      # cannot be used to indicate the end of a request body, since that would
1759
      # leave no possibility for the server to send back a response.)"
1760
      if (eof or
1761
          self.content_length is None or
1762
          (self.content_length is not None and
1763
           self.body_buffer.tell() >= self.content_length)):
1764
        self.parser_status = self.PS_COMPLETE
1765

  
1766
    return buf
1767

  
1768
  def _CheckStartLineLength(self, length):
1769
    """Limits the start line buffer size.
1770

  
1771
    @type length: int
1772
    @param length: Buffer size
1773

  
1774
    """
1775
    if (self.START_LINE_LENGTH_MAX is not None and
1776
        length > self.START_LINE_LENGTH_MAX):
1777
      raise HttpError("Start line longer than %d chars" %
1778
                       self.START_LINE_LENGTH_MAX)
1779

  
1780
  def _CheckHeaderLength(self, length):
1781
    """Limits the header buffer size.
1782

  
1783
    @type length: int
1784
    @param length: Buffer size
1785

  
1786
    """
1787
    if (self.HEADER_LENGTH_MAX is not None and
1788
        length > self.HEADER_LENGTH_MAX):
1789
      raise HttpError("Headers longer than %d chars" % self.HEADER_LENGTH_MAX)
1790

  
1791
  def ParseStartLine(self, start_line):
1792
    """Parses the start line of a message.
1793

  
1794
    Must be overriden by subclass.
1795

  
1796
    @type start_line: string
1797
    @param start_line: Start line string
1798

  
1799
    """
1800
    raise NotImplementedError()
1801

  
1802
  def _WillPeerCloseConnection(self):
1803
    """Evaluate whether peer will close the connection.
1804

  
1805
    @rtype: bool
1806
    @return: Whether peer will close the connection
1807

  
1808
    """
1809
    # RFC2616, section 14.10: "HTTP/1.1 defines the "close" connection option
1810
    # for the sender to signal that the connection will be closed after
1811
    # completion of the response. For example,
1812
    #
1813
    #        Connection: close
1814
    #
1815
    # in either the request or the response header fields indicates that the
1816
    # connection SHOULD NOT be considered `persistent' (section 8.1) after the
1817
    # current request/response is complete."
1818

  
1819
    hdr_connection = self.msg.headers.get(HTTP_CONNECTION, None)
1820
    if hdr_connection:
1821
      hdr_connection = hdr_connection.lower()
1822

  
1823
    # An HTTP/1.1 server is assumed to stay open unless explicitly closed.
1824
    if self.msg.start_line.version == HTTP_1_1:
1825
      return (hdr_connection and "close" in hdr_connection)
1826

  
1827
    # Some HTTP/1.0 implementations have support for persistent connections,
1828
    # using rules different than HTTP/1.1.
1829

  
1830
    # For older HTTP, Keep-Alive indicates persistent connection.
1831
    if self.msg.headers.get(HTTP_KEEP_ALIVE):
1832
      return False
1833

  
1834
    # At least Akamai returns a "Connection: Keep-Alive" header, which was
1835
    # supposed to be sent by the client.
1836
    if hdr_connection and "keep-alive" in hdr_connection:
1837
      return False
1838

  
1839
    return True
1840

  
1841
  def _ParseHeaders(self):
1842
    """Parses the headers.
1843

  
1844
    This function also adjusts internal variables based on header values.
1845

  
1846
    RFC2616, section 4.3: "The presence of a message-body in a request is
1847
    signaled by the inclusion of a Content-Length or Transfer-Encoding header
1848
    field in the request's message-headers."
1849

  
1850
    """
1851
    # Parse headers
1852
    self.header_buffer.seek(0, 0)
1853
    self.msg.headers = mimetools.Message(self.header_buffer, 0)
1854

  
1855
    self.peer_will_close = self._WillPeerCloseConnection()
1856

  
1857
    # Do we have a Content-Length header?
1858
    hdr_content_length = self.msg.headers.get(HTTP_CONTENT_LENGTH, None)
1859
    if hdr_content_length:
1860
      try:
1861
        self.content_length = int(hdr_content_length)
1862
      except ValueError:
1863
        self.content_length = None
1864
      if self.content_length is not None and self.content_length < 0:
1865
        self.content_length = None
1866

  
1867
    # if the connection remains open and a content-length was not provided,
1868
    # then assume that the connection WILL close.
1869
    if self.content_length is None:
1870
      self.peer_will_close = True
b/lib/http/client.py
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 client module.
22

  
23
"""
24

  
25
import BaseHTTPServer
26
import cgi
27
import logging
28
import OpenSSL
29
import os
30
import select
31
import socket
32
import sys
33
import time
34
import signal
35
import errno
36
import threading
37

  
38
from ganeti import constants
39
from ganeti import serializer
40
from ganeti import workerpool
41
from ganeti import utils
42
from ganeti import http
43

  
44

  
45
HTTP_CLIENT_THREADS = 10
46

  
47

  
48
class HttpClientRequest(object):
49
  def __init__(self, host, port, method, path, headers=None, post_data=None,
50
               ssl_params=None, ssl_verify_peer=False):
51
    """Describes an HTTP request.
52

  
53
    @type host: string
54
    @param host: Hostname
55
    @type port: int
56
    @param port: Port
57
    @type method: string
58
    @param method: Method name
59
    @type path: string
60
    @param path: Request path
61
    @type headers: dict or None
62
    @param headers: Additional headers to send
63
    @type post_data: string or None
64
    @param post_data: Additional data to send
65
    @type ssl_params: HttpSslParams
66
    @param ssl_params: SSL key and certificate
67
    @type ssl_verify_peer: bool
68
    @param ssl_verify_peer: Whether to compare our certificate with server's
69
                            certificate
70

  
71
    """
72
    if post_data is not None:
73
      assert method.upper() in (http.HTTP_POST, http.HTTP_PUT), \
74
        "Only POST and GET requests support sending data"
75

  
76
    assert path.startswith("/"), "Path must start with slash (/)"
77

  
78
    # Request attributes
79
    self.host = host
80
    self.port = port
81
    self.ssl_params = ssl_params
82
    self.ssl_verify_peer = ssl_verify_peer
83
    self.method = method
84
    self.path = path
85
    self.headers = headers
86
    self.post_data = post_data
87

  
88
    self.success = None
89
    self.error = None
90

  
91
    # Raw response
92
    self.response = None
93

  
94
    # Response attributes
95
    self.resp_version = None
96
    self.resp_status_code = None
97
    self.resp_reason = None
98
    self.resp_headers = None
99
    self.resp_body = None
100

  
101

  
102
class _HttpClientToServerMessageWriter(http.HttpMessageWriter):
103
  pass
104

  
105

  
106
class _HttpServerToClientMessageReader(http.HttpMessageReader):
107
  # Length limits
108
  START_LINE_LENGTH_MAX = 512
109
  HEADER_LENGTH_MAX = 4096
110

  
111
  def ParseStartLine(self, start_line):
112
    """Parses the status line sent by the server.
113

  
114
    """
115
    # Empty lines are skipped when reading
116
    assert start_line
117

  
118
    try:
119
      [version, status, reason] = start_line.split(None, 2)
120
    except ValueError:
121
      try:
122
        [version, status] = start_line.split(None, 1)
123
        reason = ""
124
      except ValueError:
125
        version = http.HTTP_0_9
126

  
127
    if version:
128
      version = version.upper()
129

  
130
    # The status code is a three-digit number
131
    try:
132
      status = int(status)
133
      if status < 100 or status > 999:
134
        status = -1
135
    except ValueError:
136
      status = -1
137

  
138
    if status == -1:
139
      raise http.HttpError("Invalid status code (%r)" % start_line)
140

  
141
    return http.HttpServerToClientStartLine(version, status, reason)
142

  
143

  
144
class HttpClientRequestExecutor(http.HttpSocketBase):
145
  # Default headers
146
  DEFAULT_HEADERS = {
147
    http.HTTP_USER_AGENT: http.HTTP_GANETI_VERSION,
148
    # TODO: For keep-alive, don't send "Connection: close"
149
    http.HTTP_CONNECTION: "close",
150
    }
151

  
152
  # Timeouts in seconds for socket layer
153
  # TODO: Soft timeout instead of only socket timeout?
154
  # TODO: Make read timeout configurable per OpCode?
155
  CONNECT_TIMEOUT = 5
156
  WRITE_TIMEOUT = 10
157
  READ_TIMEOUT = None
158
  CLOSE_TIMEOUT = 1
159

  
160
  def __init__(self, req):
161
    """Initializes the HttpClientRequestExecutor class.
162

  
163
    @type req: HttpClientRequest
164
    @param req: Request object
165

  
166
    """
167
    http.HttpSocketBase.__init__(self)
168
    self.request = req
169

  
170
    self.poller = select.poll()
171

  
172
    try:
173
      # TODO: Implement connection caching/keep-alive
174
      self.sock = self._CreateSocket(req.ssl_params,
175
                                     req.ssl_verify_peer)
176

  
177
      # Disable Python's timeout
178
      self.sock.settimeout(None)
179

  
180
      # Operate in non-blocking mode
181
      self.sock.setblocking(0)
182

  
183
      response_msg_reader = None
184
      response_msg = None
185
      force_close = True
186

  
187
      self._Connect()
188
      try:
189
        self._SendRequest()
190
        (response_msg_reader, response_msg) = self._ReadResponse()
191

  
192
        # Only wait for server to close if we didn't have any exception.
193
        force_close = False
194
      finally:
195
        # TODO: Keep-alive is not supported, always close connection
196
        force_close = True
197
        http.ShutdownConnection(self.poller, self.sock,
198
                                self.CLOSE_TIMEOUT, self.WRITE_TIMEOUT,
199
                                response_msg_reader, force_close)
200

  
201
      self.sock.close()
202
      self.sock = None
203

  
204
      req.response = response_msg
205

  
206
      req.resp_version = req.response.start_line.version
207
      req.resp_status_code = req.response.start_line.code
208
      req.resp_reason = req.response.start_line.reason
209
      req.resp_headers = req.response.headers
210
      req.resp_body = req.response.body
211

  
212
      req.success = True
213
      req.error = None
214

  
215
    except http.HttpError, err:
216
      req.success = False
217
      req.error = str(err)
218

  
219
  def _Connect(self):
220
    """Non-blocking connect to host with timeout.
221

  
222
    """
223
    connected = False
224
    while True:
225
      try:
226
        connect_error = self.sock.connect_ex((self.request.host,
227
                                              self.request.port))
228
      except socket.gaierror, err:
229
        raise http.HttpError("Connection failed: %s" % str(err))
230

  
231
      if connect_error == errno.EINTR:
232
        # Mask signals
233
        pass
234

  
235
      elif connect_error == 0:
236
        # Connection established
237
        connected = True
238
        break
239

  
240
      elif connect_error == errno.EINPROGRESS:
241
        # Connection started
242
        break
243

  
244
      raise http.HttpError("Connection failed (%s: %s)" %
245
                             (connect_error, os.strerror(connect_error)))
246

  
247
    if not connected:
248
      # Wait for connection
249
      event = http.WaitForSocketCondition(self.poller, self.sock,
250
                                          select.POLLOUT, self.CONNECT_TIMEOUT)
251
      if event is None:
252
        raise http.HttpError("Timeout while connecting to server")
253

  
254
      # Get error code
255
      connect_error = self.sock.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
256
      if connect_error != 0:
257
        raise http.HttpError("Connection failed (%s: %s)" %
258
                               (connect_error, os.strerror(connect_error)))
259

  
260
    # Enable TCP keep-alive
261
    self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
262

  
263
    # If needed, Linux specific options are available to change the TCP
264
    # keep-alive settings, see "man 7 tcp" for TCP_KEEPCNT, TCP_KEEPIDLE and
265
    # TCP_KEEPINTVL.
266

  
267
  def _SendRequest(self):
268
    """Sends request to server.
269

  
270
    """
271
    # Headers
272
    send_headers = self.DEFAULT_HEADERS.copy()
273

  
274
    if self.request.headers:
275
      send_headers.update(self.request.headers)
276

  
277
    send_headers[http.HTTP_HOST] = "%s:%s" % (self.request.host, self.request.port)
278

  
279
    # Response message
280
    msg = http.HttpMessage()
281

  
282
    # Combine request line. We only support HTTP/1.0 (no chunked transfers and
283
    # no keep-alive).
284
    # TODO: For keep-alive, change to HTTP/1.1
285
    msg.start_line = \
286
      http.HttpClientToServerStartLine(method=self.request.method.upper(),
287
                                       path=self.request.path, version=http.HTTP_1_0)
288
    msg.headers = send_headers
289
    msg.body = self.request.post_data
290

  
291
    try:
292
      _HttpClientToServerMessageWriter(self.sock, msg, self.WRITE_TIMEOUT)
293
    except http.HttpSocketTimeout:
294
      raise http.HttpError("Timeout while sending request")
295
    except socket.error, err:
296
      raise http.HttpError("Error sending request: %s" % err)
297

  
298
  def _ReadResponse(self):
299
    """Read response from server.
300

  
301
    """
302
    response_msg = http.HttpMessage()
303

  
304
    try:
305
      response_msg_reader = \
306
        _HttpServerToClientMessageReader(self.sock, response_msg,
307
                                         self.READ_TIMEOUT)
308
    except http.HttpSocketTimeout:
309
      raise http.HttpError("Timeout while reading response")
310
    except socket.error, err:
311
      raise http.HttpError("Error reading response: %s" % err)
312

  
313
    return (response_msg_reader, response_msg)
314

  
315

  
316
class _HttpClientPendingRequest(object):
317
  """Data class for pending requests.
318

  
319
  """
320
  def __init__(self, request):
321
    self.request = request
322

  
323
    # Thread synchronization
324
    self.done = threading.Event()
325

  
326

  
327
class HttpClientWorker(workerpool.BaseWorker):
328
  """HTTP client worker class.
329

  
330
  """
331
  def RunTask(self, pend_req):
332
    try:
333
      HttpClientRequestExecutor(pend_req.request)
334
    finally:
335
      pend_req.done.set()
336

  
337

  
338
class HttpClientWorkerPool(workerpool.WorkerPool):
339
  def __init__(self, manager):
340
    workerpool.WorkerPool.__init__(self, HTTP_CLIENT_THREADS,
341
                                   HttpClientWorker)
342
    self.manager = manager
343

  
344

  
345
class HttpClientManager(object):
346
  """Manages HTTP requests.
347

  
348
  """
349
  def __init__(self):
350
    self._wpool = HttpClientWorkerPool(self)
351

  
352
  def __del__(self):
353
    self.Shutdown()
354

  
355
  def ExecRequests(self, requests):
356
    """Execute HTTP requests.
357

  
358
    This function can be called from multiple threads at the same time.
359

  
360
    @type requests: List of HttpClientRequest instances
361
    @param requests: The requests to execute
362
    @rtype: List of HttpClientRequest instances
363
    @returns: The list of requests passed in
364

  
365
    """
366
    # _HttpClientPendingRequest is used for internal thread synchronization
367
    pending = [_HttpClientPendingRequest(req) for req in requests]
368

  
369
    try:
370
      # Add requests to queue
371
      for pend_req in pending:
372
        self._wpool.AddTask(pend_req)
373

  
374
    finally:
375
      # In case of an exception we should still wait for the rest, otherwise
376
      # another thread from the worker pool could modify the request object
377
      # after we returned.
378

  
379
      # And wait for them to finish
380
      for pend_req in pending:
381
        pend_req.done.wait()
382

  
383
    # Return original list
384
    return requests
385

  
386
  def Shutdown(self):
387
    self._wpool.Quiesce()
388
    self._wpool.TerminateWorkers()
b/lib/http/server.py
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 select
30
import socket
31
import time
32
import signal
33

  
34
from ganeti import constants
35
from ganeti import serializer
36
from ganeti import utils
37
from ganeti import http
38

  
39

  
40
WEEKDAYNAME = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
41
MONTHNAME = [None,
42
             'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
43
             'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
44

  
45
# Default error message
46
DEFAULT_ERROR_CONTENT_TYPE = "text/html"
47
DEFAULT_ERROR_MESSAGE = """\
48
<html>
49
<head>
50
<title>Error response</title>
51
</head>
52
<body>
53
<h1>Error response</h1>
54
<p>Error code %(code)d.
55
<p>Message: %(message)s.
56
<p>Error code explanation: %(code)s = %(explain)s.
57
</body>
58
</html>
59
"""
60

  
61

  
62
def _DateTimeHeader():
63
  """Return the current date and time formatted for a message header.
64

  
65
  """
66
  (year, month, day, hh, mm, ss, wd, _, _) = time.gmtime()
67
  return ("%s, %02d %3s %4d %02d:%02d:%02d GMT" %
68
          (WEEKDAYNAME[wd], day, MONTHNAME[month], year, hh, mm, ss))
69

  
70

  
71
class _HttpServerRequest(object):
72
  """Data structure for HTTP request on server side.
73

  
74
  """
75
  def __init__(self, request_msg):
76
    # Request attributes
77
    self.request_method = request_msg.start_line.method
78
    self.request_path = request_msg.start_line.path
79
    self.request_headers = request_msg.headers
80
    self.request_body = request_msg.decoded_body
81

  
82
    # Response attributes
83
    self.resp_headers = {}
84

  
85

  
86
class _HttpServerToClientMessageWriter(http.HttpMessageWriter):
87
  """Writes an HTTP response to client.
88

  
89
  """
90
  def __init__(self, sock, request_msg, response_msg, write_timeout):
91
    """TODO
92

  
93
    """
94
    self._request_msg = request_msg
95
    self._response_msg = response_msg
96
    http.HttpMessageWriter.__init__(self, sock, response_msg, write_timeout)
97

  
98
  def HasMessageBody(self):
99
    """Logic to detect whether response should contain a message body.
100

  
101
    """
102
    if self._request_msg.start_line:
103
      request_method = self._request_msg.start_line.method
104
    else:
105
      request_method = None
106

  
107
    response_code = self._response_msg.start_line.code
108

  
109
    # RFC2616, section 4.3: "A message-body MUST NOT be included in a request
110
    # if the specification of the request method (section 5.1.1) does not allow
111
    # sending an entity-body in requests"
112
    #
113
    # RFC2616, section 9.4: "The HEAD method is identical to GET except that
114
    # the server MUST NOT return a message-body in the response."
115
    #
116
    # RFC2616, section 10.2.5: "The 204 response MUST NOT include a
117
    # message-body [...]"
118
    #
119
    # RFC2616, section 10.3.5: "The 304 response MUST NOT contain a
120
    # message-body, [...]"
121

  
122
    return (http.HttpMessageWriter.HasMessageBody(self) and
123
            (request_method is not None and request_method != http.HTTP_HEAD) and
124
            response_code >= http.HTTP_OK and
125
            response_code not in (http.HTTP_NO_CONTENT, http.HTTP_NOT_MODIFIED))
126

  
127

  
128
class _HttpClientToServerMessageReader(http.HttpMessageReader):
129
  """Reads an HTTP request sent by client.
130

  
131
  """
132
  # Length limits
133
  START_LINE_LENGTH_MAX = 4096
134
  HEADER_LENGTH_MAX = 4096
135

  
136
  def ParseStartLine(self, start_line):
137
    """Parses the start line sent by client.
138

  
139
    Example: "GET /index.html HTTP/1.1"
140

  
141
    @type start_line: string
142
    @param start_line: Start line
143

  
144
    """
145
    # Empty lines are skipped when reading
146
    assert start_line
147

  
148
    logging.debug("HTTP request: %s", start_line)
149

  
150
    words = start_line.split()
151

  
152
    if len(words) == 3:
153
      [method, path, version] = words
154
      if version[:5] != 'HTTP/':
155
        raise http.HttpBadRequest("Bad request version (%r)" % version)
156

  
157
      try:
158
        base_version_number = version.split("/", 1)[1]
159
        version_number = base_version_number.split(".")
160

  
161
        # RFC 2145 section 3.1 says there can be only one "." and
162
        #   - major and minor numbers MUST be treated as
163
        #      separate integers;
164
        #   - HTTP/2.4 is a lower version than HTTP/2.13, which in
165
        #      turn is lower than HTTP/12.3;
166
        #   - Leading zeros MUST be ignored by recipients.
167
        if len(version_number) != 2:
168
          raise http.HttpBadRequest("Bad request version (%r)" % version)
169

  
170
        version_number = (int(version_number[0]), int(version_number[1]))
171
      except (ValueError, IndexError):
172
        raise http.HttpBadRequest("Bad request version (%r)" % version)
173

  
174
      if version_number >= (2, 0):
175
        raise http.HttpVersionNotSupported("Invalid HTTP Version (%s)" %
176
                                      base_version_number)
177

  
178
    elif len(words) == 2:
179
      version = http.HTTP_0_9
180
      [method, path] = words
181
      if method != http.HTTP_GET:
182
        raise http.HttpBadRequest("Bad HTTP/0.9 request type (%r)" % method)
183

  
184
    else:
185
      raise http.HttpBadRequest("Bad request syntax (%r)" % start_line)
186

  
187
    return http.HttpClientToServerStartLine(method, path, version)
188

  
189

  
190
class _HttpServerRequestExecutor(object):
191
  """Implements server side of HTTP.
192

  
193
  This class implements the server side of HTTP. It's based on code of Python's
194
  BaseHTTPServer, from both version 2.4 and 3k. It does not support non-ASCII
195
  character encodings. Keep-alive connections are not supported.
196

  
197
  """
198
  # The default request version.  This only affects responses up until
199
  # the point where the request line is parsed, so it mainly decides what
200
  # the client gets back when sending a malformed request line.
201
  # Most web servers default to HTTP 0.9, i.e. don't send a status line.
202
  default_request_version = http.HTTP_0_9
203

  
204
  # Error message settings
205
  error_message_format = DEFAULT_ERROR_MESSAGE
206
  error_content_type = DEFAULT_ERROR_CONTENT_TYPE
207

  
208
  responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
209

  
210
  # Timeouts in seconds for socket layer
211
  WRITE_TIMEOUT = 10
212
  READ_TIMEOUT = 10
213
  CLOSE_TIMEOUT = 1
214

  
215
  def __init__(self, server, sock, client_addr):
216
    """Initializes this class.
217

  
218
    """
219
    self.server = server
220
    self.sock = sock
221
    self.client_addr = client_addr
222

  
223
    self.poller = select.poll()
224

  
225
    self.request_msg = http.HttpMessage()
226
    self.response_msg = http.HttpMessage()
227

  
228
    self.response_msg.start_line = \
229
      http.HttpServerToClientStartLine(version=self.default_request_version,
230
                                       code=None, reason=None)
231

  
232
    # Disable Python's timeout
233
    self.sock.settimeout(None)
234

  
235
    # Operate in non-blocking mode
236
    self.sock.setblocking(0)
237

  
238
    logging.info("Connection from %s:%s", client_addr[0], client_addr[1])
239
    try:
240
      request_msg_reader = None
241
      force_close = True
242
      try:
243
        try:
244
          try:
245
            request_msg_reader = self._ReadRequest()
246
            self._HandleRequest()
247

  
248
            # Only wait for client to close if we didn't have any exception.
249
            force_close = False
250
          except http.HttpException, err:
251
            self._SetErrorStatus(err)
252
        finally:
253
          # Try to send a response
254
          self._SendResponse()
255
      finally:
256
        http.ShutdownConnection(self.poller, sock,
257
                                self.CLOSE_TIMEOUT, self.WRITE_TIMEOUT,
258
                                request_msg_reader, force_close)
259

  
260
      self.sock.close()
261
      self.sock = None
262
    finally:
263
      logging.info("Disconnected %s:%s", client_addr[0], client_addr[1])
264

  
265
  def _ReadRequest(self):
266
    """Reads a request sent by client.
267

  
268
    """
269
    try:
270
      request_msg_reader = \
271
        _HttpClientToServerMessageReader(self.sock, self.request_msg,
272
                                         self.READ_TIMEOUT)
273
    except http.HttpSocketTimeout:
274
      raise http.HttpError("Timeout while reading request")
275
    except socket.error, err:
276
      raise http.HttpError("Error reading request: %s" % err)
277

  
278
    self.response_msg.start_line.version = self.request_msg.start_line.version
279

  
280
    return request_msg_reader
281

  
282
  def _HandleRequest(self):
283
    """Calls the handler function for the current request.
284

  
285
    """
286
    handler_context = _HttpServerRequest(self.request_msg)
287

  
288
    try:
289
      result = self.server.HandleRequest(handler_context)
290
    except (http.HttpException, KeyboardInterrupt, SystemExit):
291
      raise
292
    except Exception, err:
293
      logging.exception("Caught exception")
294
      raise http.HttpInternalError(message=str(err))
295
    except:
296
      logging.exception("Unknown exception")
297
      raise http.HttpInternalError(message="Unknown error")
298

  
299
    # TODO: Content-type
300
    encoder = http.HttpJsonConverter()
301
    self.response_msg.start_line.code = http.HTTP_OK
302
    self.response_msg.body = encoder.Encode(result)
303
    self.response_msg.headers = handler_context.resp_headers
304
    self.response_msg.headers[http.HTTP_CONTENT_TYPE] = encoder.CONTENT_TYPE
305

  
306
  def _SendResponse(self):
307
    """Sends the response to the client.
308

  
309
    """
310
    if self.response_msg.start_line.code is None:
311
      return
312

  
313
    if not self.response_msg.headers:
314
      self.response_msg.headers = {}
315

  
316
    self.response_msg.headers.update({
317
      # TODO: Keep-alive is not supported
318
      http.HTTP_CONNECTION: "close",
319
      http.HTTP_DATE: _DateTimeHeader(),
320
      http.HTTP_SERVER: http.HTTP_GANETI_VERSION,
321
      })
322

  
323
    # Get response reason based on code
324
    response_code = self.response_msg.start_line.code
325
    if response_code in self.responses:
326
      response_reason = self.responses[response_code][0]
327
    else:
328
      response_reason = ""
329
    self.response_msg.start_line.reason = response_reason
330

  
331
    logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1],
332
                 self.request_msg.start_line, response_code)
333

  
334
    try:
335
      _HttpServerToClientMessageWriter(self.sock, self.request_msg,
336
                                       self.response_msg, self.WRITE_TIMEOUT)
337
    except http.HttpSocketTimeout:
338
      raise http.HttpError("Timeout while sending response")
339
    except socket.error, err:
340
      raise http.HttpError("Error sending response: %s" % err)
341

  
342
  def _SetErrorStatus(self, err):
343
    """Sets the response code and body from a HttpException.
344

  
345
    @type err: HttpException
346
    @param err: Exception instance
347

  
348
    """
349
    try:
350
      (shortmsg, longmsg) = self.responses[err.code]
351
    except KeyError:
352
      shortmsg = longmsg = "Unknown"
353

  
354
    if err.message:
355
      message = err.message
356
    else:
357
      message = shortmsg
358

  
359
    values = {
360
      "code": err.code,
361
      "message": cgi.escape(message),
362
      "explain": longmsg,
363
      }
364

  
365
    self.response_msg.start_line.code = err.code
366
    self.response_msg.headers = {
367
      http.HTTP_CONTENT_TYPE: self.error_content_type,
368
      }
369
    self.response_msg.body = self.error_message_format % values
370

  
371

  
372
class HttpServer(http.HttpSocketBase):
373
  """Generic HTTP server class
374

  
375
  Users of this class must subclass it and override the HandleRequest function.
376

  
377
  """
378
  MAX_CHILDREN = 20
379

  
380
  def __init__(self, mainloop, local_address, port,
381
               ssl_params=None, ssl_verify_peer=False):
382
    """Initializes the HTTP server
383

  
384
    @type mainloop: ganeti.daemon.Mainloop
385
    @param mainloop: Mainloop used to poll for I/O events
386
    @type local_addess: string
387
    @param local_address: Local IP address to bind to
388
    @type port: int
389
    @param port: TCP port to listen on
390
    @type ssl_params: HttpSslParams
391
    @param ssl_params: SSL key and certificate
392
    @type ssl_verify_peer: bool
393
    @param ssl_verify_peer: Whether to require client certificate and compare
394
                            it with our certificate
395

  
396
    """
397
    http.HttpSocketBase.__init__(self)
398

  
399
    self.mainloop = mainloop
400
    self.local_address = local_address
401
    self.port = port
402

  
403
    self.socket = self._CreateSocket(ssl_params, ssl_verify_peer)
404

  
405
    # Allow port to be reused
406
    self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
407

  
408
    self._children = []
409

  
410
    mainloop.RegisterIO(self, self.socket.fileno(), select.POLLIN)
411
    mainloop.RegisterSignal(self)
412

  
413
  def Start(self):
414
    self.socket.bind((self.local_address, self.port))
415
    self.socket.listen(5)
416

  
417
  def Stop(self):
418
    self.socket.close()
419

  
420
  def OnIO(self, fd, condition):
421
    if condition & select.POLLIN:
422
      self._IncomingConnection()
423

  
424
  def OnSignal(self, signum):
425
    if signum == signal.SIGCHLD:
426
      self._CollectChildren(True)
427

  
428
  def _CollectChildren(self, quick):
429
    """Checks whether any child processes are done
430

  
431
    @type quick: bool
432
    @param quick: Whether to only use non-blocking functions
433

  
434
    """
435
    if not quick:
436
      # Don't wait for other processes if it should be a quick check
437
      while len(self._children) > self.MAX_CHILDREN:
438
        try:
439
          # Waiting without a timeout brings us into a potential DoS situation.
440
          # As soon as too many children run, we'll not respond to new
441
          # requests. The real solution would be to add a timeout for children
442
          # and killing them after some time.
443
          pid, status = os.waitpid(0, 0)
444
        except os.error:
445
          pid = None
446
        if pid and pid in self._children:
447
          self._children.remove(pid)
448

  
449
    for child in self._children:
450
      try:
451
        pid, status = os.waitpid(child, os.WNOHANG)
452
      except os.error:
453
        pid = None
454
      if pid and pid in self._children:
455
        self._children.remove(pid)
456

  
457
  def _IncomingConnection(self):
458
    """Called for each incoming connection
459

  
460
    """
461
    (connection, client_addr) = self.socket.accept()
462

  
463
    self._CollectChildren(False)
464

  
465
    pid = os.fork()
466
    if pid == 0:
467
      # Child process
468
      try:
469
        _HttpServerRequestExecutor(self, connection, client_addr)
470
      except Exception:
471
        logging.exception("Error while handling request from %s:%s",
472
                          client_addr[0], client_addr[1])
473
        os._exit(1)
474
      os._exit(0)
475
    else:
476
      self._children.append(pid)
477

  
478
  def HandleRequest(self, req):
479
    """Handles a request.
480

  
481
    Must be overriden by subclass.
482

  
483
    """
484
    raise NotImplementedError()

Also available in: Unified diff