ia64/xen-unstable

view tools/python/xen/web/http.py @ 7070:b6e58e2daff8

Added missing import socket statement.

Signed-off-by: Ewan Mellor <ewan@xensource.com>
author emellor@ewan
date Tue Sep 27 12:29:36 2005 +0100 (2005-09-27)
parents 16efdf7bbd57
children 9ff1bea68d51
line source
1 #============================================================================
2 # This library is free software; you can redistribute it and/or
3 # modify it under the terms of version 2.1 of the GNU Lesser General Public
4 # License as published by the Free Software Foundation.
5 #
6 # This library is distributed in the hope that it will be useful,
7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9 # Lesser General Public License for more details.
10 #
11 # You should have received a copy of the GNU Lesser General Public
12 # License along with this library; if not, write to the Free Software
13 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
14 #
15 #============================================================================
16 # Parts of this library are derived from Twisted:
17 # Copyright (C) 2001 Matthew W. Lefkowitz
18 #
19 # Copyright (C) 2005 Mike Wray <mike.wray@hp.com>
20 #============================================================================
22 from mimetools import Message
23 from cStringIO import StringIO
24 import math
25 import socket
26 import time
27 import cgi
29 CONTINUE = 100
30 SWITCHING_PROTOCOLS = 101
32 OK = 200
33 CREATED = 201
34 ACCEPTED = 202
35 NON_AUTHORITATIVE_INFORMATION = 203
36 NO_CONTENT = 204
37 RESET_CONTENT = 205
38 PARTIAL_CONTENT = 206
39 MULTI_STATUS = 207
41 MULTIPLE_CHOICE = 300
42 MOVED_PERMANENTLY = 301
43 FOUND = 302
44 SEE_OTHER = 303
45 NOT_MODIFIED = 304
46 USE_PROXY = 305
47 TEMPORARY_REDIRECT = 307
49 BAD_REQUEST = 400
50 UNAUTHORIZED = 401
51 PAYMENT_REQUIRED = 402
52 FORBIDDEN = 403
53 NOT_FOUND = 404
54 NOT_ALLOWED = 405
55 NOT_ACCEPTABLE = 406
56 PROXY_AUTH_REQUIRED = 407
57 REQUEST_TIMEOUT = 408
58 CONFLICT = 409
59 GONE = 410
60 LENGTH_REQUIRED = 411
61 PRECONDITION_FAILED = 412
62 REQUEST_ENTITY_TOO_LARGE = 413
63 REQUEST_URI_TOO_LONG = 414
64 UNSUPPORTED_MEDIA_TYPE = 415
65 REQUESTED_RANGE_NOT_SATISFIABLE = 416
66 EXPECTATION_FAILED = 417
68 INTERNAL_SERVER_ERROR = 500
69 NOT_IMPLEMENTED = 501
70 BAD_GATEWAY = 502
71 SERVICE_UNAVAILABLE = 503
72 GATEWAY_TIMEOUT = 504
73 VERSION_NOT_SUPPORTED = 505
74 INSUFFICIENT_STORAGE_SPACE = 507
75 NOT_EXTENDED = 510
77 NO_BODY_CODES = [ NO_CONTENT, NOT_MODIFIED ]
80 STATUS = {
81 CONTINUE : "Continue",
82 SWITCHING_PROTOCOLS : "Switching protocols",
84 OK : "OK",
85 CREATED : "Created",
86 ACCEPTED : "Accepted",
87 NON_AUTHORITATIVE_INFORMATION : "Non-authoritative information",
88 NO_CONTENT : "No content",
89 RESET_CONTENT : "Reset content",
90 PARTIAL_CONTENT : "Partial content",
91 MULTI_STATUS : "Multi-status",
93 MULTIPLE_CHOICE : "Multiple choice",
94 MOVED_PERMANENTLY : "Moved permanently",
95 FOUND : "Found",
96 SEE_OTHER : "See other",
97 NOT_MODIFIED : "Not modified",
98 USE_PROXY : "Use proxy",
99 TEMPORARY_REDIRECT : "Temporary redirect",
101 BAD_REQUEST : "Bad request",
102 UNAUTHORIZED : "Unauthorized",
103 PAYMENT_REQUIRED : "Payment required",
104 FORBIDDEN : "Forbidden",
105 NOT_FOUND : "Not found",
106 NOT_ALLOWED : "Not allowed",
107 NOT_ACCEPTABLE : "Not acceptable",
108 PROXY_AUTH_REQUIRED : "Proxy authentication required",
109 REQUEST_TIMEOUT : "Request timeout",
110 CONFLICT : "Conflict",
111 GONE : "Gone",
112 LENGTH_REQUIRED : "Length required",
113 PRECONDITION_FAILED : "Precondition failed",
114 REQUEST_ENTITY_TOO_LARGE : "Request entity too large",
115 REQUEST_URI_TOO_LONG : "Request URI too long",
116 UNSUPPORTED_MEDIA_TYPE : "Unsupported media type",
117 REQUESTED_RANGE_NOT_SATISFIABLE : "Requested range not satisfiable",
118 EXPECTATION_FAILED : "Expectation failed",
120 INTERNAL_SERVER_ERROR : "Internal server error",
121 NOT_IMPLEMENTED : "Not implemented",
122 BAD_GATEWAY : "Bad gateway",
123 SERVICE_UNAVAILABLE : "Service unavailable",
124 GATEWAY_TIMEOUT : "Gateway timeout",
125 VERSION_NOT_SUPPORTED : "HTTP version not supported",
126 INSUFFICIENT_STORAGE_SPACE : "Insufficient storage space",
127 NOT_EXTENDED : "Not extended",
128 }
130 def getStatus(code):
131 return STATUS.get(code, "unknown")
133 MULTIPART_FORM_DATA = 'multipart/form-data'
134 URLENCODED = 'application/x-www-form-urlencoded'
136 parseQueryArgs = cgi.parse_qs
138 def timegm(year, month, day, hour, minute, second):
139 """Convert time tuple in GMT to seconds since epoch, GMT"""
140 EPOCH = 1970
141 assert year >= EPOCH
142 assert 1 <= month <= 12
143 days = 365*(year-EPOCH) + calendar.leapdays(EPOCH, year)
144 for i in range(1, month):
145 days = days + calendar.mdays[i]
146 if month > 2 and calendar.isleap(year):
147 days = days + 1
148 days = days + day - 1
149 hours = days*24 + hour
150 minutes = hours*60 + minute
151 seconds = minutes*60 + second
152 return seconds
154 def stringToDatetime(dateString):
155 """Convert an HTTP date string to seconds since epoch."""
156 parts = dateString.split(' ')
157 day = int(parts[1])
158 month = int(monthname.index(parts[2]))
159 year = int(parts[3])
160 hour, min, sec = map(int, parts[4].split(':'))
161 return int(timegm(year, month, day, hour, min, sec))
163 class HttpRequest:
165 http_version = (1, 1)
167 http_version_string = ("HTTP/%d.%d" % http_version)
169 max_content_length = 10000
170 max_headers = 500
172 request_line = None
173 request_method = None
174 request_uri = None
175 request_path = None
176 request_query = None
177 request_version = None
178 content_length = 0
179 content = None
180 etag = None
181 close_connection = True
182 response_code = 200
183 response_status = "OK"
184 response_sent = False
185 cached = False
186 last_modified = None
188 forceSSL = False
190 def __init__(self, host, rin, out):
191 self.host = host
192 self.rin = rin
193 self.out = out
194 self.request_args = {}
195 self.args = self.request_args
196 self.request_headers = {}
197 self.request_cookies = {}
198 self.response_headers = {}
199 self.response_cookies = {}
200 self.output = StringIO()
201 self.parseRequest()
203 def isSecure(self):
204 return self.forceSSL
206 def getRequestMethod(self):
207 return self.request_method
209 def trim(self, str, ends):
210 for end in ends:
211 if str.endswith(end):
212 str = str[ : -len(end) ]
213 break
214 return str
216 def requestError(self, code, msg=None):
217 self.sendError(code, msg)
218 raise ValueError(self.response_status)
220 def sendError(self, code, msg=None):
221 self.setResponseCode(code, msg=msg)
222 self.sendResponse()
224 def parseRequestVersion(self, version):
225 try:
226 if not version.startswith('HTTP/'):
227 raise ValueError
228 version_string = version.split('/', 1)[1]
229 version_codes = version_string.split('.')
230 if len(version_codes) != 2:
231 raise ValueError
232 request_version = (int(version_codes[0]), int(version_codes[1]))
233 except (ValueError, IndexError):
234 self.requestError(400, "Bad request version (%s)" % `version`)
236 def parseRequestLine(self):
237 line = self.trim(self.request_line, ['\r\n', '\n'])
238 line_fields = line.split()
239 n = len(line_fields)
240 if n == 3:
241 [method, uri, version] = line_fields
242 elif n == 2:
243 [method, uri] = line_fields
244 version = 'HTTP/0.9'
245 else:
246 self.requestError(BAD_REQUEST,
247 "Bad request (%s)" % `line`)
249 request_version = self.parseRequestVersion(version)
251 if request_version > (2, 0):
252 self.requestError(VERSION_NOT_SUPPORTED,
253 "HTTP version not supported (%s)" % `version`)
254 #if request_version >= (1, 1) and self.http_version >= (1, 1):
255 # self.close_connection = False
256 #else:
257 # self.close_connection = True
259 self.request_method = method
260 self.method = method
261 self.request_uri = uri
262 self.request_version = version
264 uri_query = uri.split('?')
265 if len(uri_query) == 1:
266 self.request_path = uri
267 else:
268 self.request_path = uri_query[0]
269 self.request_query = uri_query[1]
270 self.request_args = parseQueryArgs(self.request_query)
271 self.args = self.request_args
274 def parseRequestHeaders(self):
275 header_bytes = ""
276 header_count = 0
277 while True:
278 if header_count >= self.max_headers:
279 self.requestError(BAD_REQUEST,
280 "Bad request (too many headers)")
281 line = self.rin.readline()
282 header_bytes += line
283 header_count += 1
284 if line == '\r\n' or line == '\n' or line == '':
285 break
286 header_input = StringIO(header_bytes)
287 self.request_headers = Message(header_input)
289 def parseRequestCookies(self):
290 cookie_hdr = self.getHeader("cookie")
291 if not cookie_hdr: return
292 for cookie in cookie_hdr.split(';'):
293 try:
294 cookie = cookie.lstrip()
295 (k, v) = cookie.split('=', 1)
296 self.request_cookies[k] = v
297 except ValueError:
298 pass
300 def parseRequestArgs(self):
301 if ((self.content is None) or
302 (self.request_method != "POST")):
303 return
304 content_type = self.getHeader('content-type')
305 if not content_type:
306 return
307 (encoding, params) = cgi.parse_header(content_type)
308 if encoding == URLENCODED:
309 xargs = cgi.parse_qs(self.content.getvalue(),
310 keep_blank_values=True)
311 elif encoding == MULTIPART_FORM_DATA:
312 xargs = cgi.parse_multipart(self.content, params)
313 else:
314 xargs = {}
315 self.request_args.update(xargs)
317 def getCookie(self, k):
318 return self.request_cookies[k]
320 def readContent(self):
321 try:
322 self.content_length = int(self.getHeader("Content-Length"))
323 except:
324 return
325 if self.content_length > self.max_content_length:
326 self.requestError(REQUEST_ENTITY_TOO_LARGE)
327 self.content = self.rin.read(self.content_length)
328 self.content = StringIO(self.content)
329 self.content.seek(0,0)
331 def parseRequest(self):
332 self.request_line = self.rin.readline()
333 self.parseRequestLine()
334 self.parseRequestHeaders()
335 self.parseRequestCookies()
336 connection_mode = self.getHeader('Connection')
337 self.setCloseConnection(connection_mode)
338 self.readContent()
339 self.parseRequestArgs()
341 def setCloseConnection(self, mode):
342 if not mode: return
343 mode = mode.lower()
344 if mode == 'close':
345 self.close_connection = True
346 elif (mode == 'keep-alive') and (self.http_version >= (1, 1)):
347 self.close_connection = False
349 def getCloseConnection(self):
350 return self.close_connection
352 def getHeader(self, k, v=None):
353 return self.request_headers.get(k, v)
355 def getRequestMethod(self):
356 return self.request_method
358 def getRequestPath(self):
359 return self.request_path
361 def setResponseCode(self, code, status=None, msg=None):
362 self.response_code = code
363 if not status:
364 status = getStatus(code)
365 self.response_status = status
367 def setResponseHeader(self, k, v):
368 k = k.lower()
369 self.response_headers[k] = v
370 if k == 'connection':
371 self.setCloseConnection(v)
373 setHeader = setResponseHeader
375 def setLastModified(self, when):
376 # time.time() may be a float, but the HTTP-date strings are
377 # only good for whole seconds.
378 when = long(math.ceil(when))
379 if (not self.last_modified) or (self.last_modified < when):
380 self.lastModified = when
382 modified_since = self.getHeader('if-modified-since')
383 if modified_since:
384 modified_since = stringToDatetime(modified_since)
385 if modified_since >= when:
386 self.setResponseCode(NOT_MODIFIED)
387 self.cached = True
389 def setContentType(self, ty):
390 self.setResponseHeader("Content-Type", ty)
392 def setEtag(self, etag):
393 if etag:
394 self.etag = etag
396 tags = self.getHeader("if-none-match")
397 if tags:
398 tags = tags.split()
399 if (etag in tags) or ('*' in tags):
400 if self.request_method in ("HEAD", "GET"):
401 code = NOT_MODIFIED
402 else:
403 code = PRECONDITION_FAILED
404 self.setResponseCode(code)
405 self.cached = True
407 def addCookie(self, k, v, expires=None, domain=None, path=None,
408 max_age=None, comment=None, secure=None):
409 cookie = v
410 if expires != None:
411 cookie += "; Expires=%s" % expires
412 if domain != None:
413 cookie += "; Domain=%s" % domain
414 if path != None:
415 cookie += "; Path=%s" % path
416 if max_age != None:
417 cookie += "; Max-Age=%s" % max_age
418 if comment != None:
419 cookie += "; Comment=%s" % comment
420 if secure:
421 cookie += "; Secure"
422 self.response_cookies[k] = cookie
424 def sendResponseHeaders(self):
425 if self.etag:
426 self.setResponseHeader("ETag", self.etag)
427 for (k, v) in self.response_headers.items():
428 self.send("%s: %s\r\n" % (k.capitalize(), v))
429 for (k, v) in self.response_cookies.items():
430 self.send("Set-Cookie: %s=%s\r\n" % (k, v))
431 self.send("\r\n")
433 def sendResponse(self):
434 if self.response_sent:
435 return
436 self.response_sent = True
437 send_body = self.hasBody()
438 if not self.close_connection:
439 self.setResponseHeader("Connection", "keep-alive")
440 if send_body:
441 self.output.seek(0, 0)
442 body = self.output.getvalue()
443 body_length = len(body)
444 self.setResponseHeader("Content-Length", body_length)
445 if self.http_version > (0, 9):
446 self.send("%s %d %s\r\n" % (self.http_version_string,
447 self.response_code,
448 self.response_status))
449 self.sendResponseHeaders()
450 if send_body:
451 self.send(body)
452 self.flush()
454 def write(self, data):
455 self.output.write(data)
457 def send(self, data):
458 #print 'send>', data
459 self.out.write(data)
461 def flush(self):
462 self.out.flush()
464 def hasNoBody(self):
465 return ((self.request_method == "HEAD") or
466 (self.response_code in NO_BODY_CODES) or
467 self.cached)
469 def hasBody(self):
470 return not self.hasNoBody()
472 def process(self):
473 pass
474 return self.close_connection
476 def getRequestHostname(self):
477 """Get the hostname that the user passed in to the request.
479 Uses the 'Host:' header if it is available, and the
480 host we are listening on otherwise.
481 """
482 return (self.getHeader('host') or
483 socket.gethostbyaddr(self.getHostAddr())[0]
484 ).split(':')[0]
486 def getHost(self):
487 return self.host
489 def getHostAddr(self):
490 return self.host[0]
492 def getPort(self):
493 return self.host[1]
495 def setHost(self, host, port, ssl=0):
496 """Change the host and port the request thinks it's using.
498 This method is useful for working with reverse HTTP proxies (e.g.
499 both Squid and Apache's mod_proxy can do this), when the address
500 the HTTP client is using is different than the one we're listening on.
502 For example, Apache may be listening on https://www.example.com, and then
503 forwarding requests to http://localhost:8080, but we don't want HTML produced
504 to say 'http://localhost:8080', they should say 'https://www.example.com',
505 so we do::
507 request.setHost('www.example.com', 443, ssl=1)
509 """
510 self.forceSSL = ssl
511 self.received_headers["host"] = host
512 self.host = (host, port)