Convert SnapshotBlockDevice's docstring to epydoc
[ganeti-local] / lib / http.py
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
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)