/circuits/web/wrappers.py

https://bitbucket.org/prologic/circuits/ · Python · 384 lines · 335 code · 25 blank · 24 comment · 34 complexity · b81dbd0a9a3d13d066cf5643489925e9 MD5 · raw file

  1. # Module: wrappers
  2. # Date: 13th September 2007
  3. # Author: James Mills, prologic at shortcircuit dot net dot au
  4. """Request/Response Wrappers
  5. This module implements the Request and Response objects.
  6. """
  7. from time import time
  8. from io import BytesIO
  9. from functools import partial
  10. try:
  11. from Cookie import SimpleCookie
  12. except ImportError:
  13. from http.cookies import SimpleCookie # NOQA
  14. from .url import parse_url
  15. from .headers import Headers
  16. from ..six import binary_type
  17. from .errors import httperror
  18. from circuits.net.sockets import BUFSIZE
  19. from .constants import HTTP_STATUS_CODES, SERVER_VERSION
  20. try:
  21. unicode
  22. except NameError:
  23. unicode = str
  24. try:
  25. from email.utils import formatdate
  26. formatdate = partial(formatdate, usegmt=True)
  27. except ImportError:
  28. from rfc822 import formatdate as HTTPDate # NOQA
  29. def file_generator(input, chunkSize=BUFSIZE):
  30. chunk = input.read(chunkSize)
  31. while chunk:
  32. yield chunk
  33. chunk = input.read(chunkSize)
  34. input.close()
  35. class Host(object):
  36. """An internet address.
  37. name should be the client's host name. If not available (because no DNS
  38. lookup is performed), the IP address should be used instead.
  39. """
  40. ip = "0.0.0.0"
  41. port = 80
  42. name = "unknown.tld"
  43. def __init__(self, ip, port, name=None):
  44. self.ip = ip
  45. self.port = port
  46. if name is None:
  47. name = ip
  48. self.name = name
  49. def __repr__(self):
  50. return "Host(%r, %r, %r)" % (self.ip, self.port, self.name)
  51. class HTTPStatus(object):
  52. __slots__ = ("_reason", "_status",)
  53. def __init__(self, status=200, reason=None):
  54. self._status = status
  55. self._reason = reason or HTTP_STATUS_CODES.get(status, "")
  56. def __int__(self):
  57. return self._status
  58. def __lt__(self, other):
  59. if isinstance(other, int):
  60. return self._status < other
  61. return super(HTTPStatus, self).__lt__(other)
  62. def __gt__(self, other):
  63. if isinstance(other, int):
  64. return self._status > other
  65. return super(HTTPStatus, self).__gt__(other)
  66. def __le__(self, other):
  67. if isinstance(other, int):
  68. return self._status <= other
  69. return super(HTTPStatus, self).__le__(other)
  70. def __ge__(self, other):
  71. if isinstance(other, int):
  72. return self._status >= other
  73. return super(HTTPStatus, self).__ge__(other)
  74. def __eq__(self, other):
  75. if isinstance(other, int):
  76. return self._status == other
  77. return super(HTTPStatus, self).__eq__(other)
  78. def __str__(self):
  79. return "{0:d} {1:s}".format(self._status, self._reason)
  80. def __repr__(self):
  81. return "<Status (status={0:d} reason={1:s}>".format(
  82. self._status, self._reason
  83. )
  84. def __format__(self, format_spec):
  85. return format(str(self), format_spec)
  86. @property
  87. def status(self):
  88. return self._status
  89. @property
  90. def reason(self):
  91. return self._reason
  92. class Request(object):
  93. """Creates a new Request object to hold information about a request.
  94. :param sock: The socket object of the request.
  95. :type sock: socket.socket
  96. :param method: The requested method.
  97. :type method: str
  98. :param scheme: The requested scheme.
  99. :type scheme: str
  100. :param path: The requested path.
  101. :type path: str
  102. :param protocol: The requested protocol.
  103. :type protocol: str
  104. :param qs: The query string of the request.
  105. :type qs: str
  106. """
  107. server = None
  108. """:cvar: A reference to the underlying server"""
  109. scheme = "http"
  110. protocol = (1, 1)
  111. host = ""
  112. local = Host("127.0.0.1", 80)
  113. remote = Host("", 0)
  114. index = None
  115. script_name = ""
  116. login = None
  117. handled = False
  118. def __init__(self, sock, method="GET", scheme="http", path="/",
  119. protocol=(1, 1), qs="", headers=None, server=None):
  120. "initializes x; see x.__class__.__doc__ for signature"
  121. self.sock = sock
  122. self.method = method
  123. self.scheme = scheme or Request.scheme
  124. self.path = path
  125. self.protocol = protocol
  126. self.qs = qs
  127. self.headers = headers or Headers()
  128. self.server = server
  129. self.cookie = SimpleCookie()
  130. if sock is not None:
  131. name = sock.getpeername()
  132. if name is not None:
  133. self.remote = Host(*name)
  134. else:
  135. name = sock.getsockname()
  136. self.remote = Host(name, "", name)
  137. cookie = self.headers.get("Cookie")
  138. if cookie is not None:
  139. self.cookie.load(cookie)
  140. self.body = BytesIO()
  141. if self.server is not None:
  142. self.local = Host(self.server.host, self.server.port)
  143. try:
  144. host = self.headers["Host"]
  145. if ":" in host:
  146. parts = host.split(":", 1)
  147. host = parts[0]
  148. port = int(parts[1])
  149. else:
  150. port = 443 if self.scheme == "https" else 80
  151. except KeyError:
  152. host = self.local.name or self.local.ip
  153. port = getattr(self.server, "port")
  154. self.host = host
  155. self.port = port
  156. base = "{0:s}://{1:s}{2:s}/".format(
  157. self.scheme,
  158. self.host,
  159. ":{0:d}".format(self.port) if self.port not in (80, 443) else ""
  160. )
  161. self.base = parse_url(base)
  162. url = "{0:s}{1:s}{2:s}".format(
  163. base,
  164. self.path,
  165. "?{0:s}".format(self.qs) if self.qs else ""
  166. )
  167. self.uri = parse_url(url)
  168. self.uri.sanitize()
  169. def __repr__(self):
  170. protocol = "HTTP/%d.%d" % self.protocol
  171. return "<Request %s %s %s>" % (self.method, self.path, protocol)
  172. class Body(object):
  173. """Response Body"""
  174. def __get__(self, response, cls=None):
  175. if response is None:
  176. return self
  177. else:
  178. return response._body
  179. def __set__(self, response, value):
  180. if response == value:
  181. return
  182. if isinstance(value, binary_type):
  183. if value:
  184. value = [value]
  185. else:
  186. value = []
  187. elif hasattr(value, "read"):
  188. response.stream = True
  189. value = file_generator(value)
  190. elif isinstance(value, httperror):
  191. value = [str(value)]
  192. elif value is None:
  193. value = []
  194. response._body = value
  195. class Status(object):
  196. """Response Status"""
  197. def __get__(self, response, cls=None):
  198. if response is None:
  199. return self
  200. else:
  201. return response._status
  202. def __set__(self, response, value):
  203. value = HTTPStatus(value) if isinstance(value, int) else value
  204. response._status = value
  205. class Response(object):
  206. """Response(sock, request) -> new Response object
  207. A Response object that holds the response to
  208. send back to the client. This ensure that the correct data
  209. is sent in the correct order.
  210. """
  211. body = Body()
  212. status = Status()
  213. done = False
  214. close = False
  215. stream = False
  216. chunked = False
  217. def __init__(self, request, encoding='utf-8', status=None):
  218. "initializes x; see x.__class__.__doc__ for signature"
  219. self.request = request
  220. self.encoding = encoding
  221. self._body = []
  222. self._status = HTTPStatus(status if status is not None else 200)
  223. self.time = time()
  224. self.headers = Headers()
  225. self.headers["Date"] = formatdate()
  226. if self.request.server is not None:
  227. self.headers.add_header("Server", self.request.server.http.version)
  228. else:
  229. self.headers.add_header("X-Powered-By", SERVER_VERSION)
  230. self.cookie = self.request.cookie
  231. self.protocol = "HTTP/%d.%d" % self.request.protocol
  232. def __repr__(self):
  233. return "<Response %s %s (%d)>" % (
  234. self.status,
  235. self.headers.get("Content-Type"),
  236. (len(self.body) if isinstance(self.body, str) else 0)
  237. )
  238. def __str__(self):
  239. self.prepare()
  240. protocol = self.protocol
  241. status = "{0:s}".format(self.status)
  242. return "{0:s} {1:s}\r\n".format(protocol, status)
  243. def __bytes__(self):
  244. return str(self).encode(self.encoding)
  245. def prepare(self):
  246. # Set a default content-Type if we don't have one.
  247. self.headers.setdefault(
  248. "Content-Type", "text/html; charset={0:s}".format(self.encoding)
  249. )
  250. cLength = None
  251. if self.body is not None:
  252. if isinstance(self.body, bytes):
  253. cLength = len(self.body)
  254. elif isinstance(self.body, unicode):
  255. cLength = len(self.body.encode(self.encoding))
  256. elif isinstance(self.body, list):
  257. cLength = sum(
  258. [
  259. len(s.encode(self.encoding))
  260. if not isinstance(s, bytes)
  261. else len(s) for s in self.body
  262. if s is not None
  263. ]
  264. )
  265. if cLength is not None:
  266. self.headers["Content-Length"] = str(cLength)
  267. for k, v in self.cookie.items():
  268. self.headers.add_header("Set-Cookie", v.OutputString())
  269. status = self.status
  270. if status == 413:
  271. self.close = True
  272. elif "Content-Length" not in self.headers:
  273. if status < 200 or status in (204, 205, 304):
  274. pass
  275. else:
  276. if self.protocol == "HTTP/1.1" \
  277. and self.request.method != "HEAD" \
  278. and self.request.server is not None \
  279. and not cLength == 0:
  280. self.chunked = True
  281. self.headers.add_header("Transfer-Encoding", "chunked")
  282. else:
  283. self.close = True
  284. if (self.request.server is not None
  285. and "Connection" not in self.headers):
  286. if self.protocol == "HTTP/1.1":
  287. if self.close:
  288. self.headers.add_header("Connection", "close")
  289. else:
  290. if not self.close:
  291. self.headers.add_header("Connection", "Keep-Alive")
  292. if self.headers.get("Transfer-Encoding", "") == "chunked":
  293. self.chunked = True