Statistics
| Branch: | Tag: | Revision:

root / lib / http.py @ 23e46494

History | View | Annotate | Download (18.3 kB)

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

    
18
"""HTTP server module.
19

20
"""
21

    
22
import BaseHTTPServer
23
import cgi
24
import logging
25
import mimetools
26
import OpenSSL
27
import os
28
import select
29
import socket
30
import sys
31
import time
32
import signal
33
import logging
34

    
35
from ganeti import constants
36
from ganeti import serializer
37

    
38

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

    
44
# Default error message
45
DEFAULT_ERROR_CONTENT_TYPE = "text/html"
46
DEFAULT_ERROR_MESSAGE = """\
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
"""
57

    
58
HTTP_OK = 200
59
HTTP_NO_CONTENT = 204
60
HTTP_NOT_MODIFIED = 304
61

    
62
HTTP_0_9 = "HTTP/0.9"
63
HTTP_1_0 = "HTTP/1.0"
64
HTTP_1_1 = "HTTP/1.1"
65

    
66
HTTP_GET = "GET"
67
HTTP_HEAD = "HEAD"
68
HTTP_ETAG = "ETag"
69

    
70

    
71
class SocketClosed(socket.error):
72
  pass
73

    
74

    
75
class HTTPException(Exception):
76
  code = None
77
  message = None
78

    
79
  def __init__(self, message=None):
80
    Exception.__init__(self)
81
    if message is not None:
82
      self.message = message
83

    
84

    
85
class HTTPBadRequest(HTTPException):
86
  code = 400
87

    
88

    
89
class HTTPForbidden(HTTPException):
90
  code = 403
91

    
92

    
93
class HTTPNotFound(HTTPException):
94
  code = 404
95

    
96

    
97
class HTTPGone(HTTPException):
98
  code = 410
99

    
100

    
101
class HTTPLengthRequired(HTTPException):
102
  code = 411
103

    
104

    
105
class HTTPInternalError(HTTPException):
106
  code = 500
107

    
108

    
109
class HTTPNotImplemented(HTTPException):
110
  code = 501
111

    
112

    
113
class HTTPServiceUnavailable(HTTPException):
114
  code = 503
115

    
116

    
117
class HTTPVersionNotSupported(HTTPException):
118
  code = 505
119

    
120

    
121
class ApacheLogfile:
122
  """Utility class to write HTTP server log files.
123

124
  The written format is the "Common Log Format" as defined by Apache:
125
  http://httpd.apache.org/docs/2.2/mod/mod_log_config.html#examples
126

127
  """
128
  def __init__(self, fd):
129
    """Constructor for ApacheLogfile class.
130

131
    Args:
132
    - fd: Open file object
133

134
    """
135
    self._fd = fd
136

    
137
  def LogRequest(self, request, format, *args):
138
    self._fd.write("%s %s %s [%s] %s\n" % (
139
      # Remote host address
140
      request.address_string(),
141

    
142
      # RFC1413 identity (identd)
143
      "-",
144

    
145
      # Remote user
146
      "-",
147

    
148
      # Request time
149
      self._FormatCurrentTime(),
150

    
151
      # Message
152
      format % args,
153
      ))
154
    self._fd.flush()
155

    
156
  def _FormatCurrentTime(self):
157
    """Formats current time in Common Log Format.
158

159
    """
160
    return self._FormatLogTime(time.time())
161

    
162
  def _FormatLogTime(self, seconds):
163
    """Formats time for Common Log Format.
164

165
    All timestamps are logged in the UTC timezone.
166

167
    Args:
168
    - seconds: Time in seconds since the epoch
169

170
    """
171
    (_, month, _, _, _, _, _, _, _) = tm = time.gmtime(seconds)
172
    format = "%d/" + MONTHNAME[month] + "/%Y:%H:%M:%S +0000"
173
    return time.strftime(format, tm)
174

    
175

    
176
class HTTPJsonConverter:
177
  CONTENT_TYPE = "application/json"
