Fix a small typo in a constant
[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 socket
23 import BaseHTTPServer
24 import OpenSSL
25 import time
26 import logging
27
28 from ganeti import logger
29 from ganeti import serializer
30
31
32 class HTTPException(Exception):
33   code = None
34   message = None
35
36   def __init__(self, message=None):
37     if message is not None:
38       self.message = message
39
40
41 class HTTPBadRequest(HTTPException):
42   code = 400
43
44
45 class HTTPForbidden(HTTPException):
46   code = 403
47
48
49 class HTTPNotFound(HTTPException):
50   code = 404
51
52
53 class HTTPGone(HTTPException):
54   code = 410
55
56
57 class HTTPLengthRequired(HTTPException):
58   code = 411
59
60
61 class HTTPInternalError(HTTPException):
62   code = 500
63
64
65 class HTTPNotImplemented(HTTPException):
66   code = 501
67
68
69 class HTTPServiceUnavailable(HTTPException):
70   code = 503
71
72
73 class ApacheLogfile:
74   """Utility class to write HTTP server log files.
75
76   The written format is the "Common Log Format" as defined by Apache:
77   http://httpd.apache.org/docs/2.2/mod/mod_log_config.html#examples
78
79   """
80   MONTHNAME = [None,
81                'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
82                'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
83
84   def __init__(self, fd):
85     """Constructor for ApacheLogfile class.
86
87     Args:
88     - fd: Open file object
89
90     """
91     self._fd = fd
92
93   def LogRequest(self, request, format, *args):
94     self._fd.write("%s %s %s [%s] %s\n" % (
95       # Remote host address
96       request.address_string(),
97
98       # RFC1413 identity (identd)
99       "-",
100
101       # Remote user
102       "-",
103
104       # Request time
105       self._FormatCurrentTime(),
106
107       # Message
108       format % args,
109       ))
110     self._fd.flush()
111
112   def _FormatCurrentTime(self):
113     """Formats current time in Common Log Format.
114
115     """
116     return self._FormatLogTime(time.time())
117
118   def _FormatLogTime(self, seconds):
119     """Formats time for Common Log Format.
120
121     All timestamps are logged in the UTC timezone.
122
123     Args:
124     - seconds: Time in seconds since the epoch
125
126     """
127     (_, month, _, _, _, _, _, _, _) = tm = time.gmtime(seconds)
128     format = "%d/" + self.MONTHNAME[month] + "/%Y:%H:%M:%S +0000"
129     return time.strftime(format, tm)
130
131
132 class HTTPServer(BaseHTTPServer.HTTPServer, object):
133   """Class to provide an HTTP/HTTPS server.
134
135   """
136   allow_reuse_address = True
137
138   def __init__(self, server_address, HandlerClass, httplog=None,
139                enable_ssl=False, ssl_key=None, ssl_cert=None):
140     """Server constructor.
141
142     Args:
143       server_address: a touple containing:
144         ip: a string with IP address, localhost if empty string
145         port: port number, integer
146       HandlerClass: HTTPRequestHandler object
147       httplog: Access log object
148       enable_ssl: Whether to enable SSL
149       ssl_key: SSL key file
150       ssl_cert: SSL certificate key
151
152     """
153     BaseHTTPServer.HTTPServer.__init__(self, server_address, HandlerClass)
154
155     self.httplog = httplog
156
157     if enable_ssl:
158       # Set up SSL
159       context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
160       context.use_privatekey_file(ssl_key)
161       context.use_certificate_file(ssl_cert)
162       self.socket = OpenSSL.SSL.Connection(context,
163                                            socket.socket(self.address_family,
164                                            self.socket_type))
165     else:
166       self.socket = socket.socket(self.address_family, self.socket_type)
167
168     self.server_bind()
169     self.server_activate()
170
171
172 class HTTPJsonConverter:
173   CONTENT_TYPE = "application/json"
174
175   def Encode(self, data):
176     return serializer.DumpJson(data)
177
178   def Decode(self, data):
179     return serializer.LoadJson(data)
180
181
182 class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler, object):
183   """Request handler class.
184
185   """
186   def setup(self):
187     """Setup secure read and write file objects.
188
189     """
190     self.connection = self.request
191     self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
192     self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
193
194   def handle_one_request(self):
195     """Parses a request and calls the handler function.
196
197     """
198     self.raw_requestline = None
199     try:
200       self.raw_requestline = self.rfile.readline()
201     except OpenSSL.SSL.Error, ex:
202       logger.Error("Error in SSL: %s" % str(ex))
203     if not self.raw_requestline:
204       self.close_connection = 1
205       return
206     if not self.parse_request(): # An error code has been sent, just exit
207       return
208     logging.debug("HTTP request: %s", self.raw_requestline.rstrip("\r\n"))
209
210     try:
211       self._ReadPostData()
212
213       result = self.HandleRequest()
214
215       # TODO: Content-type
216       encoder = HTTPJsonConverter()
217       encoded_result = encoder.Encode(result)
218
219       self.send_response(200)
220       self.send_header("Content-Type", encoder.CONTENT_TYPE)
221       self.send_header("Content-Length", str(len(encoded_result)))
222       self.end_headers()
223
224       self.wfile.write(encoded_result)
225
226     except HTTPException, err:
227       self.send_error(err.code, message=err.message)
228
229     except Exception, err:
230       self.send_error(HTTPInternalError.code, message=str(err))
231
232     except:
233       self.send_error(HTTPInternalError.code, message="Unknown error")
234
235   def _ReadPostData(self):
236     if self.command.upper() not in ("POST", "PUT"):
237       self.post_data = None
238       return
239
240     # TODO: Decide what to do when Content-Length header was not sent
241     try:
242       content_length = int(self.headers.get('Content-Length', 0))
243     except ValueError:
244       raise HTTPBadRequest("No Content-Length header or invalid format")
245
246     try:
247       data = self.rfile.read(content_length)
248     except socket.error, err:
249       logger.Error("Socket error while reading: %s" % str(err))
250       return
251
252     # TODO: Content-type, error handling
253     self.post_data = HTTPJsonConverter().Decode(data)
254
255     logging.debug("HTTP POST data: %s", self.post_data)
256
257   def HandleRequest(self):
258     """Handles a request.
259
260     """
261     raise NotImplementedError()
262
263   def log_message(self, format, *args):
264     """Log an arbitrary message.
265
266     This is used by all other logging functions.
267
268     The first argument, FORMAT, is a format string for the
269     message to be logged.  If the format string contains
270     any % escapes requiring parameters, they should be
271     specified as subsequent arguments (it's just like
272     printf!).
273
274     """
275     logging.debug("Handled request: %s", format % args)
276     if self.server.httplog:
277       self.server.httplog.LogRequest(self, format, *args)