Statistics
| Branch: | Tag: | Revision:

root / lib / http / server.py @ d44ea6a3

History | View | Annotate | Download (17.1 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 socket
30
import time
31
import signal
32
import asyncore
33

    
34
from ganeti import http
35
from ganeti import utils
36

    
37

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

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

    
59

    
60
def _DateTimeHeader(gmnow=None):
61
  """Return the current date and time formatted for a message header.
62

63
  The time MUST be in the GMT timezone.
64

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

    
72

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

76
  """
77
  def __init__(self, method, path, headers, body):
78
    # Request attributes
79
    self.request_method = method
80
    self.request_path = path
81
    self.request_headers = headers
82
    self.request_body = body
83

    
84
    # Response attributes
85
    self.resp_headers = {}
86

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

    
91
  def __repr__(self):
92
    status = ["%s.%s" % (self.__class__.__module__, self.__class__.__name__),
93
              self.request_method, self.request_path,
94
              "headers=%r" % str(self.request_headers),
95
              "body=%r" % (self.request_body, )]
96

    
97
    return "<%s at %#x>" % (" ".join(status), id(self))
98

    
99

    
100
class _HttpServerToClientMessageWriter(http.HttpMessageWriter):
101
  """Writes an HTTP response to client.
102

103
  """
104
  def __init__(self, sock, request_msg, response_msg, write_timeout):
105
    """Writes the response to the client.
106

107
    @type sock: socket
108
    @param sock: Target socket
109
    @type request_msg: http.HttpMessage
110
    @param request_msg: Request message, required to determine whether
111
        response may have a message body
112
    @type response_msg: http.HttpMessage
113
    @param response_msg: Response message
114
    @type write_timeout: float
115
    @param write_timeout: Write timeout for socket
116

117
    """
118
    self._request_msg = request_msg
119
    self._response_msg = response_msg
120
    http.HttpMessageWriter.__init__(self, sock, response_msg, write_timeout)
121

    
122
  def HasMessageBody(self):
123
    """Logic to detect whether response should contain a message body.
124

125
    """
126
    if self._request_msg.start_line:
127
      request_method = self._request_msg.start_line.method
128
    else:
129
      request_method = None
130

    
131
    response_code = self._response_msg.start_line.code
132

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

    
146
    return (http.HttpMessageWriter.HasMessageBody(self) and
147
            (request_method is not None and
148
             request_method != http.HTTP_HEAD) and
149
            response_code >= http.HTTP_OK and
150
            response_code not in (http.HTTP_NO_CONTENT,
151
                                  http.HTTP_NOT_MODIFIED))
152

    
153

    
154
class _HttpClientToServerMessageReader(http.HttpMessageReader):
155
  """Reads an HTTP request sent by client.
156

157
  """
158
  # Length limits
159
  START_LINE_LENGTH_MAX = 4096
160
  HEADER_LENGTH_MAX = 4096
161

    
162
  def ParseStartLine(self, start_line):
163
    """Parses the start line sent by client.
164

165
    Example: "GET /index.html HTTP/1.1"
166

167
    @type start_line: string
168
    @param start_line: Start line
169

170
    """
171
    # Empty lines are skipped when reading
172
    assert start_line
173

    
174
    logging.debug("HTTP request: %s", start_line)
175

    
176
    words = start_line.split()
177

    
178
    if len(words) == 3:
179
      [method, path, version] = words
180
      if version[:5] != 'HTTP/':
181
        raise http.HttpBadRequest("Bad request version (%r)" % version)
182

    
183
      try:
184
        base_version_number = version.split("/", 1)[1]
185
        version_number = base_version_number.split(".")
186

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

    
196
        version_number = (int(version_number[0]), int(version_number[1]))
197
      except (ValueError, IndexError):
198
        raise http.HttpBadRequest("Bad request version (%r)" % version)
199

    
200
      if version_number >= (2, 0):
201
        raise http.HttpVersionNotSupported("Invalid HTTP Version (%s)" %
202
                                      base_version_number)
203

    
204
    elif len(words) == 2:
205
      version = http.HTTP_0_9
206
      [method, path] = words
207
      if method != http.HTTP_GET:
208
        raise http.HttpBadRequest("Bad HTTP/0.9 request type (%r)" % method)
209

    
210
    else:
211
      raise http.HttpBadRequest("Bad request syntax (%r)" % start_line)
212

    
213
    return http.HttpClientToServerStartLine(method, path, version)
214

    
215

    
216
class HttpServerRequestExecutor(object):
217
  """Implements server side of HTTP.
218

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

224
  """
225
  # The default request version.  This only affects responses up until
226
  # the point where the request line is parsed, so it mainly decides what
227
  # the client gets back when sending a malformed request line.
228
  # Most web servers default to HTTP 0.9, i.e. don't send a status line.
229
  default_request_version = http.HTTP_0_9
230

    
231
  # Error message settings
232
  error_message_format = DEFAULT_ERROR_MESSAGE
233
  error_content_type = DEFAULT_ERROR_CONTENT_TYPE
234

    
235
  responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
236

    
237
  # Timeouts in seconds for socket layer
238
  WRITE_TIMEOUT = 10
239
  READ_TIMEOUT = 10
240
  CLOSE_TIMEOUT = 1
241

    
242
  def __init__(self, server, sock, client_addr):
243
    """Initializes this class.
244

245
    """
246
    self.server = server
247
    self.sock = sock
248
    self.client_addr = client_addr
249

    
250
    self.request_msg = http.HttpMessage()
251
    self.response_msg = http.HttpMessage()
252

    
253
    self.response_msg.start_line = \
254
      http.HttpServerToClientStartLine(version=self.default_request_version,
255
                                       code=None, reason=None)
256

    
257
    # Disable Python's timeout
258
    self.sock.settimeout(None)
259

    
260
    # Operate in non-blocking mode
261
    self.sock.setblocking(0)
262

    
263
    logging.debug("Connection from %s:%s", client_addr[0], client_addr[1])
264
    try:
265
      request_msg_reader = None
266
      force_close = True
267
      try:
268
        # Do the secret SSL handshake
269
        if self.server.using_ssl:
270
          self.sock.set_accept_state()
271
          try:
272
            http.Handshake(self.sock, self.WRITE_TIMEOUT)
273
          except http.HttpSessionHandshakeUnexpectedEOF:
274
            # Ignore rest
275
            return
276

    
277
        try:
278
          try:
279
            request_msg_reader = self._ReadRequest()
280
            self._HandleRequest()
281

    
282
            # Only wait for client to close if we didn't have any exception.
283
            force_close = False
284
          except http.HttpException, err:
285
            self._SetErrorStatus(err)
286
        finally:
287
          # Try to send a response
288
          self._SendResponse()
289
      finally:
290
        http.ShutdownConnection(sock, self.CLOSE_TIMEOUT, self.WRITE_TIMEOUT,
291
                                request_msg_reader, force_close)
292

    
293
      self.sock.close()
294
      self.sock = None
295
    finally:
296
      logging.debug("Disconnected %s:%s", client_addr[0], client_addr[1])
297

    
298
  def _ReadRequest(self):
299
    """Reads a request sent by client.
300

301
    """
302
    try:
303
      request_msg_reader = \
304
        _HttpClientToServerMessageReader(self.sock, self.request_msg,
305
                                         self.READ_TIMEOUT)
306
    except http.HttpSocketTimeout:
307
      raise http.HttpError("Timeout while reading request")
308
    except socket.error, err:
309
      raise http.HttpError("Error reading request: %s" % err)
310

    
311
    self.response_msg.start_line.version = self.request_msg.start_line.version
312

    
313
    return request_msg_reader
314

    
315
  def _HandleRequest(self):
316
    """Calls the handler function for the current request.
317

318
    """
319
    handler_context = _HttpServerRequest(self.request_msg.start_line.method,
320
                                         self.request_msg.start_line.path,
321
                                         self.request_msg.headers,
322
                                         self.request_msg.decoded_body)
323

    
324
    logging.debug("Handling request %r", handler_context)
325

    
326
    try:
327
      try:
328
        # Authentication, etc.
329
        self.server.PreHandleRequest(handler_context)
330

    
331
        # Call actual request handler
332
        result = self.server.HandleRequest(handler_context)
333
      except (http.HttpException, KeyboardInterrupt, SystemExit):
334
        raise
335
      except Exception, err:
336
        logging.exception("Caught exception")
337
        raise http.HttpInternalServerError(message=str(err))
338
      except:
339
        logging.exception("Unknown exception")
340
        raise http.HttpInternalServerError(message="Unknown error")
341

    
342
      # TODO: Content-type
343
      encoder = http.HttpJsonConverter()
344
      self.response_msg.start_line.code = http.HTTP_OK
345
      self.response_msg.body = encoder.Encode(result)
346
      self.response_msg.headers = handler_context.resp_headers
347
      self.response_msg.headers[http.HTTP_CONTENT_TYPE] = encoder.CONTENT_TYPE
348
    finally:
349
      # No reason to keep this any longer, even for exceptions
350
      handler_context.private = None
351

    
352
  def _SendResponse(self):
353
    """Sends the response to the client.
354

355
    """
356
    if self.response_msg.start_line.code is None:
357
      return
358

    
359
    if not self.response_msg.headers:
360
      self.response_msg.headers = {}
361

    
362
    self.response_msg.headers.update({
363
      # TODO: Keep-alive is not supported
364
      http.HTTP_CONNECTION: "close",
365
      http.HTTP_DATE: _DateTimeHeader(),
366
      http.HTTP_SERVER: http.HTTP_GANETI_VERSION,
367
      })
368

    
369
    # Get response reason based on code
370
    response_code = self.response_msg.start_line.code
371
    if response_code in self.responses:
372
      response_reason = self.responses[response_code][0]
373
    else:
374
      response_reason = ""
375
    self.response_msg.start_line.reason = response_reason
376

    
377
    logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1],