178

    
179
  def Encode(self, data):
180
    return serializer.DumpJson(data)
181

    
182
  def Decode(self, data):
183
    return serializer.LoadJson(data)
184

    
185

    
186
class _HttpConnectionHandler(object):
187
  """Implements server side of HTTP
188

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

193
  """
194
  # String for "Server" header
195
  server_version = "Ganeti %s" % constants.RELEASE_VERSION
196

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

    
203
  # Error message settings
204
  error_message_format = DEFAULT_ERROR_MESSAGE
205
  error_content_type = DEFAULT_ERROR_CONTENT_TYPE
206

    
207
  responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
208

    
209
  def __init__(self, server, conn, client_addr, fileio_class):
210
    """Initializes this class.
211

212
    Part of the initialization is reading the request and eventual POST/PUT
213
    data sent by the client.
214

215
    """
216
    self._server = server
217

    
218
    # We default rfile to buffered because otherwise it could be
219
    # really slow for large data (a getc() call per byte); we make
220
    # wfile unbuffered because (a) often after a write() we want to
221
    # read and we need to flush the line; (b) big writes to unbuffered
222
    # files are typically optimized by stdio even when big reads
223
    # aren't.
224
    self.rfile = fileio_class(conn, mode="rb", bufsize=-1)
225
    self.wfile = fileio_class(conn, mode="wb", bufsize=0)
226

    
227
    self.client_addr = client_addr
228

    
229
    self.request_headers = None
230
    self.request_method = None
231
    self.request_path = None
232
    self.request_requestline = None
233
    self.request_version = self.default_request_version
234

    
235
    self.response_body = None
236
    self.response_code = HTTP_OK
237
    self.response_content_type = None
238
    self.response_headers = {}
239

    
240
    self.should_fork = False
241

    
242
    try:
243
      self._ReadRequest()
244
      self._ReadPostData()
245
    except HTTPException, err:
246
      self._SetErrorStatus(err)
247

    
248
  def Close(self):
249
    if not self.wfile.closed:
250
      self.wfile.flush()
251
    self.wfile.close()
252
    self.rfile.close()
253

    
254
  def _DateTimeHeader(self):
255
    """Return the current date and time formatted for a message header.
256

257
    """
258
    (year, month, day, hh, mm, ss, wd, _, _) = time.gmtime()
259
    return ("%s, %02d %3s %4d %02d:%02d:%02d GMT" %
260
            (WEEKDAYNAME[wd], day, MONTHNAME[month], year, hh, mm, ss))
261

    
262
  def _SetErrorStatus(self, err):
263
    """Sets the response code and body from a HTTPException.
264

265
    @type err: HTTPException
266
    @param err: Exception instance
267

268
    """
269
    try:
270
      (shortmsg, longmsg) = self.responses[err.code]
271
    except KeyError:
272
      shortmsg = longmsg = "Unknown"
273

    
274
    if err.message:
275
      message = err.message
276
    else:
277
      message = shortmsg
278

    
279
    values = {
280
      "code": err.code,
281
      "message": cgi.escape(message),
282
      "explain": longmsg,
283
      }
284

    
285
    self.response_code = err.code
286
    self.response_content_type = self.error_content_type
287
    self.response_body = self.error_message_format % values
288

    
289
  def HandleRequest(self):
290
    """Handle the actual request.
291

292
    Calls the actual handler function and converts exceptions into HTTP errors.
293

294
    """
295
    # Don't do anything if there's already been a problem
296
    if self.response_code != HTTP_OK:
297
      return
298

    
299
    assert self.request_method, "Status code %s requires a method" % HTTP_OK
300

    
301
    # Check whether client is still there
302
    self.rfile.read(0)
303

    
304
    try:
305
      try:
306
        result = self._server.HandleRequest(self)
307

    
308
        # TODO: Content-type
309
        encoder = HTTPJsonConverter()
310
        body = encoder.Encode(result)
311

    
312
        self.response_content_type = encoder.CONTENT_TYPE
