Statistics
| Branch: | Tag: | Revision:

root / lib / http.py @ fa10bdc5

History | View | Annotate | Download (17.8 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

    
34
from ganeti import constants
35
from ganeti import logger
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

    
246
      self.should_fork = self._server.ForkForRequest(self)
247
    except HTTPException, err:
248
      self._SetErrorStatus(err)
249

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

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

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

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

267
    @type err: HTTPException
268
    @param err: Exception instance
269

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

    
276
    if err.message:
277
      message = err.message
278
    else:
279
      message = shortmsg
280

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

    
287
    self.response_code = err.code
288
    self.response_content_type = self.error_content_type
289
    self.response_body = self.error_message_format % values
290

    
291
  def HandleRequest(self):
292
    """Handle the actual request.
293

294
    Calls the actual handler function and converts exceptions into HTTP errors.
295

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

    
301
    assert self.request_method, "Status code %s requires a method" % HTTP_OK
302

    
303
    # Check whether client is still there
304
    self.rfile.read(0)
305

    
306
    try:
307
      try:
308
        result = self._server.HandleRequest(self)
309

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

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

    
325
    except HTTPException, err:
326
      self._SetErrorStatus(err)
327

    
328
  def SendResponse(self):
329
    """Sends response to the client.
330

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

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

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

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

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

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

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

    
367
  def _ReadRequest(self):
368
    """Reads and parses request line
369

370
    """
371
    raw_requestline = self.rfile.readline()
372

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

    
379
    if not requestline:
380
      raise HTTPBadRequest("Empty request line")
381

    
382
    self.request_requestline = requestline
383

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

    
386
    words = requestline.split()
387

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

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

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

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

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

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

    
420
    else:
421
      raise HTTPBadRequest("Bad request syntax (%r)" % requestline)
422

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

    
426
    self.request_method = method
427
    self.request_path = path
428
    self.request_version = version
429
    self.request_headers = headers
430

    
431
  def _ReadPostData(self):
432
    """Reads POST/PUT data
433

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

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

    
445
    data = self.rfile.read(content_length)
446

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

    
450
    logging.debug("HTTP POST data: %s", self.request_post_data)
451

    
452

    
453
class HttpServer(object):
454
  """Generic HTTP server class
455

456
  Users of this class must subclass it and override the HandleRequest function.
457
  Optionally, the ForkForRequest function can be overriden.
458

459
  """
460
  MAX_CHILDREN = 20
461

    
462
  def __init__(self, mainloop, server_address):
463
    self.mainloop = mainloop
464
    self.server_address = server_address
465

    
466
    # TODO: SSL support
467
    self.ssl_cert = None
468
    self.ssl_key = self.ssl_cert
469

    
470
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
471

    
472
    if self.ssl_cert and self.ssl_key:
473
      ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
474
      ctx.set_options(OpenSSL.SSL.OP_NO_SSLv2)
475

    
476
      ctx.use_certificate_file(self.ssl_cert)
477
      ctx.use_privatekey_file(self.ssl_key)
478

    
479
      self.socket = OpenSSL.SSL.Connection(ctx, sock)
480
      self._fileio_class = _SSLFileObject
481
    else:
482
      self.socket = sock
483
      self._fileio_class = socket._fileobject
484

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

    
488
    self._children = []
489

    
490
    mainloop.RegisterIO(self, self.socket.fileno(), select.POLLIN)
491
    mainloop.RegisterSignal(self)
492

    
493
  def Start(self):
494
    self.socket.bind(self.server_address)
495
    self.socket.listen(5)
496

    
497
  def Stop(self):
498
    self.socket.close()
499

    
500
  def OnIO(self, fd, condition):
501
    if condition & select.POLLIN:
502
      self._IncomingConnection()
503

    
504
  def OnSignal(self, signum):
505
    if signum == signal.SIGCHLD:
506
      self._CollectChildren(True)
507

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

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

514
    """
515
    if not quick:
516
      # Don't wait for other processes if it should be a quick check
517
      while len(self._children) > self.MAX_CHILDREN:
518
        try:
519
          pid, status = os.waitpid(0, 0)
520
        except os.error:
521
          pid = None
522
        if pid and pid in self._children:
523
          self._children.remove(pid)
524

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

    
533
  def _IncomingConnection(self):
534
    connection, client_addr = self.socket.accept()
535
    logging.info("Connection from %s:%s", client_addr[0], client_addr[1])
536
    try:
537
      handler = _HttpConnectionHandler(self, connection, client_addr, self._fileio_class)
538
    except (socket.error, SocketClosed):
539
      return
540

    
541
    def FinishRequest():
542
      try:
543
        try:
544
          try:
545
            handler.HandleRequest()
546
          finally:
547
            # Try to send a response
548
            handler.SendResponse()
549
            handler.Close()
550
        except SocketClosed:
551
          pass
552
      finally:
553
        logging.info("Disconnected %s:%s", client_addr[0], client_addr[1])
554

    
555
    # Check whether we should fork or not
556
    if not handler.should_fork:
557
      FinishRequest()
558
      return
559

    
560
    self._CollectChildren(False)
561

    
562
    pid = os.fork()
563
    if pid == 0:
564
      # Child process
565
      try:
566
        FinishRequest()
567
      except:
568
        logging.exception("Error while handling request from %s:%s",
569
                          client_addr[0], client_addr[1])
570
        os._exit(1)
571
      os._exit(0)
572
    else:
573
      self._children.append(pid)
574

    
575
  def HandleRequest(self, req):
576
    raise NotImplementedError()
577

    
578
  def ForkForRequest(self, req):
579
    return True
580

    
581

    
582
class _SSLFileObject(object):
583
  """Wrapper around socket._fileobject
584

585
  This wrapper is required to handle OpenSSL exceptions.
586

587
  """
588
  def _RequireOpenSocket(fn):
589
    def wrapper(self, *args, **kwargs):
590
      if self.closed:
591
        raise SocketClosed("Socket is closed")
592
      return fn(self, *args, **kwargs)
593
    return wrapper
594

    
595
  def __init__(self, sock, mode='rb', bufsize=-1):
596
    self._base = socket._fileobject(sock, mode=mode, bufsize=bufsize)
597

    
598
  def _ConnectionLost(self):
599
    self._base = None
600

    
601
  def _getclosed(self):
602
    return self._base is None or self._base.closed
603
  closed = property(_getclosed, doc="True if the file is closed")
604

    
605
  @_RequireOpenSocket
606
  def close(self):
607
    return self._base.close()
608

    
609
  @_RequireOpenSocket
610
  def flush(self):
611
    return self._base.flush()
612

    
613
  @_RequireOpenSocket
614
  def fileno(self):
615
    return self._base.fileno()
616

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

    
621
  @_RequireOpenSocket
622
  def readline(self, size=-1):
623
    return self._ReadWrapper(self._base.readline, size=size)
624

    
625
  def _ReadWrapper(self, fn, *args, **kwargs):
626
    while True:
627
      try:
628
        return fn(*args, **kwargs)
629

    
630
      except OpenSSL.SSL.ZeroReturnError, err:
631
        self._ConnectionLost()
632
        return ""
633

    
634
      except OpenSSL.SSL.WantReadError:
635
        continue
636

    
637
      #except OpenSSL.SSL.WantWriteError:
638
      # TODO
639

    
640
      except OpenSSL.SSL.SysCallError, (retval, desc):
641
        if ((retval == -1 and desc == "Unexpected EOF")
642
            or retval > 0):
643
          self._ConnectionLost()
644
          return ""
645

    
646
        logging.exception("Error in OpenSSL")
647
        self._ConnectionLost()
648
        raise socket.error(err.args)
649

    
650
      except OpenSSL.SSL.Error, err:
651
        self._ConnectionLost()
652
        raise socket.error(err.args)
653

    
654
  @_RequireOpenSocket
655
  def write(self, data):
656
    return self._WriteWrapper(self._base.write, data)
657

    
658
  def _WriteWrapper(self, fn, *args, **kwargs):
659
    while True:
660
      try:
661
        return fn(*args, **kwargs)
662
      except OpenSSL.SSL.ZeroReturnError, err:
663
        self._ConnectionLost()
664
        return 0
665

    
666
      except OpenSSL.SSL.WantWriteError:
667
        continue
668

    
669
      #except OpenSSL.SSL.WantReadError:
670
      # TODO
671

    
672
      except OpenSSL.SSL.SysCallError, err:
673
        if err.args[0] == -1 and data == "":
674
          # errors when writing empty strings are expected
675
          # and can be ignored
676
          return 0
677

    
678
        self._ConnectionLost()
679
        raise socket.error(err.args)
680

    
681
      except OpenSSL.SSL.Error, err:
682
        self._ConnectionLost()
683
        raise socket.error(err.args)