Statistics
| Branch: | Tag: | Revision:

root / lib / http / server.py @ 7c4d6c7b

History | View | Annotate | Download (16.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 select
30
import socket
31
import time
32
import signal
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.decoded_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
            self._HandleRequest()
272

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

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

    
289
  def _ReadRequest(self):
290
    """Reads a request sent by client.
291

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

    
302
    self.response_msg.start_line.version = self.request_msg.start_line.version
303

    
304
    return request_msg_reader
305

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

309
    """
310
    handler_context = _HttpServerRequest(self.request_msg)
311

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

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

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

    
338
  def _SendResponse(self):
339
    """Sends the response to the client.
340

341
    """
342
    if self.response_msg.start_line.code is None:
343
      return
344

    
345
    if not self.response_msg.headers:
346
      self.response_msg.headers = {}
347

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

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

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

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

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

377
    @type err: HttpException
378
    @param err: Exception instance
379

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

    
386
    if err.message:
387
      message = err.message
388
    else:
389
      message = shortmsg
390

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

    
397
    self.response_msg.start_line.code = err.code
398

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

    
405
    self.response_msg.body = self._FormatErrorMessage(values)
406

    
407
  def _FormatErrorMessage(self, values):
408
    """Formats the body of an error message.
409

410
    @type values: dict
411
    @param values: dictionary with keys code, message and explain.
412
    @rtype: string
413
    @return: the body of the message
414

415
    """
416
    return self.error_message_format % values
417

    
418
class HttpServer(http.HttpBase):
419
  """Generic HTTP server class
420

421
  Users of this class must subclass it and override the HandleRequest function.
422

423
  """
424
  MAX_CHILDREN = 20
425

    
426
  def __init__(self, mainloop, local_address, port,
427
               ssl_params=None, ssl_verify_peer=False,
428
               request_executor_class=None):
429
    """Initializes the HTTP server
430

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

446
    """
447
    http.HttpBase.__init__(self)
448

    
449
    if request_executor_class is None:
450
      self.request_executor = HttpServerRequestExecutor
451
    else:
452
      self.request_executor = request_executor_class
453

    
454
    self.mainloop = mainloop
455
    self.local_address = local_address
456
    self.port = port
457

    
458
    self.socket = self._CreateSocket(ssl_params, ssl_verify_peer)
459

    
460
    # Allow port to be reused
461
    self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
462

    
463
    self._children = []
464

    
465
    mainloop.RegisterIO(self, self.socket.fileno(), select.POLLIN)
466
    mainloop.RegisterSignal(self)
467

    
468
  def Start(self):
469
    self.socket.bind((self.local_address, self.port))
470
    self.socket.listen(1024)
471

    
472
  def Stop(self):
473
    self.socket.close()
474

    
475
  def OnIO(self, fd, condition):
476
    if condition & select.POLLIN:
477
      self._IncomingConnection()
478

    
479
  def OnSignal(self, signum):
480
    if signum == signal.SIGCHLD:
481
      self._CollectChildren(True)
482

    
483
  def _CollectChildren(self, quick):
484
    """Checks whether any child processes are done
485

486
    @type quick: bool
487
    @param quick: Whether to only use non-blocking functions
488

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

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

    
512
  def _IncomingConnection(self):
513
    """Called for each incoming connection
514

515
    """
516
    (connection, client_addr) = self.socket.accept()
517

    
518
    self._CollectChildren(False)
519

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

    
533
  def PreHandleRequest(self, req):
534
    """Called before handling a request.
535

536
    Can be overridden by a subclass.
537

538
    """
539

    
540
  def HandleRequest(self, req):
541
    """Handles a request.
542

543
    Must be overridden by subclass.
544

545
    """
546
    raise NotImplementedError()