313
        self.response_body = body
314
      except (HTTPException, KeyboardInterrupt, SystemExit):
315
        raise
316
      except Exception, err:
317
        logging.exception("Caught exception")
318
        raise HTTPInternalError(message=str(err))
319
      except:
320
        logging.exception("Unknown exception")
321
        raise HTTPInternalError(message="Unknown error")
322

    
323
    except HTTPException, err:
324
      self._SetErrorStatus(err)
325

    
326
  def SendResponse(self):
327
    """Sends response to the client.
328

329
    """
330
    # Check whether client is still there
331
    self.rfile.read(0)
332

    
333
    logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1],
334
                 self.request_requestline, self.response_code)
335

    
336
    if self.response_code in self.responses:
337
      response_message = self.responses[self.response_code][0]
338
    else:
339
      response_message = ""
340

    
341
    if self.request_version != HTTP_0_9:
342
      self.wfile.write("%s %d %s\r\n" %
343
                       (self.request_version, self.response_code,
344
                        response_message))
345
      self._SendHeader("Server", self.server_version)
346
      self._SendHeader("Date", self._DateTimeHeader())
347
      self._SendHeader("Content-Type", self.response_content_type)
348
      self._SendHeader("Content-Length", str(len(self.response_body)))
349
      for key, val in self.response_headers.iteritems():
350
        self._SendHeader(key, val)
351

    
352
      # We don't support keep-alive at this time
353
      self._SendHeader("Connection", "close")
354
      self.wfile.write("\r\n")
355

    
356
    if (self.request_method != HTTP_HEAD and
357
        self.response_code >= HTTP_OK and
358
        self.response_code not in (HTTP_NO_CONTENT, HTTP_NOT_MODIFIED)):
359
      self.wfile.write(self.response_body)
360

    
361
  def _SendHeader(self, name, value):
362
    if self.request_version != HTTP_0_9:
363
      self.wfile.write("%s: %s\r\n" % (name, value))
364

    
365
  def _ReadRequest(self):
366
    """Reads and parses request line
367

368
    """
369
    raw_requestline = self.rfile.readline()
370

    
371
    requestline = raw_requestline
372
    if requestline[-2:] == '\r\n':
373
      requestline = requestline[:-2]
374
    elif requestline[-1:] == '\n':
375
      requestline = requestline[:-1]
376

    
377
    if not requestline:
378
      raise HTTPBadRequest("Empty request line")
379

    
380
    self.request_requestline = requestline
381

    
382
    logging.debug("HTTP request: %s", raw_requestline.rstrip("\r\n"))
383

    
384
    words = requestline.split()
385

    
386
    if len(words) == 3:
387
      [method, path, version] = words
388
      if version[:5] != 'HTTP/':
389
        raise HTTPBadRequest("Bad request version (%r)" % version)
390

    
391
      try:
392
        base_version_number = version.split('/', 1)[1]
393
        version_number = base_version_number.split(".")
394

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

    
404
        version_number = int(version_number[0]), int(version_number[1])
405
      except (ValueError, IndexError):
406
        raise HTTPBadRequest("Bad request version (%r)" % version)
407

    
408
      if version_number >= (2, 0):
409
        raise HTTPVersionNotSupported("Invalid HTTP Version (%s)" %
410
                                      base_version_number)
411

    
412
    elif len(words) == 2:
413
      version = HTTP_0_9
414
      [method, path] = words
415
      if method != HTTP_GET:
416
        raise HTTPBadRequest("Bad HTTP/0.9 request type (%r)" % method)
417

    
418
    else:
419
      raise HTTPBadRequest("Bad request syntax (%r)" % requestline)
420

    
421
    # Examine the headers and look for a Connection directive
422
    headers = mimetools.Message(self.rfile, 0)
423

    
424
    self.request_method = method
425
    self.request_path = path
426
    self.request_version = version
427
    self.request_headers = headers
428

    
429
  def _ReadPostData(self):
430
    """Reads POST/PUT data
431

432
    """
