Statistics
| Branch: | Tag: | Revision:

root / lib / http / server.py @ 68fa9caf

History | View | Annotate | Download (15.6 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(gmnow=None):
63
  """Return the current date and time formatted for a message header.
64

65
  The time MUST be in the GMT timezone.
66

67
  """
68
  if gmnow is None:
69
    gmnow = time.gmtime()
70
  (year, month, day, hh, mm, ss, wd, _, _) = gmnow
71
  return ("%s, %02d %3s %4d %02d:%02d:%02d GMT" %
72
          (WEEKDAYNAME[wd], day, MONTHNAME[month], year, hh, mm, ss))
73

    
74

    
75
class _HttpServerRequest(object):
76
  """Data structure for HTTP request on server side.
77

78
  """
79
  def __init__(self, request_msg):
80
    # Request attributes
81
    self.request_method = request_msg.start_line.method
82
    self.request_path = request_msg.start_line.path
83
    self.request_headers = request_msg.headers
84
    self.request_body = request_msg.decoded_body
85

    
86
    # Response attributes
87
    self.resp_headers = {}
88

    
89
    # Private data for request handler (useful in combination with
90
    # authentication)
91
    self.private = None
92

    
93

    
94
class _HttpServerToClientMessageWriter(http.HttpMessageWriter):
95
  """Writes an HTTP response to client.
96

97
  """
98
  def __init__(self, sock, request_msg, response_msg, write_timeout):
99
    """Writes the response to the client.
100

101
    @type sock: socket
102
    @param sock: Target socket
103
    @type request_msg: http.HttpMessage
104
    @param request_msg: Request message, required to determine whether
105
                        response may have a message body
106
    @type response_msg: http.HttpMessage
107
    @param response_msg: Response message
108
    @type write_timeout: float
109
    @param write_timeout: Write timeout for socket
110

111
    """
112
    self._request_msg = request_msg
113
    self._response_msg = response_msg
114
    http.HttpMessageWriter.__init__(self, sock, response_msg, write_timeout)
115

    
116
  def HasMessageBody(self):
117
    """Logic to detect whether response should contain a message body.
118

119
    """
120
    if self._request_msg.start_line:
121
      request_method = self._request_msg.start_line.method
122
    else:
123
      request_method = None
124

    
125
    response_code = self._response_msg.start_line.code
126

    
127
    # RFC2616, section 4.3: "A message-body MUST NOT be included in a request
128
    # if the specification of the request method (section 5.1.1) does not allow
129
    # sending an entity-body in requests"
130
    #
131
    # RFC2616, section 9.4: "The HEAD method is identical to GET except that
132
    # the server MUST NOT return a message-body in the response."
133
    #
134
    # RFC2616, section 10.2.5: "The 204 response MUST NOT include a
135
    # message-body [...]"
136
    #
137
    # RFC2616, section 10.3.5: "The 304 response MUST NOT contain a
138
    # message-body, [...]"
139

    
140
    return (http.HttpMessageWriter.HasMessageBody(self) and
141
            (request_method is not None and
142
             request_method != http.HTTP_HEAD) and
143
            response_code >= http.HTTP_OK and
144
            response_code not in (http.HTTP_NO_CONTENT,
145
                                  http.HTTP_NOT_MODIFIED))
146

    
147

    
148
class _HttpClientToServerMessageReader(http.HttpMessageReader):
149
  """Reads an HTTP request sent by client.
150

151
  """
152
  # Length limits
153
  START_LINE_LENGTH_MAX = 4096
154
  HEADER_LENGTH_MAX = 4096
155

    
156
  def ParseStartLine(self, start_line):
157
    """Parses the start line sent by client.
158

159
    Example: "GET /index.html HTTP/1.1"
160

161
    @type start_line: string
162
    @param start_line: Start line
163

164
    """
165
    # Empty lines are skipped when reading
166
    assert start_line
167

    
168
    logging.debug("HTTP request: %s", start_line)
169

    
170
    words = start_line.split()
171

    
172
    if len(words) == 3:
173
      [method, path, version] = words
174
      if version[:5] != 'HTTP/':
175
        raise http.HttpBadRequest("Bad request version (%r)" % version)
176

    
177
      try:
178
        base_version_number = version.split("/", 1)[1]
179
        version_number = base_version_number.split(".")
180

    
181
        # RFC 2145 section 3.1 says there can be only one "." and
182
        #   - major and minor numbers MUST be treated as
183
        #      separate integers;
184
        #   - HTTP/2.4 is a lower version than HTTP/2.13, which in
185
        #      turn is lower than HTTP/12.3;
186
        #   - Leading zeros MUST be ignored by recipients.
187
        if len(version_number) != 2:
188
          raise http.HttpBadRequest("Bad request version (%r)" % version)
189

    
190
        version_number = (int(version_number[0]), int(version_number[1]))
191
      except (ValueError, IndexError):
192
        raise http.HttpBadRequest("Bad request version (%r)" % version)
193

    
194
      if version_number >= (2, 0):
195
        raise http.HttpVersionNotSupported("Invalid HTTP Version (%s)" %
196
                                      base_version_number)
197

    
198
    elif len(words) == 2:
199
      version = http.HTTP_0_9
200
      [method, path] = words
201
      if method != http.HTTP_GET:
202
        raise http.HttpBadRequest("Bad HTTP/0.9 request type (%r)" % method)
203

    
204
    else:
205
      raise http.HttpBadRequest("Bad request syntax (%r)" % start_line)
206

    
207
    return http.HttpClientToServerStartLine(method, path, version)
208

    
209

    
210
class _HttpServerRequestExecutor(object):
211
  """Implements server side of HTTP.
212

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

217
  """
218
  # The default request version.  This only affects responses up until
219
  # the point where the request line is parsed, so it mainly decides what
220
  # the client gets back when sending a malformed request line.
221
  # Most web servers default to HTTP 0.9, i.e. don't send a status line.
222
  default_request_version = http.HTTP_0_9
223

    
224
  # Error message settings
225
  error_message_format = DEFAULT_ERROR_MESSAGE
226
  error_content_type = DEFAULT_ERROR_CONTENT_TYPE
227

    
228
  responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
229

    
230
  # Timeouts in seconds for socket layer
231
  WRITE_TIMEOUT = 10
232
  READ_TIMEOUT = 10
233
  CLOSE_TIMEOUT = 1
234

    
235
  def __init__(self, server, sock, client_addr):
236
    """Initializes this class.
237

238
    """
239
    self.server = server
240
    self.sock = sock
241
    self.client_addr = client_addr
242

    
243
    self.request_msg = http.HttpMessage()
244
    self.response_msg = http.HttpMessage()
245

    
246
    self.response_msg.start_line = \
247
      http.HttpServerToClientStartLine(version=self.default_request_version,
248
                                       code=None, reason=None)
249

    
250
    # Disable Python's timeout
251
    self.sock.settimeout(None)
252

    
253
    # Operate in non-blocking mode
254
    self.sock.setblocking(0)
255

    
256
    logging.info("Connection from %s:%s", client_addr[0], client_addr[1])
257
    try:
258
      request_msg_reader = None
259
      force_close = True
260
      try:
261
        # Do the secret SSL handshake
262
        if self.server.using_ssl:
263
          self.sock.set_accept_state()
264
          try:
265
            http.Handshake(self.sock, self.WRITE_TIMEOUT)
266
          except http.HttpSessionHandshakeUnexpectedEOF:
267
            # Ignore rest
268
            return
269

    
270
        try:
271
          try:
272
            request_msg_reader = self._ReadRequest()
273
            self._HandleRequest()
274

    
275
            # Only wait for client to close if we didn't have any exception.
276
            force_close = False
277
          except http.HttpException, err:
278
            self._SetErrorStatus(err)
279
        finally:
280
          # Try to send a response
281
          self._SendResponse()
282
      finally:
283
        http.ShutdownConnection(sock, self.CLOSE_TIMEOUT, self.WRITE_TIMEOUT,
284
                                request_msg_reader, force_close)
285

    
286
      self.sock.close()
287
      self.sock = None
288
    finally:
289
      logging.info("Disconnected %s:%s", client_addr[0], client_addr[1])
290

    
291
  def _ReadRequest(self):
292
    """Reads a request sent by client.
293

294
    """
295
    try:
296
      request_msg_reader = \
297
        _HttpClientToServerMessageReader(self.sock, self.request_msg,
298
                                         self.READ_TIMEOUT)
299
    except http.HttpSocketTimeout:
300
      raise http.HttpError("Timeout while reading request")
301
    except socket.error, err:
302
      raise http.HttpError("Error reading request: %s" % err)
303

    
304
    self.response_msg.start_line.version = self.request_msg.start_line.version
305

    
306
    return request_msg_reader
307

    
308
  def _HandleRequest(self):
309
    """Calls the handler function for the current request.
310

311
    """
312
    handler_context = _HttpServerRequest(self.request_msg)
313

    
314
    try:
315
      try:
316
        # Authentication, etc.
317
        self.server.PreHandleRequest(handler_context)
318

    
319
        # Call actual request handler
320
        result = self.server.HandleRequest(handler_context)
321
      except (http.HttpException, KeyboardInterrupt, SystemExit):
322
        raise
323
      except Exception, err:
324
        logging.exception("Caught exception")
325
        raise http.HttpInternalServerError(message=str(err))
326
      except:
327
        logging.exception("Unknown exception")
328
        raise http.HttpInternalServerError(message="Unknown error")
329

    
330
      # TODO: Content-type
331
      encoder = http.HttpJsonConverter()
332
      self.response_msg.start_line.code = http.HTTP_OK
333
      self.response_msg.body = encoder.Encode(result)
334
      self.response_msg.headers = handler_context.resp_headers
335
      self.response_msg.headers[http.HTTP_CONTENT_TYPE] = encoder.CONTENT_TYPE
336
    finally:
337
      # No reason to keep this any longer, even for exceptions
338
      handler_context.private = None
339

    
340
  def _SendResponse(self):
341
    """Sends the response to the client.
342

343
    """
344
    if self.response_msg.start_line.code is None:
345
      return
346

    
347
    if not self.response_msg.headers:
348
      self.response_msg.headers = {}
349

    
350
    self.response_msg.headers.update({
351
      # TODO: Keep-alive is not supported
352
      http.HTTP_CONNECTION: "close",
353
      http.HTTP_DATE: _DateTimeHeader(),
354
      http.HTTP_SERVER: http.HTTP_GANETI_VERSION,
355
      })
356

    
357
    # Get response reason based on code
358
    response_code = self.response_msg.start_line.code
359
    if response_code in self.responses:
360
      response_reason = self.responses[response_code][0]
361
    else:
362
      response_reason = ""
363
    self.response_msg.start_line.reason = response_reason
364

    
365
    logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1],