378
                 self.request_msg.start_line, response_code)
379

    
380
    try:
381
      _HttpServerToClientMessageWriter(self.sock, self.request_msg,
382
                                       self.response_msg, self.WRITE_TIMEOUT)
383
    except http.HttpSocketTimeout:
384
      raise http.HttpError("Timeout while sending response")
385
    except socket.error, err:
386
      raise http.HttpError("Error sending response: %s" % err)
387

    
388
  def _SetErrorStatus(self, err):
389
    """Sets the response code and body from a HttpException.
390

391
    @type err: HttpException
392
    @param err: Exception instance
393

394
    """
395
    try:
396
      (shortmsg, longmsg) = self.responses[err.code]
397
    except KeyError:
398
      shortmsg = longmsg = "Unknown"
399

    
400
    if err.message:
401
      message = err.message
402
    else:
403
      message = shortmsg
404

    
405
    values = {
406
      "code": err.code,
407
      "message": cgi.escape(message),
408
      "explain": longmsg,
409
      }
410

    
411
    self.response_msg.start_line.code = err.code
412

    
413
    headers = {}
414
    if err.headers:
415
      headers.update(err.headers)
416
    headers[http.HTTP_CONTENT_TYPE] = self.error_content_type
417
    self.response_msg.headers = headers
