server.py 16.5 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#
#

# Copyright (C) 2007, 2008 Google Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.

"""HTTP server module.

"""

import BaseHTTPServer
import cgi
import logging
import os
import select
import socket
import time
import signal
33
import asyncore
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

from ganeti import http


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

# Default error message
DEFAULT_ERROR_CONTENT_TYPE = "text/html"
DEFAULT_ERROR_MESSAGE = """\
<html>
<head>
<title>Error response</title>
</head>
<body>
<h1>Error response</h1>
<p>Error code %(code)d.
<p>Message: %(message)s.
<p>Error code explanation: %(code)s = %(explain)s.
</body>
</html>
"""


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

63
64
  The time MUST be in the GMT timezone.

65
  """
66
67
68
  if gmnow is None:
    gmnow = time.gmtime()
  (year, month, day, hh, mm, ss, wd, _, _) = gmnow
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
  return ("%s, %02d %3s %4d %02d:%02d:%02d GMT" %
          (WEEKDAYNAME[wd], day, MONTHNAME[month], year, hh, mm, ss))


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

  """
  def __init__(self, request_msg):
    # Request attributes
    self.request_method = request_msg.start_line.method
    self.request_path = request_msg.start_line.path
    self.request_headers = request_msg.headers
    self.request_body = request_msg.decoded_body

    # Response attributes
    self.resp_headers = {}

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

91
92
93
94
95
96

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

  """
  def __init__(self, sock, request_msg, response_msg, write_timeout):
97
98
99
100
101
102
    """Writes the response to the client.

    @type sock: socket
    @param sock: Target socket
    @type request_msg: http.HttpMessage
    @param request_msg: Request message, required to determine whether
Iustin Pop's avatar
Iustin Pop committed
103
        response may have a message body
104
105
106
107
    @type response_msg: http.HttpMessage
    @param response_msg: Response message
    @type write_timeout: float
    @param write_timeout: Write timeout for socket
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138

    """
    self._request_msg = request_msg
    self._response_msg = response_msg
    http.HttpMessageWriter.__init__(self, sock, response_msg, write_timeout)

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

    """
    if self._request_msg.start_line:
      request_method = self._request_msg.start_line.method
    else:
      request_method = None

    response_code = self._response_msg.start_line.code

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

    return (http.HttpMessageWriter.HasMessageBody(self) and
139
140
            (request_method is not None and
             request_method != http.HTTP_HEAD) and
141
            response_code >= http.HTTP_OK and
142
143
            response_code not in (http.HTTP_NO_CONTENT,
                                  http.HTTP_NOT_MODIFIED))
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207


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

  """
  # Length limits
  START_LINE_LENGTH_MAX = 4096
  HEADER_LENGTH_MAX = 4096

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

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

    @type start_line: string
    @param start_line: Start line

    """
    # Empty lines are skipped when reading
    assert start_line

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

    words = start_line.split()

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

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

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

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

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

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

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

    return http.HttpClientToServerStartLine(method, path, version)


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

