Statistics
| Branch: | Tag: | Revision:

root / lib / http / server.py @ 79589f25

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

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

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

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

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

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

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

    
121
    response_code = self._response_msg.start_line.code
122

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

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

    
143

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

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

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

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

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

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

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

    
166
    words = start_line.split()
167

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

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

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

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

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

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

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

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

    
205

    
206
class _HttpServerRequestExecutor(object):
207
  """Implements server side of HTTP.
208

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

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

    
220
  # Error message settings
221
  error_message_format = DEFAULT_ERROR_MESSAGE
222
  error_content_type = DEFAULT_ERROR_CONTENT_TYPE
223

    
224
  responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
225

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

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

234
    """
235
    self.server = server
236
    self.sock = sock
237
    self.client_addr = client_addr
238

    
239
    self.poller = select.poll()
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.info("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.poller, 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(self.poller, sock,
282
                                self.CLOSE_TIMEOUT, self.WRITE_TIMEOUT,
283
                                request_msg_reader, force_close)
284

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

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

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

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

    
305
    return request_msg_reader
306

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

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

    
313
    try:
314
      result = self.server.HandleRequest(handler_context)
315
    except (http.HttpException, KeyboardInterrupt, SystemExit):
316
      raise
317
    except Exception, err:
318
      logging.exception("Caught exception")
319
      raise http.HttpInternalServerError(message=str(err))
320
    except:
321
      logging.exception("Unknown exception")
322
      raise http.HttpInternalServerError(message="Unknown error")
323

    
324
    # TODO: Content-type
325
    encoder = http.HttpJsonConverter()
326
    self.response_msg.start_line.code = http.HTTP_OK
327
    self.response_msg.body = encoder.Encode(result)
328
    self.response_msg.headers = handler_context.resp_headers
329
    self.response_msg.headers[http.HTTP_CONTENT_TYPE] = encoder.CONTENT_TYPE
330

    
331
  def _SendResponse(self):
332
    """Sends the response to the client.
333

334
    """
335
    if self.response_msg.start_line.code is None:
336
      return
337

    
338
    if not self.response_msg.headers:
339
      self.response_msg.headers = {}
340

    
341
    self.response_msg.headers.update({
342
      # TODO: Keep-alive is not supported
343
      http.HTTP_CONNECTION: "close",
344
      http.HTTP_DATE: _DateTimeHeader(),
345
      http.HTTP_SERVER: http.HTTP_GANETI_VERSION,
346
      })
347

    
348
    # Get response reason based on code
349
    response_code = self.response_msg.start_line.code
350
    if response_code in self.responses:
351
      response_reason = self.responses[response_code][0]
352
    else:
353
      response_reason = ""
354
    self.response_msg.start_line.reason = response_reason
355

    
356
    logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1],
357
                 self.request_msg.start_line, response_code)
358

    
359
    try:
360
      _HttpServerToClientMessageWriter(self.sock, self.request_msg,
361
                                       self.response_msg, self.WRITE_TIMEOUT)
362
    except http.HttpSocketTimeout:
363
      raise http.HttpError("Timeout while sending response")
364
    except socket.error, err:
365
      raise http.HttpError("Error sending response: %s" % err)
366

    
367
  def _SetErrorStatus(self, err):
368
    """Sets the response code and body from a HttpException.
369

370
    @type err: HttpException
371
    @param err: Exception instance
372

373
    """
374
    try:
375
      (shortmsg, longmsg) = self.responses[err.code]
376
    except KeyError:
377
      shortmsg = longmsg = "Unknown"
378

    
379
    if err.message:
380
      message = err.message
381
    else:
382
      message = shortmsg
383

    
384
    values = {
385
      "code": err.code,
386
      "message": cgi.escape(message),
387
      "explain": longmsg,
388
      }
389

    
390
    self.response_msg.start_line.code = err.code
391

    
392
    headers = {}
393
    if err.headers:
394
      headers.update(err.headers)
395
    headers[http.HTTP_CONTENT_TYPE] = self.error_content_type
396
    self.response_msg.headers = headers
397

    
398
    self.response_msg.body = self.error_message_format % values
399

    
400

    
401
class HttpServer(http.HttpBase):
402
  """Generic HTTP server class
403

404
  Users of this class must subclass it and override the HandleRequest function.
405

406
  """
407
  MAX_CHILDREN = 20
408

    
409
  def __init__(self, mainloop, local_address, port,
410
               ssl_params=None, ssl_verify_peer=False):
411
    """Initializes the HTTP server
412

413
    @type mainloop: ganeti.daemon.Mainloop
414
    @param mainloop: Mainloop used to poll for I/O events
415
    @type local_address: string
416
    @param local_address: Local IP address to bind to
417
    @type port: int
418
    @param port: TCP port to listen on
419
    @type ssl_params: HttpSslParams
420
    @param ssl_params: SSL key and certificate
421
    @type ssl_verify_peer: bool
422
    @param ssl_verify_peer: Whether to require client certificate and compare
423
                            it with our certificate
424

425
    """
426
    http.HttpBase.__init__(self)
427

    
428
    self.mainloop = mainloop
429
    self.local_address = local_address
430
    self.port = port
431

    
432
    self.socket = self._CreateSocket(ssl_params, ssl_verify_peer)
433

    
434
    # Allow port to be reused
435
    self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
436

    
437
    self._children = []
438

    
439
    mainloop.RegisterIO(self, self.socket.fileno(), select.POLLIN)
440
    mainloop.RegisterSignal(self)
441

    
442
  def Start(self):
443
    self.socket.bind((self.local_address, self.port))
444
    self.socket.listen(1024)
445

    
446
  def Stop(self):
447
    self.socket.close()
448

    
449
  def OnIO(self, fd, condition):
450
    if condition & select.POLLIN:
451
      self._IncomingConnection()
452

    
453
  def OnSignal(self, signum):
454
    if signum == signal.SIGCHLD:
455
      self._CollectChildren(True)
456

    
457
  def _CollectChildren(self, quick):
458
    """Checks whether any child processes are done
459

460
    @type quick: bool
461
    @param quick: Whether to only use non-blocking functions
462

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

    
478
    for child in self._children:
479
      try:
480
        pid, status = os.waitpid(child, os.WNOHANG)
481
      except os.error:
482
        pid = None
483
      if pid and pid in self._children:
484
        self._children.remove(pid)
485

    
486
  def _IncomingConnection(self):
487
    """Called for each incoming connection
488

489
    """
490
    (connection, client_addr) = self.socket.accept()
491

    
492
    self._CollectChildren(False)
493

    
494
    pid = os.fork()
495
    if pid == 0:
496
      # Child process
497
      try:
498
        _HttpServerRequestExecutor(self, connection, client_addr)
499
      except Exception:
500
        logging.exception("Error while handling request from %s:%s",
501
                          client_addr[0], client_addr[1])
502
        os._exit(1)
503
      os._exit(0)
504
    else:
505
      self._children.append(pid)
506

    
507
  def HandleRequest(self, req):
508
    """Handles a request.
509

510
    Must be overriden by subclass.
511

512
    """
513
    raise NotImplementedError()