Statistics
| Branch: | Tag: | Revision:

root / lib / http / server.py @ f2e13d55

History | View | Annotate | Download (15 kB)

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
    """Writes the response to the client.
92

93
    @type sock: socket
94
    @param sock: Target socket
95
    @type request_msg: http.HttpMessage
96
    @param request_msg: Request message, required to determine whether
97
                        response may have a message body
98
    @type response_msg: http.HttpMessage
99
    @param response_msg: Response message
100
    @type write_timeout: float
101
    @param write_timeout: Write timeout for socket
102

103
    """
104
    self._request_msg = request_msg
105
    self._response_msg = response_msg
106
    http.HttpMessageWriter.__init__(self, sock, response_msg, write_timeout)
107

    
108
  def HasMessageBody(self):
109
    """Logic to detect whether response should contain a message body.
110

111
    """
112
    if self._request_msg.start_line:
113
      request_method = self._request_msg.start_line.method
114
    else:
115
      request_method = None
116

    
117
    response_code = self._response_msg.start_line.code
118

    
119
    # RFC2616, section 4.3: "A message-body MUST NOT be included in a request
120
    # if the specification of the request method (section 5.1.1) does not allow
121
    # sending an entity-body in requests"
122
    #
123
    # RFC2616, section 9.4: "The HEAD method is identical to GET except that
124
    # the server MUST NOT return a message-body in the response."
125
    #
126
    # RFC2616, section 10.2.5: "The 204 response MUST NOT include a
127
    # message-body [...]"
128
    #
129
    # RFC2616, section 10.3.5: "The 304 response MUST NOT contain a
130
    # message-body, [...]"
131

    
132
    return (http.HttpMessageWriter.HasMessageBody(self) and
133
            (request_method is not None and
134
             request_method != http.HTTP_HEAD) and
135
            response_code >= http.HTTP_OK and
136
            response_code not in (http.HTTP_NO_CONTENT,
137
                                  http.HTTP_NOT_MODIFIED))
138

    
139

    
140
class _HttpClientToServerMessageReader(http.HttpMessageReader):
141
  """Reads an HTTP request sent by client.
142

143
  """
144
  # Length limits
145
  START_LINE_LENGTH_MAX = 4096
146
  HEADER_LENGTH_MAX = 4096
147

    
148
  def ParseStartLine(self, start_line):
149
    """Parses the start line sent by client.
150

151
    Example: "GET /index.html HTTP/1.1"
152

153
    @type start_line: string
154
    @param start_line: Start line
155

156
    """
157
    # Empty lines are skipped when reading
158
    assert start_line
159

    
160
    logging.debug("HTTP request: %s", start_line)
161

    
162
    words = start_line.split()
163

    
164
    if len(words) == 3:
165
      [method, path, version] = words
166
      if version[:5] != 'HTTP/':
167
        raise http.HttpBadRequest("Bad request version (%r)" % version)
168

    
169
      try:
170
        base_version_number = version.split("/", 1)[1]
171
        version_number = base_version_number.split(".")
172

    
173
        # RFC 2145 section 3.1 says there can be only one "." and
174
        #   - major and minor numbers MUST be treated as
175
        #      separate integers;
176
        #   - HTTP/2.4 is a lower version than HTTP/2.13, which in
177
        #      turn is lower than HTTP/12.3;
178
        #   - Leading zeros MUST be ignored by recipients.
179
        if len(version_number) != 2:
180
          raise http.HttpBadRequest("Bad request version (%r)" % version)
181

    
182
        version_number = (int(version_number[0]), int(version_number[1]))
183
      except (ValueError, IndexError):
184
        raise http.HttpBadRequest("Bad request version (%r)" % version)
185

    
186
      if version_number >= (2, 0):
187
        raise http.HttpVersionNotSupported("Invalid HTTP Version (%s)" %
188
                                      base_version_number)
189

    
190
    elif len(words) == 2:
191
      version = http.HTTP_0_9
192
      [method, path] = words
193
      if method != http.HTTP_GET:
194
        raise http.HttpBadRequest("Bad HTTP/0.9 request type (%r)" % method)
195

    
196
    else:
197
      raise http.HttpBadRequest("Bad request syntax (%r)" % start_line)
198

    
199
    return http.HttpClientToServerStartLine(method, path, version)
200

    
201

    
202
class _HttpServerRequestExecutor(object):
203
  """Implements server side of HTTP.
204

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

209
  """
210
  # The default request version.  This only affects responses up until
211
  # the point where the request line is parsed, so it mainly decides what
212
  # the client gets back when sending a malformed request line.
213
  # Most web servers default to HTTP 0.9, i.e. don't send a status line.
214
  default_request_version = http.HTTP_0_9
215

    
216
  # Error message settings
217
  error_message_format = DEFAULT_ERROR_MESSAGE
