http.py 6.92 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.

"""HTTP server module.

"""

import socket
import BaseHTTPServer
import OpenSSL
import time
import logging

from ganeti import logger
from ganeti import serializer


class HTTPException(Exception):
  code = None
  message = None

  def __init__(self, message=None):
    if message is not None:
      self.message = message


class HTTPBadRequest(HTTPException):
  code = 400


class HTTPForbidden(HTTPException):
  code = 403


class HTTPNotFound(HTTPException):
  code = 404


class HTTPGone(HTTPException):
  code = 410


class HTTPLengthRequired(HTTPException):
  code = 411


class HTTPInternalError(HTTPException):
  code = 500


class HTTPNotImplemented(HTTPException):
  code = 501


class HTTPServiceUnavailable(HTTPException):
  code = 503


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

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

  """
  MONTHNAME = [None,
               'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
               'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']

  def __init__(self, fd):
    """Constructor for ApacheLogfile class.

    Args:
    - fd: Open file object

    """
    self._fd = fd

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

      # RFC1413 identity (identd)
      "-",

      # Remote user
      "-",

      # Request time
      self._FormatCurrentTime(),

      # Message
      format % args,
      ))
110
    self._fd.flush()
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277

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

    """
    return self._FormatLogTime(time.time())

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

    All timestamps are logged in the UTC timezone.

    Args:
    - seconds: Time in seconds since the epoch

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


class HTTPServer(BaseHTTPServer.HTTPServer, object):
  """Class to provide an HTTP/HTTPS server.

  """
  allow_reuse_address = True

  def __init__(self, server_address, HandlerClass, httplog=None,
               enable_ssl=False, ssl_key=None, ssl_cert=None):
    """Server constructor.

    Args:
      server_address: a touple containing:
        ip: a string with IP address, localhost if empty string
        port: port number, integer
      HandlerClass: HTTPRequestHandler object
      httplog: Access log object
      enable_ssl: Whether to enable SSL
      ssl_key: SSL key file
      ssl_cert: SSL certificate key

    """
    BaseHTTPServer.HTTPServer.__init__(self, server_address, HandlerClass)

    self.httplog = httplog

    if enable_ssl:
      # Set up SSL
      context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
      context.use_privatekey_file(ssl_key)
      context.use_certificate_file(ssl_cert)
      self.socket = OpenSSL.SSL.Connection(context,
                                           socket.socket(self.address_family,
                                           self.socket_type))
    else:
      self.socket = socket.socket(self.address_family, self.socket_type)

    self.server_bind()
    self.server_activate()


class HTTPJsonConverter:
  CONTENT_TYPE = "application/json"

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

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


class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler, object):
  """Request handler class.

  """
  def setup(self):
    """Setup secure read and write file objects.

    """
    self.connection = self.request
    self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
    self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)

  def handle_one_request(self):
    """Parses a request and calls the handler function.

    """
    self.raw_requestline = None
    try:
      self.raw_requestline = self.rfile.readline()
    except OpenSSL.SSL.Error, ex:
      logger.Error("Error in SSL: %s" % str(ex))
    if not self.raw_requestline:
      self.close_connection = 1
      return
    if not self.parse_request(): # An error code has been sent, just exit
      return
    logging.debug("HTTP request: %s", self.raw_requestline.rstrip("\r\n"))

    try:
      self._ReadPostData()

      result = self.HandleRequest()

      # TODO: Content-type
      encoder = HTTPJsonConverter()
      encoded_result = encoder.Encode(result)

      self.send_response(200)
      self.send_header("Content-Type", encoder.CONTENT_TYPE)
      self.send_header("Content-Length", str(len(encoded_result)))
      self.end_headers()

      self.wfile.write(encoded_result)

    except HTTPException, err:
      self.send_error(err.code, message=err.message)

    except Exception, err:
      self.send_error(HTTPInternalError.code, message=str(err))

    except:
      self.send_error(HTTPInternalError.code, message="Unknown error")

  def _ReadPostData(self):
    if self.command.upper() not in ("POST", "PUT"):
      self.post_data = None
      return

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

    try:
      data = self.rfile.read(content_length)
    except socket.error, err:
      logger.Error("Socket error while reading: %s" % str(err))
      return

    # TODO: Content-type, error handling
    self.post_data = HTTPJsonConverter().Decode(data)

    logging.debug("HTTP POST data: %s", self.post_data)

  def HandleRequest(self):
    """Handles a request.

    """
    raise NotImplementedError()

  def log_message(self, format, *args):
    """Log an arbitrary message.

    This is used by all other logging functions.

    The first argument, FORMAT, is a format string for the
    message to be logged.  If the format string contains
    any % escapes requiring parameters, they should be
    specified as subsequent arguments (it's just like
    printf!).

    """
    logging.debug("Handled request: %s", format % args)
    if self.server.httplog:
      self.server.httplog.LogRequest(self, format, *args)