418

    
419
    self.response_msg.body = self._FormatErrorMessage(values)
420

    
421
  def _FormatErrorMessage(self, values):
422
    """Formats the body of an error message.
423

424
    @type values: dict
425
    @param values: dictionary with keys code, message and explain.
426
    @rtype: string
427
    @return: the body of the message
428

429
    """
430
    return self.error_message_format % values
431

    
432
class HttpServer(http.HttpBase, asyncore.dispatcher):
433
  """Generic HTTP server class
434

435
  Users of this class must subclass it and override the HandleRequest function.
436

437
  """
438
  MAX_CHILDREN = 20
439

    
440
  def __init__(self, mainloop, local_address, port,
441
               ssl_params=None, ssl_verify_peer=False,
442
               request_executor_class=None):
443
    """Initializes the HTTP server
444

445
    @type mainloop: ganeti.daemon.Mainloop
446
    @param mainloop: Mainloop used to poll for I/O events
447
    @type local_address: string
448
    @param local_address: Local IP address to bind to
449
    @type port: int
450
    @param port: TCP port to listen on
451
    @type ssl_params: HttpSslParams
452
    @param ssl_params: SSL key and certificate
453
    @type ssl_verify_peer: bool
454
    @param ssl_verify_peer: Whether to require client certificate
455
        and compare it with our certificate
456
    @type request_executor_class: class
457
    @param request_executor_class: an class derived from the
458
        HttpServerRequestExecutor class
459

460
    """