366
                 self.request_msg.start_line, response_code)
367

    
368
    try:
369
      _HttpServerToClientMessageWriter(self.sock, self.request_msg,
370
                                       self.response_msg, self.WRITE_TIMEOUT)
371
    except http.HttpSocketTimeout:
372
      raise http.HttpError("Timeout while sending response")
373
    except socket.error, err:
374
      raise http.HttpError("Error sending response: %s" % err)
375

    
376
  def _SetErrorStatus(self, err):
377
    """Sets the response code and body from a HttpException.
378

379
    @type err: HttpException
380
    @param err: Exception instance
381

382
    """
383
    try:
384
      (shortmsg, longmsg) = self.responses[err.code]
385
    except KeyError:
386
      shortmsg = longmsg = "Unknown"
387

    
388
    if err.message:
389
      message = err.message
390
    else:
391
      message = shortmsg
392

    
393
    values = {
394
      "code": err.code,
395
      "message": cgi.escape(message),
396
      "explain": longmsg,
397
      }
398

    
399
    self.response_msg.start_line.code = err.code
400

    
401
    headers = {}
402
    if err.headers:
403
      headers.update(err.headers)
404
    headers[http.HTTP_CONTENT_TYPE] = self.error_content_type
405
    self.response_msg.headers = headers
