Statistics
| Branch: | Tag: | Revision:

root / lib / http / server.py @ f8bd7df3

History | View | Annotate | Download (15.3 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.request_msg = http.HttpMessage()
240
    self.response_msg = http.HttpMessage()
241

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

    
246
    # Disable Python's timeout
247
    self.sock.settimeout(None)
248

    
249
    # Operate in non-blocking mode
250
    self.sock.setblocking(0)
251

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

    
266
        try:
267
          try:
268
            request_msg_reader = self._ReadRequest()
269
            self._HandleRequest()
270

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

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

    
287
  def _ReadRequest(self):
288
    """Reads a request sent by client.
289

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

    
300
    self.response_msg.start_line.version = self.request_msg.start_line.version
301

    
302
    return request_msg_reader
303

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

307
    """
308
    handler_context = _HttpServerRequest(self.request_msg)
309

    
310
    try:
311
      # Authentication, etc.
312
      self.server.PreHandleRequest(handler_context)
313

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
401

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

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

407
  """
408
  MAX_CHILDREN = 20
409

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

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

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

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

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

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

    
438
    self._children = []
439

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

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

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

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

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

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

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

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

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

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

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

    
493
    self._CollectChildren(False)
494

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

    
508
  def PreHandleRequest(self, req):
509
    """Called before handling a request.
510

511
    Can be overriden by a subclass.
512

513
    """
514

    
515
  def HandleRequest(self, req):
516
    """Handles a request.
517

518
    Must be overriden by subclass.
519

520
    """
521
    raise NotImplementedError()