Statistics
| Branch: | Tag: | Revision:

root / lib / http / server.py @ f30ca1e6

History | View | Annotate | Download (15.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 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.HttpInternalError(message=str(err))
320
    except:
321
      logging.exception("Unknown exception")
322
      raise http.HttpInternalError(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
    self.response_msg.headers = {
392
      http.HTTP_CONTENT_TYPE: self.error_content_type,
393
      }
394
    self.response_msg.body = self.error_message_format % values
395

    
396

    
397
class HttpServer(http.HttpBase):
398
  """Generic HTTP server class
399

400
  Users of this class must subclass it and override the HandleRequest function.
401

402
  """
403
  MAX_CHILDREN = 20
404

    
405
  def __init__(self, mainloop, local_address, port,
406
               ssl_params=None, ssl_verify_peer=False):
407
    """Initializes the HTTP server
408

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

421
    """
422
    http.HttpBase.__init__(self)
423

    
424
    self.mainloop = mainloop
425
    self.local_address = local_address
426
    self.port = port
427

    
428
    self.socket = self._CreateSocket(ssl_params, ssl_verify_peer)
429

    
430
    # Allow port to be reused
431
    self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
432

    
433
    self._children = []
434

    
435
    mainloop.RegisterIO(self, self.socket.fileno(), select.POLLIN)
436
    mainloop.RegisterSignal(self)
437

    
438
  def Start(self):
439
    self.socket.bind((self.local_address, self.port))
440
    self.socket.listen(1024)
441

    
442
  def Stop(self):
443
    self.socket.close()
444

    
445
  def OnIO(self, fd, condition):
446
    if condition & select.POLLIN:
447
      self._IncomingConnection()
448

    
449
  def OnSignal(self, signum):
450
    if signum == signal.SIGCHLD:
451
      self._CollectChildren(True)
452

    
453
  def _CollectChildren(self, quick):
454
    """Checks whether any child processes are done
455

456
    @type quick: bool
457
    @param quick: Whether to only use non-blocking functions
458

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

    
474
    for child in self._children:
475
      try:
476
        pid, status = os.waitpid(child, os.WNOHANG)
477
      except os.error:
478
        pid = None
479
      if pid and pid in self._children:
480
        self._children.remove(pid)
481

    
482
  def _IncomingConnection(self):
483
    """Called for each incoming connection
484

485
    """
486
    (connection, client_addr) = self.socket.accept()
487

    
488
    self._CollectChildren(False)
489

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

    
503
  def HandleRequest(self, req):
504
    """Handles a request.
505

506
    Must be overriden by subclass.
507

508
    """
509
    raise NotImplementedError()