Statistics
| Branch: | Tag: | Revision:

root / lib / http / server.py @ 3f3dfc15

History | View | Annotate | Download (14.7 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
        try:
256
          try:
257
            request_msg_reader = self._ReadRequest()
258
            self._HandleRequest()
259

    
260
            # Only wait for client to close if we didn't have any exception.
261
            force_close = False
262
          except http.HttpException, err:
263
            self._SetErrorStatus(err)
264
        finally:
265
          # Try to send a response
266
          self._SendResponse()
267
      finally:
268
        http.ShutdownConnection(self.poller, sock,
269
                                self.CLOSE_TIMEOUT, self.WRITE_TIMEOUT,
270
                                request_msg_reader, force_close)
271

    
272
      self.sock.close()
273
      self.sock = None
274
    finally:
275
      logging.info("Disconnected %s:%s", client_addr[0], client_addr[1])
276

    
277
  def _ReadRequest(self):
278
    """Reads a request sent by client.
279

280
    """
281
    try:
282
      request_msg_reader = \
283
        _HttpClientToServerMessageReader(self.sock, self.request_msg,
284
                                         self.READ_TIMEOUT)
285
    except http.HttpSocketTimeout:
286
      raise http.HttpError("Timeout while reading request")
287
    except socket.error, err:
288
      raise http.HttpError("Error reading request: %s" % err)
289

    
290
    self.response_msg.start_line.version = self.request_msg.start_line.version
291

    
292
    return request_msg_reader
293

    
294
  def _HandleRequest(self):
295
    """Calls the handler function for the current request.
296

297
    """
298
    handler_context = _HttpServerRequest(self.request_msg)
299

    
300
    try:
301
      result = self.server.HandleRequest(handler_context)
302
    except (http.HttpException, KeyboardInterrupt, SystemExit):
303
      raise
304
    except Exception, err:
305
      logging.exception("Caught exception")
306
      raise http.HttpInternalError(message=str(err))
307
    except:
308
      logging.exception("Unknown exception")
309
      raise http.HttpInternalError(message="Unknown error")
310

    
311
    # TODO: Content-type
312
    encoder = http.HttpJsonConverter()
313
    self.response_msg.start_line.code = http.HTTP_OK
314
    self.response_msg.body = encoder.Encode(result)
315
    self.response_msg.headers = handler_context.resp_headers
316
    self.response_msg.headers[http.HTTP_CONTENT_TYPE] = encoder.CONTENT_TYPE
317

    
318
  def _SendResponse(self):
319
    """Sends the response to the client.
320

321
    """
322
    if self.response_msg.start_line.code is None:
323
      return
324

    
325
    if not self.response_msg.headers:
326
      self.response_msg.headers = {}
327

    
328
    self.response_msg.headers.update({
329
      # TODO: Keep-alive is not supported
330
      http.HTTP_CONNECTION: "close",
331
      http.HTTP_DATE: _DateTimeHeader(),
332
      http.HTTP_SERVER: http.HTTP_GANETI_VERSION,
333
      })
334

    
335
    # Get response reason based on code
336
    response_code = self.response_msg.start_line.code
337
    if response_code in self.responses:
338
      response_reason = self.responses[response_code][0]
339
    else:
340
      response_reason = ""
341
    self.response_msg.start_line.reason = response_reason
342

    
343
    logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1],
344
                 self.request_msg.start_line, response_code)
345

    
346
    try:
347
      _HttpServerToClientMessageWriter(self.sock, self.request_msg,
348
                                       self.response_msg, self.WRITE_TIMEOUT)
349
    except http.HttpSocketTimeout:
350
      raise http.HttpError("Timeout while sending response")
351
    except socket.error, err:
352
      raise http.HttpError("Error sending response: %s" % err)
353

    
354
  def _SetErrorStatus(self, err):
355
    """Sets the response code and body from a HttpException.
356

357
    @type err: HttpException
358
    @param err: Exception instance
359

360
    """
361
    try:
362
      (shortmsg, longmsg) = self.responses[err.code]
363
    except KeyError:
364
      shortmsg = longmsg = "Unknown"
365

    
366
    if err.message:
367
      message = err.message
368
    else:
369
      message = shortmsg
370

    
371
    values = {
372
      "code": err.code,
373
      "message": cgi.escape(message),
374
      "explain": longmsg,
375
      }
376

    
377
    self.response_msg.start_line.code = err.code
378
    self.response_msg.headers = {
379
      http.HTTP_CONTENT_TYPE: self.error_content_type,
380
      }
381
    self.response_msg.body = self.error_message_format % values
382

    
383

    
384
class HttpServer(http.HttpBase):
385
  """Generic HTTP server class
386

387
  Users of this class must subclass it and override the HandleRequest function.
388

389
  """
390
  MAX_CHILDREN = 20
391

    
392
  def __init__(self, mainloop, local_address, port,
393
               ssl_params=None, ssl_verify_peer=False):
394
    """Initializes the HTTP server
395

396
    @type mainloop: ganeti.daemon.Mainloop
397
    @param mainloop: Mainloop used to poll for I/O events
398
    @type local_address: string
399
    @param local_address: Local IP address to bind to
400
    @type port: int
401
    @param port: TCP port to listen on
402
    @type ssl_params: HttpSslParams
403
    @param ssl_params: SSL key and certificate
404
    @type ssl_verify_peer: bool
405
    @param ssl_verify_peer: Whether to require client certificate and compare
406
                            it with our certificate
407

408
    """
409
    http.HttpBase.__init__(self)
410

    
411
    self.mainloop = mainloop
412
    self.local_address = local_address
413
    self.port = port
414

    
415
    self.socket = self._CreateSocket(ssl_params, ssl_verify_peer)
416

    
417
    # Allow port to be reused
418
    self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
419

    
420
    self._children = []
421

    
422
    mainloop.RegisterIO(self, self.socket.fileno(), select.POLLIN)
423
    mainloop.RegisterSignal(self)
424

    
425
  def Start(self):
426
    self.socket.bind((self.local_address, self.port))
427
    self.socket.listen(1024)
428

    
429
  def Stop(self):
430
    self.socket.close()
431

    
432
  def OnIO(self, fd, condition):
433
    if condition & select.POLLIN:
434
      self._IncomingConnection()
435

    
436
  def OnSignal(self, signum):
437
    if signum == signal.SIGCHLD:
438
      self._CollectChildren(True)
439

    
440
  def _CollectChildren(self, quick):
441
    """Checks whether any child processes are done
442

443
    @type quick: bool
444
    @param quick: Whether to only use non-blocking functions
445

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

    
461
    for child in self._children:
462
      try:
463
        pid, status = os.waitpid(child, os.WNOHANG)
464
      except os.error:
465
        pid = None
466
      if pid and pid in self._children:
467
        self._children.remove(pid)
468

    
469
  def _IncomingConnection(self):
470
    """Called for each incoming connection
471

472
    """
473
    (connection, client_addr) = self.socket.accept()
474

    
475
    self._CollectChildren(False)
476

    
477
    pid = os.fork()
478
    if pid == 0:
479
      # Child process
480
      try:
481
        _HttpServerRequestExecutor(self, connection, client_addr)
482
      except Exception:
483
        logging.exception("Error while handling request from %s:%s",
484
                          client_addr[0], client_addr[1])
485
        os._exit(1)
486
      os._exit(0)
487
    else:
488
      self._children.append(pid)
489

    
490
  def HandleRequest(self, req):
491
    """Handles a request.
492

493
    Must be overriden by subclass.
494

495
    """
496
    raise NotImplementedError()