433
    if not self.request_method or self.request_method.upper() not in ("POST", "PUT"):
434
      self.request_post_data = None
435
      return
436

    
437
    # TODO: Decide what to do when Content-Length header was not sent
438
    try:
439
      content_length = int(self.request_headers.get('Content-Length', 0))
440
    except ValueError:
441
      raise HTTPBadRequest("No Content-Length header or invalid format")
442

    
443
    data = self.rfile.read(content_length)
444

    
445
    # TODO: Content-type, error handling
446
    self.request_post_data = HTTPJsonConverter().Decode(data)
447

    
448
    logging.debug("HTTP POST data: %s", self.request_post_data)
449

    
450

    
451
class HttpServer(object):
452
  """Generic HTTP server class
453

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

456
  """
457
  MAX_CHILDREN = 20
458

    
459
  def __init__(self, mainloop, local_address, port):
460
    """Initializes the HTTP server
461

462
    @type mainloop: ganeti.daemon.Mainloop
463
    @param mainloop: Mainloop used to poll for I/O events
464
    @type local_addess: string
465
    @param local_address: Local IP address to bind to
466
    @type port: int
467
    @param port: TCP port to listen on
468

469
    """
470
    self.mainloop = mainloop
471
    self.local_address = local_address
472
    self.port = port
473

    
474
    # TODO: SSL support
475
    self.ssl_cert = None
476
    self.ssl_key = self.ssl_cert
477

    
478
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
479

    
480
    if self.ssl_cert and self.ssl_key:
481
      ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
482
      ctx.set_options(OpenSSL.SSL.OP_NO_SSLv2)
483

    
484
      ctx.use_certificate_file(self.ssl_cert)
485
      ctx.use_privatekey_file(self.ssl_key)
486

    
487
      self.socket = OpenSSL.SSL.Connection(ctx, sock)
488
      self._fileio_class = _SSLFileObject
489
    else:
490
      self.socket = sock
491
      self._fileio_class = socket._fileobject
492

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

    
496
    self._children = []
497

    
498
    mainloop.RegisterIO(self, self.socket.fileno(), select.POLLIN)
499
    mainloop.RegisterSignal(self)
500

    
501
  def Start(self):
502
    self.socket.bind((self.local_address, self.port))
503
    self.socket.listen(5)
504

    
505
  def Stop(self):
506
    self.socket.close()
507

    
508
  def OnIO(self, fd, condition):
509
    if condition & select.POLLIN:
510
      self._IncomingConnection()
511

    
512
  def OnSignal(self, signum):
513
    if signum == signal.SIGCHLD:
514
      self._CollectChildren(True)
515

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

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

522
    """
523
    if not quick:
524
      # Don't wait for other processes if it should be a quick check
525
      while len(self._children) > self.MAX_CHILDREN:
526
        try:
527
          # Waiting without a timeout brings us into a potential DoS situation.
528
          # As soon as too many children run, we'll not respond to new
529
          # requests. The real solution would be to add a timeout for children
530
          # and killing them after some time.
531
          pid, status = os.waitpid(0, 0)
532
        except os.error:
533
          pid = None
534
        if pid and pid in self._children:
535
          self._children.remove(pid)
536

    
537
    for child in self._children:
538
      try:
539
        pid, status = os.waitpid(child, os.WNOHANG)
540
      except os.error:
541
        pid = None
542
      if pid and pid in self._children:
543
        self._children.remove(pid)
544

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

548
    """
549
    (connection, client_addr) = self.socket.accept()
550

    
551
    self._CollectChildren(False)
552

    
553
    pid = os.fork()
554
    if pid == 0:
555
      # Child process
556
      logging.info("Connection from %s:%s", client_addr[0], client_addr[1])
557

    
558
      try:
559
        try:
560
          try:
561
            handler = None
562
            try:
563
              # Read, parse and handle request
564
              handler = _HttpConnectionHandler(self, connection, client_addr,
565
                                               self._fileio_class)
566
              handler.HandleRequest()
