Statistics
| Branch: | Tag: | Revision:

root / lib / http.py @ 6526ddcd

History | View | Annotate | Download (17.9 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, server_address):
460
    self.mainloop = mainloop
461
    self.server_address = server_address
462

    
463
    # TODO: SSL support
464
    self.ssl_cert = None
465
    self.ssl_key = self.ssl_cert
466

    
467
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
468

    
469
    if self.ssl_cert and self.ssl_key:
470
      ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
471
      ctx.set_options(OpenSSL.SSL.OP_NO_SSLv2)
472

    
473
      ctx.use_certificate_file(self.ssl_cert)
474
      ctx.use_privatekey_file(self.ssl_key)
475

    
476
      self.socket = OpenSSL.SSL.Connection(ctx, sock)
477
      self._fileio_class = _SSLFileObject
478
    else:
479
      self.socket = sock
480
      self._fileio_class = socket._fileobject
481

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

    
485
    self._children = []
486

    
487
    mainloop.RegisterIO(self, self.socket.fileno(), select.POLLIN)
488
    mainloop.RegisterSignal(self)
489

    
490
  def Start(self):
491
    self.socket.bind(self.server_address)
492
    self.socket.listen(5)
493

    
494
  def Stop(self):
495
    self.socket.close()
496

    
497
  def OnIO(self, fd, condition):
498
    if condition & select.POLLIN:
499
      self._IncomingConnection()
500

    
501
  def OnSignal(self, signum):
502
    if signum == signal.SIGCHLD:
503
      self._CollectChildren(True)
504

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

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

511
    """
512
    if not quick:
513
      # Don't wait for other processes if it should be a quick check
514
      while len(self._children) > self.MAX_CHILDREN:
515
        try:
516
          # Waiting without a timeout brings us into a potential DoS situation.
517
          # As soon as too many children run, we'll not respond to new
518
          # requests. The real solution would be to add a timeout for children
519
          # and killing them after some time.
520
          pid, status = os.waitpid(0, 0)
521
        except os.error:
522
          pid = None
523
        if pid and pid in self._children:
524
          self._children.remove(pid)
525

    
526
    for child in self._children:
527
      try:
528
        pid, status = os.waitpid(child, os.WNOHANG)
529
      except os.error:
530
        pid = None
531
      if pid and pid in self._children:
532
        self._children.remove(pid)
533

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

537
    """
538
    (connection, client_addr) = self.socket.accept()
539

    
540
    self._CollectChildren(False)
541

    
542
    pid = os.fork()
543
    if pid == 0:
544
      # Child process
545
      logging.info("Connection from %s:%s", client_addr[0], client_addr[1])
546

    
547
      try:
548
        try:
549
          try:
550
            handler = None
551
            try:
552
              # Read, parse and handle request
553
              handler = _HttpConnectionHandler(self, connection, client_addr,
554
                                               self._fileio_class)
555
              handler.HandleRequest()
556
            finally:
557
              # Try to send a response
558
              if handler:
559
                handler.SendResponse()
560
                handler.Close()
561
          except SocketClosed:
562
            pass
563
        finally:
564
          logging.info("Disconnected %s:%s", client_addr[0], client_addr[1])
565
      except:
566
        logging.exception("Error while handling request from %s:%s",
567
                          client_addr[0], client_addr[1])
568
        os._exit(1)
569
      os._exit(0)
570
    else:
571
      self._children.append(pid)
572

    
573
  def HandleRequest(self, req):
574
    raise NotImplementedError()
575

    
576

    
577
class _SSLFileObject(object):
578
  """Wrapper around socket._fileobject
579

580
  This wrapper is required to handle OpenSSL exceptions.
581

582
  """
583
  def _RequireOpenSocket(fn):
584
    def wrapper(self, *args, **kwargs):
585
      if self.closed:
586
        raise SocketClosed("Socket is closed")
587
      return fn(self, *args, **kwargs)
588
    return wrapper
589

    
590
  def __init__(self, sock, mode='rb', bufsize=-1):
591
    self._base = socket._fileobject(sock, mode=mode, bufsize=bufsize)
592

    
593
  def _ConnectionLost(self):
594
    self._base = None
595

    
596
  def _getclosed(self):
597
    return self._base is None or self._base.closed
598
  closed = property(_getclosed, doc="True if the file is closed")
599

    
600
  @_RequireOpenSocket
601
  def close(self):
602
    return self._base.close()
603

    
604
  @_RequireOpenSocket
605
  def flush(self):
606
    return self._base.flush()
607

    
608
  @_RequireOpenSocket
609
  def fileno(self):
610
    return self._base.fileno()
611

    
612
  @_RequireOpenSocket
613
  def read(self, size=-1):
614
    return self._ReadWrapper(self._base.read, size=size)
615

    
616
  @_RequireOpenSocket
617
  def readline(self, size=-1):
618
    return self._ReadWrapper(self._base.readline, size=size)
619

    
620
  def _ReadWrapper(self, fn, *args, **kwargs):
621
    while True:
622
      try:
623
        return fn(*args, **kwargs)
624

    
625
      except OpenSSL.SSL.ZeroReturnError, err:
626
        self._ConnectionLost()
627
        return ""
628

    
629
      except OpenSSL.SSL.WantReadError:
630
        continue
631

    
632
      #except OpenSSL.SSL.WantWriteError:
633
      # TODO
634

    
635
      except OpenSSL.SSL.SysCallError, (retval, desc):
636
        if ((retval == -1 and desc == "Unexpected EOF")
637
            or retval > 0):
638
          self._ConnectionLost()
639
          return ""
640

    
641
        logging.exception("Error in OpenSSL")
642
        self._ConnectionLost()
643
        raise socket.error(err.args)
644

    
645
      except OpenSSL.SSL.Error, err:
646
        self._ConnectionLost()
647
        raise socket.error(err.args)
648

    
649
  @_RequireOpenSocket
650
  def write(self, data):
651
    return self._WriteWrapper(self._base.write, data)
652

    
653
  def _WriteWrapper(self, fn, *args, **kwargs):
654
    while True:
655
      try:
656
        return fn(*args, **kwargs)
657
      except OpenSSL.SSL.ZeroReturnError, err:
658
        self._ConnectionLost()
659
        return 0
660

    
661
      except OpenSSL.SSL.WantWriteError:
662
        continue
663

    
664
      #except OpenSSL.SSL.WantReadError:
665
      # TODO
666

    
667
      except OpenSSL.SSL.SysCallError, err:
668
        if err.args[0] == -1 and data == "":
669
          # errors when writing empty strings are expected
670
          # and can be ignored
671
          return 0
672

    
673
        self._ConnectionLost()
674
        raise socket.error(err.args)
675

    
676
      except OpenSSL.SSL.Error, err:
677
        self._ConnectionLost()
678
        raise socket.error(err.args)