406

    
407
    self.response_msg.body = self.error_message_format % values
408

    
409

    
410
class HttpServer(http.HttpBase):
411
  """Generic HTTP server class
412

413
  Users of this class must subclass it and override the HandleRequest function.
414

415
  """
416
  MAX_CHILDREN = 20
417

    
418
  def __init__(self, mainloop, local_address, port,
419
               ssl_params=None, ssl_verify_peer=False):
420
    """Initializes the HTTP server
421

422
    @type mainloop: ganeti.daemon.Mainloop
423
    @param mainloop: Mainloop used to poll for I/O events
424
    @type local_address: string
425
    @param local_address: Local IP address to bind to
426
    @type port: int
427
    @param port: TCP port to listen on
428
    @type ssl_params: HttpSslParams
429
    @param ssl_params: SSL key and certificate
430
    @type ssl_verify_peer: bool
431
    @param ssl_verify_peer: Whether to require client certificate and compare
432
                            it with our certificate
433

434
    """
435
    http.HttpBase.__init__(self)
436

    
437
    self.mainloop = mainloop
438
    self.local_address = local_address
439
    self.port = port
440

    
441
    self.socket = self._CreateSocket(ssl_params, ssl_verify_peer)
442

    
443
    # Allow port to be reused
444
    self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
