1
# Copyright 2009 Canonical Ltd.
3
# This file is part of desktopcouch.
5
# desktopcouch is free software: you can redistribute it and/or modify
6
# it under the terms of the GNU Lesser General Public License version 3
7
# as published by the Free Software Foundation.
9
# desktopcouch is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU Lesser General Public License for more details.
14
# You should have received a copy of the GNU Lesser General Public License
15
# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
17
# Authors: Vincenzo Di Somma <vincenzo.di.somma@canonical.com>
19
"""This modules olds some code that should back ported to python-couchdb"""
29
from httplib import BadStatusLine
30
from urlparse import urlsplit, urlunsplit
32
from oauth import oauth
34
# pylint can't deal with failing imports even when they're handled
35
# pylint: disable=F0401
37
from cStringIO import StringIO
39
from StringIO import StringIO
41
# pylint: enable=F0401
43
from couchdb.http import (
44
Session, CHUNK_SIZE, CACHE_SIZE, RedirectLimit, ResponseBody, Unauthorized,
45
PreconditionFailed, ServerError, ResourceNotFound, ResourceConflict)
47
from couchdb import json as couchdbjson
49
NORMALIZE_SPACE = re.compile(r'(?:\r\n)?[ \t]+')
52
class OAuthSession(Session):
53
"""Session that can handle OAuth"""
55
def __init__(self, cache=None, timeout=None, max_redirects=5,
57
"""Initialize an HTTP client session with oauth credential. """
58
super(OAuthSession, self).__init__(cache=cache,
60
max_redirects=max_redirects)
61
self.credentials = credentials
63
def request(self, method, url, body=None, headers=None, credentials=None,
66
def normalize_headers(headers):
67
"""normalize the headers so oauth likes them"""
72
' ').strip()) for (key, value) in headers.iteritems()])
74
def oauth_sign(creds, url, method):
75
"""Sign the url with the tokens and return an header"""
76
consumer = oauth.OAuthConsumer(creds['consumer_key'],
77
creds['consumer_secret'])
78
access_token = oauth.OAuthToken(creds['token'],
79
creds['token_secret'])
80
sig_method = oauth.OAuthSignatureMethod_HMAC_SHA1
81
query = urlparse.urlparse(url)[4]
82
querystr_as_dict = dict(cgi.parse_qsl(query))
83
req = oauth.OAuthRequest.from_consumer_and_token(
88
parameters=querystr_as_dict)
89
req.sign_request(sig_method(), consumer, access_token)
90
return req.to_header()
92
if url in self.perm_redirects:
93
url = self.perm_redirects[url]
94
method = method.upper()
98
headers.setdefault('Accept', 'application/json')
99
headers['User-Agent'] = self.user_agent
102
if method in ('GET', 'HEAD'):
103
cached_resp = self.cache.get(url)
104
if cached_resp is not None:
105
etag = cached_resp[1].get('etag')
107
headers['If-None-Match'] = etag
110
if not isinstance(body, basestring):
112
body = couchdbjson.encode(body).encode('utf-8')
116
headers.setdefault('Content-Type', 'application/json')
117
if isinstance(body, basestring):
118
headers.setdefault('Content-Length', str(len(body)))
120
headers['Transfer-Encoding'] = 'chunked'
124
elif self.credentials:
125
creds = self.credentials
129
headers.update(normalize_headers(
130
oauth_sign(creds, url, method)))
132
path_query = urlunsplit(('', '') + urlsplit(url)[2:4] + ('',))
133
conn = self._get_connection(url)
135
def _try_request_with_retries(retries):
136
"""Retries the request if it fails for a socket problem"""
139
return _try_request()
140
except socket.error, e:
142
if ecode not in self.retryable_errors:
145
delay = retries.next()
146
except StopIteration:
147
# No more retries, raise last socket error.
153
"""Tries the request and handle socket problems"""
155
if conn.sock is None:
157
conn.putrequest(method, path_query, skip_accept_encoding=True)
158
for header in headers:
159
conn.putheader(header, headers[header])
162
if isinstance(body, str):
163
conn.sock.sendall(body)
164
else: # assume a file-like object and send in chunks
166
chunk = body.read(CHUNK_SIZE)
169
conn.sock.sendall(('%x\r\n' % len(chunk)) +
171
conn.sock.sendall('0\r\n\r\n')
172
return conn.getresponse()
173
except BadStatusLine, e:
174
# httplib raises a BadStatusLine when it cannot read the status
175
# line saying, "Presumably, the server closed the connection
176
# before sending a valid response."
177
# Raise as ECONNRESET to simplify retry logic.
178
if e.line == '' or e.line == "''":
179
raise socket.error(errno.ECONNRESET)
183
resp = _try_request_with_retries(iter(self.retry_delays))
186
# Handle conditional response
187
if status == 304 and method in ('GET', 'HEAD'):
189
self._return_connection(url, conn)
190
status, msg, data = cached_resp
192
data = StringIO(data)
193
return status, msg, data
198
if status == 303 or \
199
method in ('GET', 'HEAD') and status in (301, 302, 307):
201
self._return_connection(url, conn)
202
if num_redirects > self.max_redirects:
203
raise RedirectLimit('Redirection limit exceeded')
204
location = resp.getheader('location')
206
self.perm_redirects[url] = location
209
return self.request(method, location, body, headers,
210
num_redirects=num_redirects + 1)
215
# Read the full response for empty responses so that the connection is
216
# in good state for the next request
217
if method == 'HEAD' or resp.getheader('content-length') == '0' or \
218
status < 200 or status in (204, 304):
220
self._return_connection(url, conn)
222
# Buffer small non-JSON response bodies
223
elif int(resp.getheader('content-length', sys.maxint)) < CHUNK_SIZE:
225
self._return_connection(url, conn)
227
# For large or chunked response bodies, do not buffer the full body,
228
# and instead return a minimal file-like object
230
data = ResponseBody(resp,
231
lambda: self._return_connection(url, conn))
237
data = couchdbjson.decode(data)
238
# pylint: disable=E1103
239
error = data.get('error'), data.get('reason')
240
# pylint: enable=E1103
241
elif method != 'HEAD':
243
self._return_connection(url, conn)
247
raise Unauthorized(error)
249
raise ResourceNotFound(error)
251
raise ResourceConflict(error)
253
raise PreconditionFailed(error)
255
raise ServerError((status, error))
257
# Store cachable responses
258
if not streamed and method == 'GET' and 'etag' in resp.msg:
259
self.cache[url] = (status, resp.msg, data)
260
if len(self.cache) > CACHE_SIZE[1]:
263
if not streamed and data is not None:
264
data = StringIO(data)
266
return status, resp.msg, data