567
            finally:
568
              # Try to send a response
569
              if handler:
570
                handler.SendResponse()
571
                handler.Close()
572
          except SocketClosed:
573
            pass
574
        finally:
575
          logging.info("Disconnected %s:%s", client_addr[0], client_addr[1])
576
      except:
577
        logging.exception("Error while handling request from %s:%s",
578
                          client_addr[0], client_addr[1])
579
        os._exit(1)
580
      os._exit(0)
581
    else:
582
      self._children.append(pid)
583

    
584
  def HandleRequest(self, req):
585
    raise NotImplementedError()
586

    
587

    
588
class _SSLFileObject(object):
589
  """Wrapper around socket._fileobject
590

591
  This wrapper is required to handle OpenSSL exceptions.
592

593
  """
594
  def _RequireOpenSocket(fn):
595
    def wrapper(self, *args, **kwargs):
596
      if self.closed:
597
        raise SocketClosed("Socket is closed")
598
      return fn(self, *args, **kwargs)
599
    return wrapper
600

    
601
  def __init__(self, sock, mode='rb', bufsize=-1):
602
    self._base = socket._fileobject(sock, mode=mode, bufsize=bufsize)
603

    
604
  def _ConnectionLost(self):
605
    self._base = None
606

    
607
  def _getclosed(self):
608
    return self._base is None or self._base.closed
609
  closed = property(_getclosed, doc="True if the file is closed")
610

    
611
  @_RequireOpenSocket
612
  def close(self):
613
    return self._base.close()
614

    
615
  @_RequireOpenSocket
616
  def flush(self):
617
    return self._base.flush()
618

    
619
  @_RequireOpenSocket
620
  def fileno(self):
621
    return self._base.fileno()
622

    
623
  @_RequireOpenSocket
624
  def read(self, size=-1):
625
    return self._ReadWrapper(self._base.read, size=size)
626

    
627
  @_RequireOpenSocket
628
  def readline(self, size=-1):
629
    return self._ReadWrapper(self._base.readline, size=size)
630

    
631
  def _ReadWrapper(self, fn, *args, **kwargs):
632
    while True:
633
      try:
634
        return fn(*args, **kwargs)
635

    
636
      except OpenSSL.SSL.ZeroReturnError, err:
637
        self._ConnectionLost()
638
        return ""
639

    
640
      except OpenSSL.SSL.WantReadError:
641
        continue
642

    
643
      #except OpenSSL.SSL.WantWriteError:
644
      # TODO
645

    
646
      except OpenSSL.SSL.SysCallError, (retval, desc):
647
        if ((retval == -1 and desc == "Unexpected EOF")
648
            or retval > 0):
649
          self._ConnectionLost()
650
          return ""
651

    
652
        logging.exception("Error in OpenSSL")
653
        self._ConnectionLost()
654
        raise socket.error(err.args)
655

    
656
      except OpenSSL.SSL.Error, err:
657
        self._ConnectionLost()
658
        raise socket.error(err.args)
659

    
660
  @_RequireOpenSocket
661
  def write(self, data):
662
    return self._WriteWrapper(self._base.write, data)
663

    
664
  def _WriteWrapper(self, fn, *args, **kwargs):
665
    while True:
666
      try:
667
        return fn(*args, **kwargs)
668
      except OpenSSL.SSL.ZeroReturnError, err:
669
        self._ConnectionLost()
670
        return 0
671

    
672
      except OpenSSL.SSL.WantWriteError:
673
        continue
674

    
675
      #except OpenSSL.SSL.WantReadError:
676
      # TODO
677

    
678
      except OpenSSL.SSL.SysCallError, err:
679
        if err.args[0] == -1 and data == "":
680
          # errors when writing empty strings are expected
681
          # and can be ignored
682
          return 0
683

    
684
        self._ConnectionLost()
685
        raise socket.error(err.args)
686

    
687
      except OpenSSL.SSL.Error, err:
688
        self._ConnectionLost()
689
        raise socket.error(err.args)