Statistics
| Branch: | Tag: | Revision:

root / lib / http / server.py @ 1f8588f6

History | View | Annotate | Download (16.2 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
214
  Python's BaseHTTPServer, from both version 2.4 and 3k. It does not
215
  support non-ASCII character encodings. Keep-alive connections are
216
  not supported.
217

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

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

    
229
  responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
230

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
307
    return request_msg_reader
308

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
408
    self.response_msg.body = self._FormatErrorMessage(values)
409

    
410
  def _FormatErrorMessage(self, values):
411
    """Formats the body of an error message.
412

413
    @type values: dict
414
    @param values: dictionary with keys code, message and explain.
415
    @rtype: string
416
    @return: the body of the message
417

418
    """
419
    return self.error_message_format % values
420

    
421
class HttpServer(http.HttpBase):
422
  """Generic HTTP server class
423

424
  Users of this class must subclass it and override the HandleRequest function.
425

426
  """
427
  MAX_CHILDREN = 20
428

    
429
  def __init__(self, mainloop, local_address, port,
430
               ssl_params=None, ssl_verify_peer=False,
431
               request_executor_class=None):
432
    """Initializes the HTTP server
433

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

449
    """
450
    http.HttpBase.__init__(self)
451

    
452
    if request_executor_class is None:
453
      self.request_executor = HttpServerRequestExecutor
454
    else:
455
      self.request_executor = request_executor_class
456

    
457
    self.mainloop = mainloop
458
    self.local_address = local_address
459
    self.port = port
460

    
461
    self.socket = self._CreateSocket(ssl_params, ssl_verify_peer)
462

    
463
    # Allow port to be reused
464
    self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
465

    
466
    self._children = []
467

    
468
    mainloop.RegisterIO(self, self.socket.fileno(), select.POLLIN)
469
    mainloop.RegisterSignal(self)
470

    
471
  def Start(self):
472
    self.socket.bind((self.local_address, self.port))
473
    self.socket.listen(1024)
474

    
475
  def Stop(self):
476
    self.socket.close()
477

    
478
  def OnIO(self, fd, condition):
479
    if condition & select.POLLIN:
480
      self._IncomingConnection()
481

    
482
  def OnSignal(self, signum):
483
    if signum == signal.SIGCHLD:
484
      self._CollectChildren(True)
485

    
486
  def _CollectChildren(self, quick):
487
    """Checks whether any child processes are done
488

489
    @type quick: bool
490
    @param quick: Whether to only use non-blocking functions
491

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

    
507
    for child in self._children:
508
      try:
509
        pid, status = os.waitpid(child, os.WNOHANG)
510
      except os.error:
511
        pid = None
512
      if pid and pid in self._children:
513
        self._children.remove(pid)
514

    
515
  def _IncomingConnection(self):
516
    """Called for each incoming connection
517

518
    """
519
    (connection, client_addr) = self.socket.accept()
520

    
521
    self._CollectChildren(False)
522

    
523
    pid = os.fork()
524
    if pid == 0:
525
      # Child process
526
      try:
527
        self.request_executor(self, connection, client_addr)
528
      except Exception:
529
        logging.exception("Error while handling request from %s:%s",
530
                          client_addr[0], client_addr[1])
531
        os._exit(1)
532
      os._exit(0)
533
    else:
534
      self._children.append(pid)
535

    
536
  def PreHandleRequest(self, req):
537
    """Called before handling a request.
538

539
    Can be overriden by a subclass.
540

541
    """
542

    
543
  def HandleRequest(self, req):
544
    """Handles a request.
545

546
    Must be overriden by subclass.
547

548
    """
549
    raise NotImplementedError()