1
2
3
4
5 import base64
6 import errno
7 import logging
8 import os
9 import time
10 import socket
11 import ssl
12 import traceback
13 import types
14 import urlparse
15
16 try:
17 from http_parser.http import (
18 HttpStream, BadStatusLine, NoMoreData
19 )
20 from http_parser.reader import SocketReader
21 except ImportError:
22 raise ImportError("""http-parser isn't installed or out of data.
23
24 pip install http-parser""")
25
26 from restkit import __version__
27
28 from restkit.conn import Connection
29 from restkit.errors import RequestError, RequestTimeout, RedirectLimit, \
30 ProxyError
31 from restkit.session import get_session
32 from restkit.util import parse_netloc, rewrite_location, to_bytestring
33 from restkit.wrappers import Request, Response
34
35 MAX_CLIENT_TIMEOUT=300
36 MAX_CLIENT_CONNECTIONS = 5
37 MAX_CLIENT_TRIES =3
38 CLIENT_WAIT_TRIES = 0.3
39 MAX_FOLLOW_REDIRECTS = 5
40 USER_AGENT = "restkit/%s" % __version__
41
42 log = logging.getLogger(__name__)
43
45
46 """ A client handle a connection at a time. A client is threadsafe,
47 but an handled shouldn't be shared between threads. All connections
48 are shared between threads via a pool.
49
50 >>> from restkit import *
51 >>> c = Client()
52 >>> r = c.request("http://google.com")
53 r>>> r.status
54 '301 Moved Permanently'
55 >>> r.body_string()
56 '<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">\n<TITLE>301 Moved</TITLE></HEAD><BODY>\n<H1>301 Moved</H1>\nThe document has moved\n<A HREF="http://www.google.com/">here</A>.\r\n</BODY></HTML>\r\n'
57 >>> c.follow_redirect = True
58 >>> r = c.request("http://google.com")
59 >>> r.status
60 '200 OK'
61
62 """
63
64 version = (1, 1)
65 response_class=Response
66
67 - def __init__(self,
68 follow_redirect=False,
69 force_follow_redirect=False,
70 max_follow_redirect=MAX_FOLLOW_REDIRECTS,
71 filters=None,
72 decompress=True,
73 max_status_line_garbage=None,
74 max_header_count=0,
75 pool=None,
76 response_class=None,
77 timeout=None,
78 use_proxy=False,
79 max_tries=3,
80 wait_tries=0.3,
81 pool_size=10,
82 backend="thread",
83 **ssl_args):
84 """
85 Client parameters
86 ~~~~~~~~~~~~~~~~~
87
88 :param follow_redirect: follow redirection, by default False
89 :param max_ollow_redirect: number of redirections available
90 :filters: http filters to pass
91 :param decompress: allows the client to decompress the response
92 body
93 :param max_status_line_garbage: defines the maximum number of ignorable
94 lines before we expect a HTTP response's status line. With
95 HTTP/1.1 persistent connections, the problem arises that broken
96 scripts could return a wrong Content-Length (there are more
97 bytes sent than specified). Unfortunately, in some cases, this
98 cannot be detected after the bad response, but only before the
99 next one. So the client is abble to skip bad lines using this
100 limit. 0 disable garbage collection, None means unlimited number
101 of tries.
102 :param max_header_count: determines the maximum HTTP header count
103 allowed. by default no limit.
104 :param pool: the pool to use inherited from socketpool.Pool. By
105 default we use the global one.
106 :param response_class: the response class to use
107 :param timeout: the default timeout of the connection
108 (SO_TIMEOUT)
109
110 :param max_tries: the number of tries before we give up a
111 connection
112 :param wait_tries: number of time we wait between each tries.
113 :attr pool_size: int, default 10. Maximum number of connections we
114 keep in the default pool.
115 :param ssl_args: named argument, see ssl module for more
116 informations
117 """
118 self.follow_redirect = follow_redirect
119 self.force_follow_redirect = force_follow_redirect
120 self.max_follow_redirect = max_follow_redirect
121 self.decompress = decompress
122 self.filters = filters or []
123 self.max_status_line_garbage = max_status_line_garbage
124 self.max_header_count = max_header_count
125 self.use_proxy = use_proxy
126
127 self.request_filters = []
128 self.response_filters = []
129 self.load_filters()
130
131
132
133
134 session_options = dict(
135 retry_delay=wait_tries,
136 max_size = pool_size,
137 retry_max = max_tries,
138 timeout = timeout)
139
140
141 if pool is None:
142 pool = get_session(backend, **session_options)
143 self._pool = pool
144 self.backend = backend
145
146
147 if response_class is not None:
148 self.response_class = response_class
149
150 self.max_tries = max_tries
151 self.wait_tries = wait_tries
152 self.pool_size = pool_size
153 self.timeout = timeout
154
155 self._nb_redirections = self.max_follow_redirect
156 self._url = None
157 self._initial_url = None
158 self._write_cb = None
159 self._headers = None
160 self._sock_key = None
161 self._sock = None
162 self._original = None
163
164 self.method = 'GET'
165 self.body = None
166 self.ssl_args = ssl_args or {}
167
169 """ Populate filters from self.filters.
170 Must be called each time self.filters is updated.
171 """
172 for f in self.filters:
173 if hasattr(f, "on_request"):
174 self.request_filters.append(f)
175 if hasattr(f, "on_response"):
176 self.response_filters.append(f)
177
178
179
198
200 """ do the proxy connection """
201 proxy_settings = os.environ.get('%s_proxy' %
202 request.parsed_url.scheme)
203
204 if proxy_settings and proxy_settings is not None:
205 request.is_proxied = True
206
207 proxy_settings, proxy_auth = _get_proxy_auth(proxy_settings)
208 addr = parse_netloc(urlparse.urlparse(proxy_settings))
209
210 if is_ssl:
211 if proxy_auth:
212 proxy_auth = 'Proxy-authorization: %s' % proxy_auth
213 proxy_connect = 'CONNECT %s:%s HTTP/1.0\r\n' % req_addr
214
215 user_agent = request.headers.iget('user_agent')
216 if not user_agent:
217 user_agent = "User-Agent: restkit/%s\r\n" % __version__
218
219 proxy_pieces = '%s%s%s\r\n' % (proxy_connect, proxy_auth,
220 user_agent)
221
222 conn = self._pool.get(host=addr[0], port=addr[1],
223 pool=self._pool, is_ssl=is_ssl,
224 extra_headers=[], proxy_pieces=proxy_pieces, **self.ssl_args)
225 else:
226 headers = []
227 if proxy_auth:
228 headers = [('Proxy-authorization', proxy_auth)]
229
230 conn = self._pool.get(host=addr[0], port=addr[1],
231 pool=self._pool, is_ssl=False,
232 extra_headers=[], **self.ssl_args)
233 return conn
234
235 return
236
238 """ create final header string """
239 headers = request.headers.copy()
240 if extra_headers is not None:
241 for k, v in extra_headers:
242 headers[k] = v
243
244 if not request.body and request.method in ('POST', 'PUT',):
245 headers['Content-Length'] = 0
246
247 if self.version == (1,1):
248 httpver = "HTTP/1.1"
249 else:
250 httpver = "HTTP/1.0"
251
252 ua = headers.iget('user-agent')
253 if not ua:
254 ua = USER_AGENT
255 host = request.host
256
257 accept_encoding = headers.iget('accept-encoding')
258 if not accept_encoding:
259 accept_encoding = 'identity'
260
261 if request.is_proxied:
262 full_path = ("https://" if request.is_ssl() else "http://") + request.host + request.path
263 else:
264 full_path = request.path
265
266 lheaders = [
267 "%s %s %s\r\n" % (request.method, full_path, httpver),
268 "Host: %s\r\n" % host,
269 "User-Agent: %s\r\n" % ua,
270 "Accept-Encoding: %s\r\n" % accept_encoding
271 ]
272
273 lheaders.extend(["%s: %s\r\n" % (k, str(v)) for k, v in \
274 headers.items() if k.lower() not in \
275 ('user-agent', 'host', 'accept-encoding',)])
276 if log.isEnabledFor(logging.DEBUG):
277 log.debug("Send headers: %s" % lheaders)
278 return "%s\r\n" % "".join(lheaders)
279
401
402 - def request(self, url, method='GET', body=None, headers=None):
420
422 """ reset request, set new url of request and perform it """
423 if self._nb_redirections <= 0:
424 raise RedirectLimit("Redirection limit is reached")
425
426 if request.initial_url is None:
427 request.initial_url = self.url
428
429
430 location = rewrite_location(request.url, location)
431
432 if log.isEnabledFor(logging.DEBUG):
433 log.debug("Redirect to %s" % location)
434
435
436 request.url = location
437
438 self._nb_redirections -= 1
439
440
441 return self.perform(request)
442
444 """ return final respons, it is only accessible via peform
445 method """
446 if log.isEnabledFor(logging.DEBUG):
447 log.debug("Start to parse response")
448
449 p = HttpStream(SocketReader(connection.socket()), kind=1,
450 decompress=self.decompress)
451
452 if log.isEnabledFor(logging.DEBUG):
453 log.debug("Got response: %s %s" % (p.version(), p.status()))
454 log.debug("headers: [%s]" % p.headers())
455
456 location = p.headers().get('location')
457
458 if self.follow_redirect:
459 should_close = not p.should_keep_alive()
460 if p.status_code() in (301, 302, 307,):
461
462
463 p.body_file().read()
464 connection.release(should_close)
465
466 if request.method in ('GET', 'HEAD',) or \
467 self.force_follow_redirect:
468 if hasattr(self.body, 'read'):
469 try:
470 self.body.seek(0)
471 except AttributeError:
472 raise RequestError("Can't redirect %s to %s "
473 "because body has already been read"
474 % (self.url, location))
475 return self.redirect(location, request)
476
477 elif p.status_code() == 303 and self.method == "POST":
478
479 p.body_file().read()
480 connection.release(should_close)
481
482 request.method = "GET"
483 request.body = None
484 return self.redirect(location, request)
485
486
487 resp = self.response_class(connection, request, p)
488
489
490 for f in self.response_filters:
491 f.on_response(resp, request)
492
493 if log.isEnabledFor(logging.DEBUG):
494 log.debug("return response class")
495
496
497 return resp
498
499
501 proxy_username = os.environ.get('proxy-username')
502 if not proxy_username:
503 proxy_username = os.environ.get('proxy_username')
504 proxy_password = os.environ.get('proxy-password')
505 if not proxy_password:
506 proxy_password = os.environ.get('proxy_password')
507
508 proxy_password = proxy_password or ""
509
510 if not proxy_username:
511 u = urlparse.urlparse(proxy_settings)
512 if u.username:
513 proxy_password = u.password or proxy_password
514 proxy_settings = urlparse.urlunparse((u.scheme,
515 u.netloc.split("@")[-1], u.path, u.params, u.query,
516 u.fragment))
517
518 if proxy_username:
519 user_auth = base64.encodestring('%s:%s' % (proxy_username,
520 proxy_password))
521 return proxy_settings, 'Basic %s\r\n' % (user_auth.strip())
522 else:
523 return proxy_settings, ''
524