Iustin Pop's avatar
Iustin Pop committed
211
212
213
214
  This class implements the server side of HTTP. It's based on code of
  Python's BaseHTTPServer, from both version 2.4 and 3k. It does not
  support non-ASCII character encodings. Keep-alive connections are
  not supported.
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254

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

  # Error message settings
  error_message_format = DEFAULT_ERROR_MESSAGE
  error_content_type = DEFAULT_ERROR_CONTENT_TYPE

  responses = BaseHTTPServer.BaseHTTPRequestHandler.responses

  # Timeouts in seconds for socket layer
  WRITE_TIMEOUT = 10
  READ_TIMEOUT = 10
  CLOSE_TIMEOUT = 1

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

    """
    self.server = server
    self.sock = sock
    self.client_addr = client_addr

    self.request_msg = http.HttpMessage()
    self.response_msg = http.HttpMessage()

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

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

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

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

269
270
271
272
273
274
275
276
277
278
279
280
281
        try:
          try:
            request_msg_reader = self._ReadRequest()
            self._HandleRequest()

            # Only wait for client to close if we didn't have any exception.
            force_close = False
          except http.HttpException, err:
            self._SetErrorStatus(err)
        finally:
          # Try to send a response
          self._SendResponse()
      finally:
282
        http.ShutdownConnection(sock, self.CLOSE_TIMEOUT, self.WRITE_TIMEOUT,
283
284
285
286
287
                                request_msg_reader, force_close)

      self.sock.close()
      self.sock = None
    finally:
Iustin Pop's avatar
Iustin Pop committed
288
      logging.debug("Disconnected %s:%s", client_addr[0], client_addr[1])
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313

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

    """
    try:
      request_msg_reader = \
        _HttpClientToServerMessageReader(self.sock, self.request_msg,
                                         self.READ_TIMEOUT)
    except http.HttpSocketTimeout:
      raise http.HttpError("Timeout while reading request")
    except socket.error, err:
      raise http.HttpError("Error reading request: %s" % err)

    self.response_msg.start_line.version = self.request_msg.start_line.version

    return request_msg_reader

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

    """
    handler_context = _HttpServerRequest(self.request_msg)

    try:
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
      try:
        # Authentication, etc.
        self.server.PreHandleRequest(handler_context)

        # Call actual request handler
        result = self.server.HandleRequest(handler_context)
      except (http.HttpException, KeyboardInterrupt, SystemExit):
        raise
      except Exception, err:
        logging.exception("Caught exception")
        raise http.HttpInternalServerError(message=str(err))
      except:
        logging.exception("Unknown exception")
        raise http.HttpInternalServerError(message="Unknown error")

      # TODO: Content-type
      encoder = http.HttpJsonConverter()
      self.response_msg.start_line.code = http.HTTP_OK
      self.response_msg.body = encoder.Encode(result)
      self.response_msg.headers = handler_context.resp_headers
      self.response_msg.headers[http.HTTP_CONTENT_TYPE] = encoder.CONTENT_TYPE
    finally:
      # No reason to keep this any longer, even for exceptions
      handler_context.private = None
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398

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

    """
    if self.response_msg.start_line.code is None:
      return

    if not self.response_msg.headers:
      self.response_msg.headers = {}

    self.response_msg.headers.update({
      # TODO: Keep-alive is not supported
      http.HTTP_CONNECTION: "close",
      http.HTTP_DATE: _DateTimeHeader(),
      http.HTTP_SERVER: http.HTTP_GANETI_VERSION,
      })

    # Get response reason based on code
    response_code = self.response_msg.start_line.code
    if response_code in self.responses:
      response_reason = self.responses[response_code][0]
    else:
      response_reason = ""
    self.response_msg.start_line.reason = response_reason

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

    try:
      _HttpServerToClientMessageWriter(self.sock, self.request_msg,
                                       self.response_msg, self.WRITE_TIMEOUT)
    except http.HttpSocketTimeout:
      raise http.HttpError("Timeout while sending response")
    except socket.error, err:
      raise http.HttpError("Error sending response: %s" % err)

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

    @type err: HttpException
    @param err: Exception instance

    """
    try:
      (shortmsg, longmsg) = self.responses[err.code]
    except KeyError:
      shortmsg = longmsg = "Unknown"

    if err.message:
      message = err.message
    else:
      message = shortmsg

    values = {
      "code": err.code,
      "message": cgi.escape(message),
      "explain": longmsg,
      }

    self.response_msg.start_line.code = err.code
399
400
401
402
403
404
405

    headers = {}
    if err.headers:
      headers.update(err.headers)
    headers[http.HTTP_CONTENT_TYPE] = self.error_content_type
    self.response_msg.headers = headers

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

408
409
410
411
412
413
414
415
416
417
  def _FormatErrorMessage(self, values):
    """Formats the body of an error message.

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

    """
    return self.error_message_format % values
418

419
class HttpServer(http.HttpBase, asyncore.dispatcher):
420
421
422
423
424
425
426
427
  """Generic HTTP server class

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

  """
  MAX_CHILDREN = 20

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

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

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

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

456
457
458
459
460
461
462
463
464
465
    self.mainloop = mainloop
    self.local_address = local_address
    self.port = port

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

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

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

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

  def Stop(self):
    self.socket.close()

477
478
  def handle_accept(self):
    self._IncomingConnection()
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498

  def OnSignal(self, signum):
    if signum == signal.SIGCHLD:
      self._CollectChildren(True)

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

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

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

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

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

    """
Iustin Pop's avatar
Iustin Pop committed
517
    # pylint: disable-msg=W0212
518
519
520
521
522
523
524
525
    (connection, client_addr) = self.socket.accept()

    self._CollectChildren(False)

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

536
        self.request_executor(self, connection, client_addr)
Iustin Pop's avatar
Iustin Pop committed
537
      except Exception: # pylint: disable-msg=W0703
538
539
540
541
542
543
544
        logging.exception("Error while handling request from %s:%s",
                          client_addr[0], client_addr[1])
        os._exit(1)
      os._exit(0)
    else:
      self._children.append(pid)

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

Michael Hanselmann's avatar
Michael Hanselmann committed
548
    Can be overridden by a subclass.
549
550
551

    """

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

Michael Hanselmann's avatar
Michael Hanselmann committed
555
    Must be overridden by subclass.
556
557
558

    """
    raise NotImplementedError()