461
    http.HttpBase.__init__(self)
462
    asyncore.dispatcher.__init__(self)
463

    
464
    if request_executor_class is None:
465
      self.request_executor = HttpServerRequestExecutor
466
    else:
467
      self.request_executor = request_executor_class
468

    
469
    self.mainloop = mainloop
470
    self.local_address = local_address
471
    self.port = port
472

    
473
    self.socket = self._CreateSocket(ssl_params, ssl_verify_peer)
474

    
475
    # Allow port to be reused
476
    self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
477

    
478
    self._children = []
479
    self.set_socket(self.socket)
480
    self.accepting = True
481
    mainloop.RegisterSignal(self)
482

    
483
  def Start(self):
484
    self.socket.bind((self.local_address, self.port))
485
    self.socket.listen(1024)
486

    
487
  def Stop(self):
488
    self.socket.close()
489

    
490
  def handle_accept(self):
491
    self._IncomingConnection()
492

    
493
  def OnSignal(self, signum):
494
    if signum == signal.SIGCHLD:
495
      self._CollectChildren(True)
496

    
497
  def _CollectChildren(self, quick):
498
    """Checks whether any child processes are done
499

500
    @type quick: bool
501
    @param quick: Whether to only use non-blocking functions
502

503
    """
504
    if not quick:
505
      # Don't wait for other processes if it should be a quick check
506
      while len(self._children) > self.MAX_CHILDREN:
507
        try:
508
          # Waiting without a timeout brings us into a potential DoS situation.
509
          # As soon as too many children run, we'll not respond to new
510
          # requests. The real solution would be to add a timeout for children
511
          # and killing them after some time.
512
          pid, _ = os.waitpid(0, 0)
513
        except os.error:
514
          pid = None
515
        if pid and pid in self._children:
516
          self._children.remove(pid)
517

    
518
    for child in self._children:
519
      try:
520
        pid, _ = os.waitpid(child, os.WNOHANG)
521
      except os.error:
522
        pid = None
523
      if pid and pid in self._children:
524
        self._children.remove(pid)
525

    
526
  def _IncomingConnection(self):
527
    """Called for each incoming connection
528

529
    """
530
    # pylint: disable-msg=W0212
531
    (connection, client_addr) = self.socket.accept()
532

    
533
    self._CollectChildren(False)
534

    
535
    pid = os.fork()
536
    if pid == 0:
537
      # Child process
538
      try:
539
        # The client shouldn't keep the listening socket open. If the parent
540
        # process is restarted, it would fail when there's already something
541
        # listening (in this case its own child from a previous run) on the
542
        # same port.
543
        try:
544
          self.socket.close()
545
        except socket.error:
546
          pass
547
        self.socket = None
548

    
549
        # In case the handler code uses temporary files
550
        utils.ResetTempfileModule()
551

    
552
        self.request_executor(self, connection, client_addr)
553
      except Exception: # pylint: disable-msg=W0703
554
        logging.exception("Error while handling request from %s:%s",
555
                          client_addr[0], client_addr[1])
556
        os._exit(1)
557
      os._exit(0)
558
    else:
559
      self._children.append(pid)
560

    
561
  def PreHandleRequest(self, req):
562
    """Called before handling a request.
563

564
    Can be overridden by a subclass.
565

566
    """
567

    
568
  def HandleRequest(self, req):
569
    """Handles a request.
570

571
    Must be overridden by subclass.
572

573
    """
574
    raise NotImplementedError()