Statistics
| Branch: | Tag: | Revision:

root / lib / http / server.py @ 57fd6d0b

History | View | Annotate | Download (16.5 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
import asyncore
34

    
35
from ganeti import http
36

    
37

    
38
WEEKDAYNAME = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
39
MONTHNAME = [None,
40
             'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
41
             'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
42

    
43
# Default error message
44
DEFAULT_ERROR_CONTENT_TYPE = "text/html"
45
DEFAULT_ERROR_MESSAGE = """\
46
<html>
47
<head>
48
<title>Error response</title>
49
</head>
50
<body>
51
<h1>Error response</h1>
52
<p>Error code %(code)d.
53
<p>Message: %(message)s.
54
<p>Error code explanation: %(code)s = %(explain)s.
55
</body>
56
</html>
57
"""
58

    
59

    
60
def _DateTimeHeader(gmnow=None):
61
  """Return the current date and time formatted for a message header.
62

63
  The time MUST be in the GMT timezone.
64

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

    
72

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

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

    
84
    # Response attributes
85
    self.resp_headers = {}
86

    
87
    # Private data for request handler (useful in combination with
88
    # authentication)
89
    self.private = None
90

    
91

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

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

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

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

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

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

    
123
    response_code = self._response_msg.start_line.code
124

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

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

    
145

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

149
  """
150
  # Length limits
151
  START_LINE_LENGTH_MAX = 4096
152
  HEADER_LENGTH_MAX = 4096
153

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

157
    Example: "GET /index.html HTTP/1.1"
158

159
    @type start_line: string
160
    @param start_line: Start line
161

162
    """
163
    # Empty lines are skipped when reading
164
    assert start_line
165

    
166
    logging.debug("HTTP request: %s", start_line)
167

    
168
    words = start_line.split()
169

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

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

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

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

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

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

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

    
205
    return http.HttpClientToServerStartLine(method, path, version)
206

    
207

    
208
class HttpServerRequestExecutor(object):
209
  """Implements server side of HTTP.
210

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

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

    
223
  # Error message settings
224
  error_message_format = DEFAULT_ERROR_MESSAGE
225
  error_content_type = DEFAULT_ERROR_CONTENT_TYPE
226

    
227
  responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
228

    
229
  # Timeouts in seconds for socket layer
230
  WRITE_TIMEOUT = 10
231
  READ_TIMEOUT = 10
232
  CLOSE_TIMEOUT = 1
233

    
234
  def __init__(self, server, sock, client_addr):
235
    """Initializes this class.
236

237
    """
238
    self.server = server
239
    self.sock = sock
240
    self.client_addr = client_addr
241

    
242
    self.request_msg = http.HttpMessage()
243
    self.response_msg = http.HttpMessage()
244

    
245
    self.response_msg.start_line = \
246
      http.HttpServerToClientStartLine(version=self.default_request_version,
247
                                       code=None, reason=None)
248

    
249
    # Disable Python's timeout
250
    self.sock.settimeout(None)
251

    
252
    # Operate in non-blocking mode
253
    self.sock.setblocking(0)
254

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

    
269
        try:
270
          try:
271
            request_msg_reader = self._ReadRequest()
272
            self._HandleRequest()
273

    
274
            # Only wait for client to close if we didn't have any exception.
275
            force_close = False
276
          except http.HttpException, err:
277
            self._SetErrorStatus(err)
278
        finally:
279
          # Try to send a response
280
          self._SendResponse()
281
      finally:
282
        http.ShutdownConnection(sock, 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.debug("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
      try:
315
        # Authentication, etc.
316
        self.server.PreHandleRequest(handler_context)
317

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

    
329
      # TODO: Content-type
330
      encoder = http.HttpJsonConverter()
331
      self.response_msg.start_line.code = http.HTTP_OK
332
      self.response_msg.body = encoder.Encode(result)
333
      self.response_msg.headers = handler_context.resp_headers
334
      self.response_msg.headers[http.HTTP_CONTENT_TYPE] = encoder.CONTENT_TYPE
335
    finally:
336
      # No reason to keep this any longer, even for exceptions
337
      handler_context.private = None
338

    
339
  def _SendResponse(self):
340
    """Sends the response to the client.
341

342
    """
343
    if self.response_msg.start_line.code is None:
344
      return
345

    
346
    if not self.response_msg.headers:
347
      self.response_msg.headers = {}
348

    
349
    self.response_msg.headers.update({
350
      # TODO: Keep-alive is not supported
351
      http.HTTP_CONNECTION: "close",
352
      http.HTTP_DATE: _DateTimeHeader(),
353
      http.HTTP_SERVER: http.HTTP_GANETI_VERSION,
354
      })
355

    
356
    # Get response reason based on code
357
    response_code = self.response_msg.start_line.code
358
    if response_code in self.responses:
359
      response_reason = self.responses[response_code][0]
360
    else:
361
      response_reason = ""
362
    self.response_msg.start_line.reason = response_reason
363

    
364
    logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1],
365
                 self.request_msg.start_line, response_code)
366

    
367
    try:
368
      _HttpServerToClientMessageWriter(self.sock, self.request_msg,
369
                                       self.response_msg, self.WRITE_TIMEOUT)
370
    except http.HttpSocketTimeout:
371
      raise http.HttpError("Timeout while sending response")
372
    except socket.error, err:
373
      raise http.HttpError("Error sending response: %s" % err)
374

    
375
  def _SetErrorStatus(self, err):
376
    """Sets the response code and body from a HttpException.
377

378
    @type err: HttpException
379
    @param err: Exception instance
380

381
    """
382
    try:
383
      (shortmsg, longmsg) = self.responses[err.code]
384
    except KeyError:
385
      shortmsg = longmsg = "Unknown"
386

    
387
    if err.message:
388
      message = err.message
389
    else:
390
      message = shortmsg
391

    
392
    values = {
393
      "code": err.code,
394
      "message": cgi.escape(message),
395
      "explain": longmsg,
396
      }
397

    
398
    self.response_msg.start_line.code = err.code
399

    
400
    headers = {}
401
    if err.headers:
402
      headers.update(err.headers)
403
    headers[http.HTTP_CONTENT_TYPE] = self.error_content_type
404
    self.response_msg.headers = headers
405

    
406
    self.response_msg.body = self._FormatErrorMessage(values)
407

    
408
  def _FormatErrorMessage(self, values):
409
    """Formats the body of an error message.
410

411
    @type values: dict
412
    @param values: dictionary with keys code, message and explain.
413
    @rtype: string
414
    @return: the body of the message
415

416
    """
417
    return self.error_message_format % values
418

    
419

    
420
class HttpServer(http.HttpBase, asyncore.dispatcher):
421
  """Generic HTTP server class
422

423
  Users of this class must subclass it and override the HandleRequest function.
424

425
  """
426
  MAX_CHILDREN = 20
427

    
428
  def __init__(self, mainloop, local_address, port,
429
               ssl_params=None, ssl_verify_peer=False,
430
               request_executor_class=None):
431
    """Initializes the HTTP server
432

433
    @type mainloop: ganeti.daemon.Mainloop
434
    @param mainloop: Mainloop used to poll for I/O events
435
    @type local_address: string
436
    @param local_address: Local IP address to bind to
437
    @type port: int
438
    @param port: TCP port to listen on
439
    @type ssl_params: HttpSslParams
440
    @param ssl_params: SSL key and certificate
441
    @type ssl_verify_peer: bool
442
    @param ssl_verify_peer: Whether to require client certificate
443
        and compare it with our certificate
444
    @type request_executor_class: class
445
    @param request_executor_class: an class derived from the
446
        HttpServerRequestExecutor class
447

448
    """
449
    http.HttpBase.__init__(self)
450
    asyncore.dispatcher.__init__(self)
451

    
452
    if request_executor_class is None:
453
      self.request_executor = HttpServerRequestExecutor
454
    else:
455
      self.request_executor = request_executor_class
456

    
457
    self.mainloop = mainloop
458
    self.local_address = local_address
459
    self.port = port
460

    
461
    self.socket = self._CreateSocket(ssl_params, ssl_verify_peer)
462

    
463
    # Allow port to be reused
464
    self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
465

    
466
    self._children = []
467
    self.set_socket(self.socket)
468
    self.accepting = True
469
    mainloop.RegisterSignal(self)
470

    
471
  def Start(self):
472
    self.socket.bind((self.local_address, self.port))
473
    self.socket.listen(1024)
474

    
475
  def Stop(self):
476
    self.socket.close()
477

    
478
  def handle_accept(self):
479
    self._IncomingConnection()
480

    
481
  def OnSignal(self, signum):
482
    if signum == signal.SIGCHLD:
483
      self._CollectChildren(True)
484

    
485
  def _CollectChildren(self, quick):
486
    """Checks whether any child processes are done
487

488
    @type quick: bool
489
    @param quick: Whether to only use non-blocking functions
490

491
    """
492
    if not quick:
493
      # Don't wait for other processes if it should be a quick check
494
      while len(self._children) > self.MAX_CHILDREN:
495
        try:
496
          # Waiting without a timeout brings us into a potential DoS situation.
497
          # As soon as too many children run, we'll not respond to new
498
          # requests. The real solution would be to add a timeout for children
499
          # and killing them after some time.
500
          pid, _ = os.waitpid(0, 0)
501
        except os.error:
502
          pid = None
503
        if pid and pid in self._children:
504
          self._children.remove(pid)
505

    
506
    for child in self._children:
507
      try:
508
        pid, status = os.waitpid(child, os.WNOHANG)
509
      except os.error:
510
        pid = None
511
      if pid and pid in self._children:
512
        self._children.remove(pid)
513

    
514
  def _IncomingConnection(self):
515
    """Called for each incoming connection
516

517
    """
518
    (connection, client_addr) = self.socket.accept()
519

    
520
    self._CollectChildren(False)
521

    
522
    pid = os.fork()
523
    if pid == 0:
524
      # Child process
525
      try:
526
        # The client shouldn't keep the listening socket open. If the parent
527
        # process is restarted, it would fail when there's already something
528
        # listening (in this case its own child from a previous run) on the
529
        # same port.
530
        try:
531
          self.socket.close()
532
        except socket.error:
533
          pass
534
        self.socket = None
535

    
536
        self.request_executor(self, connection, client_addr)
537
      except Exception:
538
        logging.exception("Error while handling request from %s:%s",
539
                          client_addr[0], client_addr[1])
540
        os._exit(1)
541
      os._exit(0)
542
    else:
543
      self._children.append(pid)
544

    
545
  def PreHandleRequest(self, req):
546
    """Called before handling a request.
547

548
    Can be overridden by a subclass.
549

550
    """
551

    
552
  def HandleRequest(self, req):
553
    """Handles a request.
554

555
    Must be overridden by subclass.
556

557
    """
558
    raise NotImplementedError()