Statistics
| Branch: | Tag: | Revision:

root / lib / http / server.py @ ab221ddf

History | View | Annotate | Download (16.9 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

    
36

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

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

    
58

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

62
  The time MUST be in the GMT timezone.
63

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

    
71

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

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

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

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

    
90

    
91
class _HttpServerToClientMessageWriter(http.HttpMessageWriter):
92
  """Writes an HTTP response to client.
93

94
  """
95
  def __init__(self, sock, request_msg, response_msg, write_timeout):
96
    """Writes the response to the client.
97

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

108
    """
109
    self._request_msg = request_msg
110
    self._response_msg = response_msg
111
    http.HttpMessageWriter.__init__(self, sock, response_msg, write_timeout)
112

    
113
  def HasMessageBody(self):
114
    """Logic to detect whether response should contain a message body.
115

116
    """
117
    if self._request_msg.start_line:
118
      request_method = self._request_msg.start_line.method
119
    else:
120
      request_method = None
121

    
122
    response_code = self._response_msg.start_line.code
123

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

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

    
144

    
145
class _HttpClientToServerMessageReader(http.HttpMessageReader):
146
  """Reads an HTTP request sent by client.
147

148
  """
149
  # Length limits
150
  START_LINE_LENGTH_MAX = 4096
151
  HEADER_LENGTH_MAX = 4096
152

    
153
  def ParseStartLine(self, start_line):
154
    """Parses the start line sent by client.
155

156
    Example: "GET /index.html HTTP/1.1"
157

158
    @type start_line: string
159
    @param start_line: Start line
160

161
    """
162
    # Empty lines are skipped when reading
163
    assert start_line
164

    
165
    logging.debug("HTTP request: %s", start_line)
166

    
167
    words = start_line.split()
168

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

    
174
      try:
175
        base_version_number = version.split("/", 1)[1]
176
        version_number = base_version_number.split(".")
177

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

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

    
191
      if version_number >= (2, 0):
192
        raise http.HttpVersionNotSupported("Invalid HTTP Version (%s)" %
193
                                      base_version_number)
194

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

    
201
    else:
202
      raise http.HttpBadRequest("Bad request syntax (%r)" % start_line)
203

    
204
    return http.HttpClientToServerStartLine(method, path, version)
205

    
206

    
207
class HttpServerRequestExecutor(object):
208
  """Implements server side of HTTP.
209

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

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

    
222
  # Error message settings
223
  error_message_format = DEFAULT_ERROR_MESSAGE
224
  error_content_type = DEFAULT_ERROR_CONTENT_TYPE
225

    
226
  responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
227

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

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

236
    """
237
    self.server = server
238
    self.sock = sock
239
    self.client_addr = client_addr
240

    
241
    self.request_msg = http.HttpMessage()
242
    self.response_msg = http.HttpMessage()
243

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

    
248
    # Disable Python's timeout
249
    self.sock.settimeout(None)
250

    
251
    # Operate in non-blocking mode
252
    self.sock.setblocking(0)
253

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

    
268
        try:
269
          try:
270
            request_msg_reader = self._ReadRequest()
271

    
272
            # RFC2616, 14.23: All Internet-based HTTP/1.1 servers MUST respond
273
            # with a 400 (Bad Request) status code to any HTTP/1.1 request
274
            # message which lacks a Host header field.
275
            if (self.request_msg.start_line.version == http.HTTP_1_1 and
276
                http.HTTP_HOST not in self.request_msg.headers):
277
              raise http.HttpBadRequest(message="Missing Host header")
278

    
279
            self._HandleRequest()
280

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

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

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

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

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

    
312
    return request_msg_reader
313

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

317
    """
318
    handler_context = _HttpServerRequest(self.request_msg)
319

    
320
    try:
321
      try:
322
        # Authentication, etc.
323
        self.server.PreHandleRequest(handler_context)
324

    
325
        # Call actual request handler
326
        result = self.server.HandleRequest(handler_context)
327
      except (http.HttpException, KeyboardInterrupt, SystemExit):
328
        raise
329
      except Exception, err:
330
        logging.exception("Caught exception")
331
        raise http.HttpInternalServerError(message=str(err))
332
      except:
333
        logging.exception("Unknown exception")
334
        raise http.HttpInternalServerError(message="Unknown error")
335

    
336
      if not isinstance(result, basestring):
337
        raise http.HttpError("Handler function didn't return string type")
338

    
339
      self.response_msg.start_line.code = http.HTTP_OK
340
      self.response_msg.headers = handler_context.resp_headers
341
      self.response_msg.body = result
342
    finally:
343
      # No reason to keep this any longer, even for exceptions
344
      handler_context.private = None
345

    
346
  def _SendResponse(self):
347
    """Sends the response to the client.
348

349
    """
350
    if self.response_msg.start_line.code is None:
351
      return
352

    
353
    if not self.response_msg.headers:
354
      self.response_msg.headers = {}
355

    
356
    self.response_msg.headers.update({
357
      # TODO: Keep-alive is not supported
358
      http.HTTP_CONNECTION: "close",
359
      http.HTTP_DATE: _DateTimeHeader(),
360
      http.HTTP_SERVER: http.HTTP_GANETI_VERSION,
361
      })
362

    
363
    # Get response reason based on code
364
    response_code = self.response_msg.start_line.code
365
    if response_code in self.responses:
366
      response_reason = self.responses[response_code][0]
367
    else:
368
      response_reason = ""
369
    self.response_msg.start_line.reason = response_reason
370

    
371
    logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1],
372
                 self.request_msg.start_line, response_code)
373

    
374
    try:
375
      _HttpServerToClientMessageWriter(self.sock, self.request_msg,
376
                                       self.response_msg, self.WRITE_TIMEOUT)
377
    except http.HttpSocketTimeout:
378
      raise http.HttpError("Timeout while sending response")
379
    except socket.error, err:
380
      raise http.HttpError("Error sending response: %s" % err)
381

    
382
  def _SetErrorStatus(self, err):
383
    """Sets the response code and body from a HttpException.
384

385
    @type err: HttpException
386
    @param err: Exception instance
387

388
    """
389
    try:
390
      (shortmsg, longmsg) = self.responses[err.code]
391
    except KeyError:
392
      shortmsg = longmsg = "Unknown"
393

    
394
    if err.message:
395
      message = err.message
396
    else:
397
      message = shortmsg
398

    
399
    values = {
400
      "code": err.code,
401
      "message": cgi.escape(message),
402
      "explain": longmsg,
403
      }
404

    
405
    self.response_msg.start_line.code = err.code
406

    
407
    headers = {}
408
    if err.headers:
409
      headers.update(err.headers)
410
    headers[http.HTTP_CONTENT_TYPE] = self.error_content_type
411
    self.response_msg.headers = headers
412

    
413
    self.response_msg.body = self._FormatErrorMessage(values)
414

    
415
  def _FormatErrorMessage(self, values):
416
    """Formats the body of an error message.
417

418
    @type values: dict
419
    @param values: dictionary with keys code, message and explain.
420
    @rtype: string
421
    @return: the body of the message
422

423
    """
424
    return self.error_message_format % values
425

    
426

    
427
class HttpServer(http.HttpBase, asyncore.dispatcher):
428
  """Generic HTTP server class
429

430
  Users of this class must subclass it and override the HandleRequest function.
431

432
  """
433
  MAX_CHILDREN = 20
434

    
435
  def __init__(self, mainloop, local_address, port,
436
               ssl_params=None, ssl_verify_peer=False,
437
               request_executor_class=None):
438
    """Initializes the HTTP server
439

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

455
    """
456
    http.HttpBase.__init__(self)
457
    asyncore.dispatcher.__init__(self)
458

    
459
    if request_executor_class is None:
460
      self.request_executor = HttpServerRequestExecutor
461
    else:
462
      self.request_executor = request_executor_class
463

    
464
    self.mainloop = mainloop
465
    self.local_address = local_address
466
    self.port = port
467

    
468
    self.socket = self._CreateSocket(ssl_params, ssl_verify_peer)
469

    
470
    # Allow port to be reused
471
    self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
472

    
473
    self._children = []
474
    self.set_socket(self.socket)
475
    self.accepting = True
476
    mainloop.RegisterSignal(self)
477

    
478
  def Start(self):
479
    self.socket.bind((self.local_address, self.port))
480
    self.socket.listen(1024)
481

    
482
  def Stop(self):
483
    self.socket.close()
484

    
485
  def handle_accept(self):
486
    self._IncomingConnection()
487

    
488
  def OnSignal(self, signum):
489
    if signum == signal.SIGCHLD:
490
      self._CollectChildren(True)
491

    
492
  def _CollectChildren(self, quick):
493
    """Checks whether any child processes are done
494

495
    @type quick: bool
496
    @param quick: Whether to only use non-blocking functions
497

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

    
513
    for child in self._children:
514
      try:
515
        pid, _ = os.waitpid(child, os.WNOHANG)
516
      except os.error:
517
        pid = None
518
      if pid and pid in self._children:
519
        self._children.remove(pid)
520

    
521
  def _IncomingConnection(self):
522
    """Called for each incoming connection
523

524
    """
525
    # pylint: disable-msg=W0212
526
    (connection, client_addr) = self.socket.accept()
527

    
528
    self._CollectChildren(False)
529

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

    
544
        self.request_executor(self, connection, client_addr)
545
      except Exception: # pylint: disable-msg=W0703
546
        logging.exception("Error while handling request from %s:%s",
547
                          client_addr[0], client_addr[1])
548
        os._exit(1)
549
      os._exit(0)
550
    else:
551
      self._children.append(pid)
552

    
553
  def PreHandleRequest(self, req):
554
    """Called before handling a request.
555

556
    Can be overridden by a subclass.
557

558
    """
559

    
560
  def HandleRequest(self, req):
561
    """Handles a request.
562

563
    Must be overridden by subclass.
564

565
    """
566
    raise NotImplementedError()