218
  error_content_type = DEFAULT_ERROR_CONTENT_TYPE
219

    
220
  responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
221

    
222
  # Timeouts in seconds for socket layer
223
  WRITE_TIMEOUT = 10
224
  READ_TIMEOUT = 10
225
  CLOSE_TIMEOUT = 1
226

    
227
  def __init__(self, server, sock, client_addr):
228
    """Initializes this class.
229

230
    """
231
    self.server = server
232
    self.sock = sock
233
    self.client_addr = client_addr
234

    
235
    self.poller = select.poll()
236

    
237
    self.request_msg = http.HttpMessage()
238
    self.response_msg = http.HttpMessage()
239

    
240
    self.response_msg.start_line = \
241
      http.HttpServerToClientStartLine(version=self.default_request_version,
242
                                       code=None, reason=None)
243

    
244
    # Disable Python's timeout
245
    self.sock.settimeout(None)
246

    
247
    # Operate in non-blocking mode
248
    self.sock.setblocking(0)
249

    
250
    logging.info("Connection from %s:%s", client_addr[0], client_addr[1])
251
    try:
252
      request_msg_reader = None
253
      force_close = True
254
      try:
255
        # Do the secret SSL handshake
256
        if self.server.using_ssl:
257
          self.sock.set_accept_state()
258
          try:
259
            http.Handshake(self.poller, self.sock, self.WRITE_TIMEOUT)
260
          except http.HttpSessionHandshakeUnexpectedEOF:
261
            # Ignore rest
262
            return
263

    
264
        try:
265
          try:
266
            request_msg_reader = self._ReadRequest()
267
            self._HandleRequest()
268

    
269
            # Only wait for client to close if we didn't have any exception.
270
            force_close = False
271
          except http.HttpException, err:
272
            self._SetErrorStatus(err)
273
        finally:
274
          # Try to send a response
275
          self._SendResponse()
276
      finally:
277
        http.ShutdownConnection(self.poller, sock,
278
                                self.CLOSE_TIMEOUT, self.WRITE_TIMEOUT,
279
                                request_msg_reader, force_close)
280

    
281
      self.sock.close()
282
      self.sock = None
283
    finally:
284
      logging.info("Disconnected %s:%s", client_addr[0], client_addr[1])
285

    
286
  def _ReadRequest(self):
287
    """Reads a request sent by client.
288

289
    """
290
    try:
291
      request_msg_reader = \
292
        _HttpClientToServerMessageReader(self.sock, self.request_msg,
293
                                         self.READ_TIMEOUT)
294
    except http.HttpSocketTimeout:
295
      raise http.HttpError("Timeout while reading request")
296
    except socket.error, err:
297
      raise http.HttpError("Error reading request: %s" % err)
298

    
299
    self.response_msg.start_line.version = self.request_msg.start_line.version
300

    
301
    return request_msg_reader
302

    
303
  def _HandleRequest(self):
304
    """Calls the handler function for the current request.
305

306
    """
307
    handler_context = _HttpServerRequest(self.request_msg)
308

    
309
    try:
310
      result = self.server.HandleRequest(handler_context)
311
    except (http.HttpException, KeyboardInterrupt, SystemExit):
312
      raise
313
    except Exception, err:
314
      logging.exception("Caught exception")
315
      raise http.HttpInternalError(message=str(err))
316
    except:
317
      logging.exception("Unknown exception")
318
      raise http.HttpInternalError(message="Unknown error")
319

    
320
    # TODO: Content-type
321
    encoder = http.HttpJsonConverter()
322
    self.response_msg.start_line.code = http.HTTP_OK
323
    self.response_msg.body = encoder.Encode(result)
324
    self.response_msg.headers = handler_context.resp_headers
325
    self.response_msg.headers[http.HTTP_CONTENT_TYPE] = encoder.CONTENT_TYPE
326

    
327
  def _SendResponse(self):
328
    """Sends the response to the client.
329

330
    """
331
    if self.response_msg.start_line.code is None:
332
      return
333

    
334
    if not self.response_msg.headers:
335
      self.response_msg.headers = {}
336

    
337
    self.response_msg.headers.update({
338
      # TODO: Keep-alive is not supported
339
      http.HTTP_CONNECTION: "close",
340
      http.HTTP_DATE: _DateTimeHeader(),
341
      http.HTTP_SERVER: http.HTTP_GANETI_VERSION,
342
      })
343

    
344
    # Get response reason based on code
345
    response_code = self.response_msg.start_line.code
346
    if response_code in self.responses:
347
      response_reason = self.responses[response_code][0]
348
    else:
349
      response_reason = ""
350
    self.response_msg.start_line.reason = response_reason
351

    
352
    logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1],
353
                 self.request_msg.start_line, response_code)
354

    
355
    try:
356
      _HttpServerToClientMessageWriter(self.sock, self.request_msg,
357
                                       self.response_msg, self.WRITE_TIMEOUT)
358
    except http.HttpSocketTimeout:
359
      raise http.HttpError("Timeout while sending response")
360
    except socket.error, err:
361
      raise http.HttpError("Error sending response: %s" % err)
362

    
363
  def _SetErrorStatus(self, err):
364
    """Sets the response code and body from a HttpException.
365

366
    @type err: HttpException
367
    @param err: Exception instance
368

369
    """
370
    try:
371
      (shortmsg, longmsg) = self.responses[err.code]
372
    except KeyError:
373
      shortmsg = longmsg = "Unknown"
374

    
375
    if err.message:
376
      message = err.message
377
    else:
378
      message = shortmsg
379

    
380
    values = {
381
      "code": err.code,
382
      "message": cgi.escape(message),
383
      "explain": longmsg,
384
      }
385

    
386
    self.response_msg.start_line.code = err.code
387
    self.response_msg.headers = {
388
      http.HTTP_CONTENT_TYPE: self.error_content_type,
389
      }
390
    self.response_msg.body = self.error_message_format % values
391

    
392

    
393
class HttpServer(http.HttpBase):
394
  """Generic HTTP server class
395

396
  Users of this class must subclass it and override the HandleRequest function.
397

398
  """
399
  MAX_CHILDREN = 20
400

    
401
  def __init__(self, mainloop, local_address, port,
402
               ssl_params=None, ssl_verify_peer=False):
403
    """Initializes the HTTP server
404

405
    @type mainloop: ganeti.daemon.Mainloop
406
    @param mainloop: Mainloop used to poll for I/O events
407
    @type local_address: string
408
    @param local_address: Local IP address to bind to
409
    @type port: int
410
    @param port: TCP port to listen on
411
    @type ssl_params: HttpSslParams
412
    @param ssl_params: SSL key and certificate
413
    @type ssl_verify_peer: bool
414
    @param ssl_verify_peer: Whether to require client certificate and compare
415
                            it with our certificate
416

417
    """
418
    http.HttpBase.__init__(self)
419

    
420
    self.mainloop = mainloop
421
    self.local_address = local_address
422
    self.port = port
423

    
424
    self.socket = self._CreateSocket(ssl_params, ssl_verify_peer)
425

    
426
    # Allow port to be reused
427
    self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
428

    
429
    self._children = []
430

    
431
    mainloop.RegisterIO(self, self.socket.fileno(), select.POLLIN)
432
    mainloop.RegisterSignal(self)
433

    
434
  def Start(self):
435
    self.socket.bind((self.local_address, self.port))
436
    self.socket.listen(1024)
437

    
438
  def Stop(self):
439
    self.socket.close()
440

    
441
  def OnIO(self, fd, condition):
442
    if condition & select.POLLIN:
443
      self._IncomingConnection()
444

    
445
  def OnSignal(self, signum):
446
    if signum == signal.SIGCHLD:
447
      self._CollectChildren(True)
448

    
449
  def _CollectChildren(self, quick):
450
    """Checks whether any child processes are done
451

452
    @type quick: bool
453
    @param quick: Whether to only use non-blocking functions
454

455
    """
456
    if not quick:
457
      # Don't wait for other processes if it should be a quick check
458
      while len(self._children) > self.MAX_CHILDREN:
459
        try:
460
          # Waiting without a timeout brings us into a potential DoS situation.
461
          # As soon as too many children run, we'll not respond to new
462
          # requests. The real solution would be to add a timeout for children
463
          # and killing them after some time.
464
          pid, status = os.waitpid(0, 0)
465
        except os.error:
466
          pid = None
467
        if pid and pid in self._children:
468
          self._children.remove(pid)
469

    
470
    for child in self._children:
471
      try:
472
        pid, status = os.waitpid(child, os.WNOHANG)
473
      except os.error:
474
        pid = None
475
      if pid and pid in self._children:
476
        self._children.remove(pid)
477

    
478
  def _IncomingConnection(self):
479
    """Called for each incoming connection
480

481
    """
482
    (connection, client_addr) = self.socket.accept()
483

    
484
    self._CollectChildren(False)
485

    
486
    pid = os.fork()
487
    if pid == 0:
488
      # Child process
489
      try:
490
        _HttpServerRequestExecutor(self, connection, client_addr)
491
      except Exception:
492
        logging.exception("Error while handling request from %s:%s",
493
                          client_addr[0], client_addr[1])
494
        os._exit(1)
495
      os._exit(0)
496
    else:
497
      self._children.append(pid)
498

    
499
  def HandleRequest(self, req):
500
    """Handles a request.
501

502
    Must be overriden by subclass.
503

504
    """
505
    raise NotImplementedError()