445

    
446
    self._children = []
447

    
448
    mainloop.RegisterIO(self, self.socket.fileno(), select.POLLIN)
449
    mainloop.RegisterSignal(self)
450

    
451
  def Start(self):
452
    self.socket.bind((self.local_address, self.port))
453
    self.socket.listen(1024)
454

    
455
  def Stop(self):
456
    self.socket.close()
457

    
458
  def OnIO(self, fd, condition):
459
    if condition & select.POLLIN:
460
      self._IncomingConnection()
461

    
462
  def OnSignal(self, signum):
463
    if signum == signal.SIGCHLD:
464
      self._CollectChildren(True)
465

    
466
  def _CollectChildren(self, quick):
467
    """Checks whether any child processes are done
468

469
    @type quick: bool
470
    @param quick: Whether to only use non-blocking functions
471

472
    """
473
    if not quick:
474
      # Don't wait for other processes if it should be a quick check
475
      while len(self._children) > self.MAX_CHILDREN:
476
        try:
477
          # Waiting without a timeout brings us into a potential DoS situation.
478
          # As soon as too many children run, we'll not respond to new
479
          # requests. The real solution would be to add a timeout for children
480
          # and killing them after some time.
481
          pid, status = os.waitpid(0, 0)
482
        except os.error:
483
          pid = None
484
        if pid and pid in self._children:
485
          self._children.remove(pid)
486

    
487
    for child in self._children:
488
      try:
489
        pid, status = os.waitpid(child, os.WNOHANG)
490
      except os.error:
491
        pid = None
492
      if pid and pid in self._children:
493
        self._children.remove(pid)
494

    
495
  def _IncomingConnection(self):
496
    """Called for each incoming connection
497

498
    """
499
    (connection, client_addr) = self.socket.accept()
500

    
501
    self._CollectChildren(False)
502

    
503
    pid = os.fork()
504
    if pid == 0:
505
      # Child process
506
      try:
507
        _HttpServerRequestExecutor(self, connection, client_addr)
508
      except Exception:
509
        logging.exception("Error while handling request from %s:%s",
510
                          client_addr[0], client_addr[1])
511
        os._exit(1)
512
      os._exit(0)
513
    else:
514
      self._children.append(pid)
515

    
516
  def PreHandleRequest(self, req):
517
    """Called before handling a request.
518

519
    Can be overriden by a subclass.
520

521
    """
522

    
523
  def HandleRequest(self, req):
524
    """Handles a request.
525

526
    Must be overriden by subclass.
527

528
    """
529
    raise NotImplementedError()