PageRenderTime 44ms CodeModel.GetById 1ms app.highlight 36ms RepoModel.GetById 2ms app.codeStats 0ms

/circuits/web/wrappers.py

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