Statistics
| Branch: | Tag: | Revision:

root / lib / http / server.py @ aea0ed67

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.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
      result = self.server.HandleRequest(handler_context)
312
    except (http.HttpException, KeyboardInterrupt, SystemExit):
313
      raise
314
    except Exception, err:
315
      logging.exception("Caught exception")
316
      raise http.HttpInternalServerError(message=str(err))
317
    except:
318
      logging.exception("Unknown exception")
319
      raise http.HttpInternalServerError(message="Unknown error")
320

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

    
328
  def _SendResponse(self):
329
    """Sends the response to the client.
330

331
    """
332
    if self.response_msg.start_line.code is None:
333
      return
334

    
335
    if not self.response_msg.headers:
336
      self.response_msg.headers = {}
337

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

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

    
353
    logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1],
354
                 self.request_msg.start_line, response_code)
355

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

    
364
  def _SetErrorStatus(self, err):
365
    """Sets the response code and body from a HttpException.
366

367
    @type err: HttpException
368
    @param err: Exception instance
369

370
    """
371
    try:
372
      (shortmsg, longmsg) = self.responses[err.code]
373
    except KeyError:
374
      shortmsg = longmsg = "Unknown"
375

    
376
    if err.message:
377
      message = err.message
378
    else:
379
      message = shortmsg
380

    
381
    values = {
382
      "code": err.code,
383
      "message": cgi.escape(message),
384
      "explain": longmsg,
385
      }
386

    
387
    self.response_msg.start_line.code = err.code
388

    
389
    headers = {}
390
    if err.headers:
391
      headers.update(err.headers)
392
    headers[http.HTTP_CONTENT_TYPE] = self.error_content_type
393
    self.response_msg.headers = headers
394

    
395
    self.response_msg.body = self.error_message_format % values
396

    
397

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

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

403
  """
404
  MAX_CHILDREN = 20
405

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

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

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

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

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

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

    
434
    self._children = []
435

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

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

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

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

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

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

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

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

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

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

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

    
489
    self._CollectChildren(False)
490

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

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

507
    Must be overriden by subclass.
508

509
    """
510
    raise NotImplementedError()