Statistics
| Branch: | Tag: | Revision:

root / lib / http / server.py @ c41eea6e

History | View | Annotate | Download (14.7 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():
63
  """Return the current date and time formatted for a message header.
64

65
  """
66
  (year, month, day, hh, mm, ss, wd, _, _) = time.gmtime()
67
  return ("%s, %02d %3s %4d %02d:%02d:%02d GMT" %
68
          (WEEKDAYNAME[wd], day, MONTHNAME[month], year, hh, mm, ss))
69

    
70

    
71
class _HttpServerRequest(object):
72
  """Data structure for HTTP request on server side.
73

74
  """
75
  def __init__(self, request_msg):
76
    # Request attributes
77
    self.request_method = request_msg.start_line.method
78
    self.request_path = request_msg.start_line.path
79
    self.request_headers = request_msg.headers
80
    self.request_body = request_msg.decoded_body
81

    
82
    # Response attributes
83
    self.resp_headers = {}
84

    
85

    
86
class _HttpServerToClientMessageWriter(http.HttpMessageWriter):
87
  """Writes an HTTP response to client.
88

89
  """
90
  def __init__(self, sock, request_msg, response_msg, write_timeout):
91
    """Writes the response to the client.
92

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

103
    """
104
    self._request_msg = request_msg
105
    self._response_msg = response_msg
106
    http.HttpMessageWriter.__init__(self, sock, response_msg, write_timeout)
107

    
108
  def HasMessageBody(self):
109
    """Logic to detect whether response should contain a message body.
110

111
    """
112
    if self._request_msg.start_line:
113
      request_method = self._request_msg.start_line.method
114
    else:
115
      request_method = None
116

    
117
    response_code = self._response_msg.start_line.code
118

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

    
132
    return (http.HttpMessageWriter.HasMessageBody(self) and
133
            (request_method is not None and request_method != http.HTTP_HEAD) and
134
            response_code >= http.HTTP_OK and
135
            response_code not in (http.HTTP_NO_CONTENT, http.HTTP_NOT_MODIFIED))
136

    
137

    
138
class _HttpClientToServerMessageReader(http.HttpMessageReader):
139
  """Reads an HTTP request sent by client.
140

141
  """
142
  # Length limits
143
  START_LINE_LENGTH_MAX = 4096
144
  HEADER_LENGTH_MAX = 4096
145

    
146
  def ParseStartLine(self, start_line):
147
    """Parses the start line sent by client.
148

149
    Example: "GET /index.html HTTP/1.1"
150

151
    @type start_line: string
152
    @param start_line: Start line
153

154
    """
155
    # Empty lines are skipped when reading
156
    assert start_line
157

    
158
    logging.debug("HTTP request: %s", start_line)
159

    
160
    words = start_line.split()
161

    
162
    if len(words) == 3:
163
      [method, path, version] = words
164
      if version[:5] != 'HTTP/':
165
        raise http.HttpBadRequest("Bad request version (%r)" % version)
166

    
167
      try:
168
        base_version_number = version.split("/", 1)[1]
169
        version_number = base_version_number.split(".")
170

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

    
180
        version_number = (int(version_number[0]), int(version_number[1]))
181
      except (ValueError, IndexError):
182
        raise http.HttpBadRequest("Bad request version (%r)" % version)
183

    
184
      if version_number >= (2, 0):
185
        raise http.HttpVersionNotSupported("Invalid HTTP Version (%s)" %
186
                                      base_version_number)
187

    
188
    elif len(words) == 2:
189
      version = http.HTTP_0_9
190
      [method, path] = words
191
      if method != http.HTTP_GET:
192
        raise http.HttpBadRequest("Bad HTTP/0.9 request type (%r)" % method)
193

    
194
    else:
195
      raise http.HttpBadRequest("Bad request syntax (%r)" % start_line)
196

    
197
    return http.HttpClientToServerStartLine(method, path, version)
198

    
199

    
200
class _HttpServerRequestExecutor(object):
201
  """Implements server side of HTTP.
202

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

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

    
214
  # Error message settings
215
  error_message_format = DEFAULT_ERROR_MESSAGE
216
  error_content_type = DEFAULT_ERROR_CONTENT_TYPE
217

    
218
  responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
219

    
220
  # Timeouts in seconds for socket layer
221
  WRITE_TIMEOUT = 10
222
  READ_TIMEOUT = 10
223
  CLOSE_TIMEOUT = 1
224

    
225
  def __init__(self, server, sock, client_addr):
226
    """Initializes this class.
227

228
    """
229
    self.server = server
230
    self.sock = sock
231
    self.client_addr = client_addr
232

    
233
    self.poller = select.poll()
234

    
235
    self.request_msg = http.HttpMessage()
236
    self.response_msg = http.HttpMessage()
237

    
238
    self.response_msg.start_line = \
239
      http.HttpServerToClientStartLine(version=self.default_request_version,
240
                                       code=None, reason=None)
241

    
242
    # Disable Python's timeout
243
    self.sock.settimeout(None)
244

    
245
    # Operate in non-blocking mode
246
    self.sock.setblocking(0)
247

    
248
    logging.info("Connection from %s:%s", client_addr[0], client_addr[1])
249
    try:
250
      request_msg_reader = None
251
      force_close = True
252
      try:
253
        try:
254
          try:
255
            request_msg_reader = self._ReadRequest()
256
            self._HandleRequest()
257

    
258
            # Only wait for client to close if we didn't have any exception.
259
            force_close = False
260
          except http.HttpException, err:
261
            self._SetErrorStatus(err)
262
        finally:
263
          # Try to send a response
264
          self._SendResponse()
265
      finally:
266
        http.ShutdownConnection(self.poller, sock,
267
                                self.CLOSE_TIMEOUT, self.WRITE_TIMEOUT,
268
                                request_msg_reader, force_close)
269

    
270
      self.sock.close()
271
      self.sock = None
272
    finally:
273
      logging.info("Disconnected %s:%s", client_addr[0], client_addr[1])
274

    
275
  def _ReadRequest(self):
276
    """Reads a request sent by client.
277

278
    """
279
    try:
280
      request_msg_reader = \
281
        _HttpClientToServerMessageReader(self.sock, self.request_msg,
282
                                         self.READ_TIMEOUT)
283
    except http.HttpSocketTimeout:
284
      raise http.HttpError("Timeout while reading request")
285
    except socket.error, err:
286
      raise http.HttpError("Error reading request: %s" % err)
287

    
288
    self.response_msg.start_line.version = self.request_msg.start_line.version
289

    
290
    return request_msg_reader
291

    
292
  def _HandleRequest(self):
293
    """Calls the handler function for the current request.
294

295
    """
296
    handler_context = _HttpServerRequest(self.request_msg)
297

    
298
    try:
299
      result = self.server.HandleRequest(handler_context)
300
    except (http.HttpException, KeyboardInterrupt, SystemExit):
301
      raise
302
    except Exception, err:
303
      logging.exception("Caught exception")
304
      raise http.HttpInternalError(message=str(err))
305
    except:
306
      logging.exception("Unknown exception")
307
      raise http.HttpInternalError(message="Unknown error")
308

    
309
    # TODO: Content-type
310
    encoder = http.HttpJsonConverter()
311
    self.response_msg.start_line.code = http.HTTP_OK
312
    self.response_msg.body = encoder.Encode(result)
313
    self.response_msg.headers = handler_context.resp_headers
314
    self.response_msg.headers[http.HTTP_CONTENT_TYPE] = encoder.CONTENT_TYPE
315

    
316
  def _SendResponse(self):
317
    """Sends the response to the client.
318

319
    """
320
    if self.response_msg.start_line.code is None:
321
      return
322

    
323
    if not self.response_msg.headers:
324
      self.response_msg.headers = {}
325

    
326
    self.response_msg.headers.update({
327
      # TODO: Keep-alive is not supported
328
      http.HTTP_CONNECTION: "close",
329
      http.HTTP_DATE: _DateTimeHeader(),
330
      http.HTTP_SERVER: http.HTTP_GANETI_VERSION,
331
      })
332

    
333
    # Get response reason based on code
334
    response_code = self.response_msg.start_line.code
335
    if response_code in self.responses:
336
      response_reason = self.responses[response_code][0]
337
    else:
338
      response_reason = ""
339
    self.response_msg.start_line.reason = response_reason
340

    
341
    logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1],
342
                 self.request_msg.start_line, response_code)
343

    
344
    try:
345
      _HttpServerToClientMessageWriter(self.sock, self.request_msg,
346
                                       self.response_msg, self.WRITE_TIMEOUT)
347
    except http.HttpSocketTimeout:
348
      raise http.HttpError("Timeout while sending response")
349
    except socket.error, err:
350
      raise http.HttpError("Error sending response: %s" % err)
351

    
352
  def _SetErrorStatus(self, err):
353
    """Sets the response code and body from a HttpException.
354

355
    @type err: HttpException
356
    @param err: Exception instance
357

358
    """
359
    try:
360
      (shortmsg, longmsg) = self.responses[err.code]
361
    except KeyError:
362
      shortmsg = longmsg = "Unknown"
363

    
364
    if err.message:
365
      message = err.message
366
    else:
367
      message = shortmsg
368

    
369
    values = {
370
      "code": err.code,
371
      "message": cgi.escape(message),
372
      "explain": longmsg,
373
      }
374

    
375
    self.response_msg.start_line.code = err.code
376
    self.response_msg.headers = {
377
      http.HTTP_CONTENT_TYPE: self.error_content_type,
378
      }
379
    self.response_msg.body = self.error_message_format % values
380

    
381

    
382
class HttpServer(http.HttpSocketBase):
383
  """Generic HTTP server class
384

385
  Users of this class must subclass it and override the HandleRequest function.
386

387
  """
388
  MAX_CHILDREN = 20
389

    
390
  def __init__(self, mainloop, local_address, port,
391
               ssl_params=None, ssl_verify_peer=False):
392
    """Initializes the HTTP server
393

394
    @type mainloop: ganeti.daemon.Mainloop
395
    @param mainloop: Mainloop used to poll for I/O events
396
    @type local_address: string
397
    @param local_address: Local IP address to bind to
398
    @type port: int
399
    @param port: TCP port to listen on
400
    @type ssl_params: HttpSslParams
401
    @param ssl_params: SSL key and certificate
402
    @type ssl_verify_peer: bool
403
    @param ssl_verify_peer: Whether to require client certificate and compare
404
                            it with our certificate
405

406
    """
407
    http.HttpSocketBase.__init__(self)
408

    
409
    self.mainloop = mainloop
410
    self.local_address = local_address
411
    self.port = port
412

    
413
    self.socket = self._CreateSocket(ssl_params, ssl_verify_peer)
414

    
415
    # Allow port to be reused
416
    self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
417

    
418
    self._children = []
419

    
420
    mainloop.RegisterIO(self, self.socket.fileno(), select.POLLIN)
421
    mainloop.RegisterSignal(self)
422

    
423
  def Start(self):
424
    self.socket.bind((self.local_address, self.port))
425
    self.socket.listen(1024)
426

    
427
  def Stop(self):
428
    self.socket.close()
429

    
430
  def OnIO(self, fd, condition):
431
    if condition & select.POLLIN:
432
      self._IncomingConnection()
433

    
434
  def OnSignal(self, signum):
435
    if signum == signal.SIGCHLD:
436
      self._CollectChildren(True)
437

    
438
  def _CollectChildren(self, quick):
439
    """Checks whether any child processes are done
440

441
    @type quick: bool
442
    @param quick: Whether to only use non-blocking functions
443

444
    """
445
    if not quick:
446
      # Don't wait for other processes if it should be a quick check
447
      while len(self._children) > self.MAX_CHILDREN:
448
        try:
449
          # Waiting without a timeout brings us into a potential DoS situation.
450
          # As soon as too many children run, we'll not respond to new
451
          # requests. The real solution would be to add a timeout for children
452
          # and killing them after some time.
453
          pid, status = os.waitpid(0, 0)
454
        except os.error:
455
          pid = None
456
        if pid and pid in self._children:
457
          self._children.remove(pid)
458

    
459
    for child in self._children:
460
      try:
461
        pid, status = os.waitpid(child, os.WNOHANG)
462
      except os.error:
463
        pid = None
464
      if pid and pid in self._children:
465
        self._children.remove(pid)
466

    
467
  def _IncomingConnection(self):
468
    """Called for each incoming connection
469

470
    """
471
    (connection, client_addr) = self.socket.accept()
472

    
473
    self._CollectChildren(False)
474

    
475
    pid = os.fork()
476
    if pid == 0:
477
      # Child process
478
      try:
479
        _HttpServerRequestExecutor(self, connection, client_addr)
480
      except Exception:
481
        logging.exception("Error while handling request from %s:%s",
482
                          client_addr[0], client_addr[1])
483
        os._exit(1)
484
      os._exit(0)
485
    else:
486
      self._children.append(pid)
487

    
488
  def HandleRequest(self, req):
489
    """Handles a request.
490

491
    Must be overriden by subclass.
492

493
    """
494
    raise NotImplementedError()