gvsig-scripting / org.gvsig.scripting / trunk / org.gvsig.scripting / org.gvsig.scripting.app / org.gvsig.scripting.app.mainplugin / src / main / resources-plugin / scripting / lib / oauthlib / oauth1 / rfc5849 / __init__.py @ 564
History | View | Annotate | Download (14.8 KB)
1 |
# -*- coding: utf-8 -*-
|
---|---|
2 |
"""
|
3 |
oauthlib.oauth1.rfc5849
|
4 |
~~~~~~~~~~~~~~
|
5 |
|
6 |
This module is an implementation of various logic needed
|
7 |
for signing and checking OAuth 1.0 RFC 5849 requests.
|
8 |
"""
|
9 |
from __future__ import absolute_import, unicode_literals |
10 |
import base64 |
11 |
import hashlib |
12 |
import logging |
13 |
log = logging.getLogger(__name__) |
14 |
|
15 |
import sys |
16 |
try:
|
17 |
import urlparse |
18 |
except ImportError: |
19 |
import urllib.parse as urlparse |
20 |
|
21 |
if sys.version_info[0] == 3: |
22 |
bytes_type = bytes
|
23 |
else:
|
24 |
bytes_type = str
|
25 |
|
26 |
from oauthlib.common import Request, urlencode, generate_nonce |
27 |
from oauthlib.common import generate_timestamp, to_unicode |
28 |
from . import parameters, signature
|
29 |
|
30 |
SIGNATURE_HMAC = "HMAC-SHA1"
|
31 |
SIGNATURE_RSA = "RSA-SHA1"
|
32 |
SIGNATURE_PLAINTEXT = "PLAINTEXT"
|
33 |
SIGNATURE_METHODS = (SIGNATURE_HMAC, SIGNATURE_RSA, SIGNATURE_PLAINTEXT) |
34 |
|
35 |
SIGNATURE_TYPE_AUTH_HEADER = 'AUTH_HEADER'
|
36 |
SIGNATURE_TYPE_QUERY = 'QUERY'
|
37 |
SIGNATURE_TYPE_BODY = 'BODY'
|
38 |
|
39 |
CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'
|
40 |
|
41 |
|
42 |
class Client(object): |
43 |
|
44 |
"""A client used to sign OAuth 1.0 RFC 5849 requests."""
|
45 |
SIGNATURE_METHODS = { |
46 |
SIGNATURE_HMAC: signature.sign_hmac_sha1_with_client, |
47 |
SIGNATURE_RSA: signature.sign_rsa_sha1_with_client, |
48 |
SIGNATURE_PLAINTEXT: signature.sign_plaintext_with_client |
49 |
} |
50 |
|
51 |
@classmethod
|
52 |
def register_signature_method(cls, method_name, method_callback): |
53 |
cls.SIGNATURE_METHODS[method_name] = method_callback |
54 |
|
55 |
def __init__(self, client_key, |
56 |
client_secret=None,
|
57 |
resource_owner_key=None,
|
58 |
resource_owner_secret=None,
|
59 |
callback_uri=None,
|
60 |
signature_method=SIGNATURE_HMAC, |
61 |
signature_type=SIGNATURE_TYPE_AUTH_HEADER, |
62 |
rsa_key=None, verifier=None, realm=None, |
63 |
encoding='utf-8', decoding=None, |
64 |
nonce=None, timestamp=None): |
65 |
"""Create an OAuth 1 client.
|
66 |
|
67 |
:param client_key: Client key (consumer key), mandatory.
|
68 |
:param resource_owner_key: Resource owner key (oauth token).
|
69 |
:param resource_owner_secret: Resource owner secret (oauth token secret).
|
70 |
:param callback_uri: Callback used when obtaining request token.
|
71 |
:param signature_method: SIGNATURE_HMAC, SIGNATURE_RSA or SIGNATURE_PLAINTEXT.
|
72 |
:param signature_type: SIGNATURE_TYPE_AUTH_HEADER (default),
|
73 |
SIGNATURE_TYPE_QUERY or SIGNATURE_TYPE_BODY
|
74 |
depending on where you want to embed the oauth
|
75 |
credentials.
|
76 |
:param rsa_key: RSA key used with SIGNATURE_RSA.
|
77 |
:param verifier: Verifier used when obtaining an access token.
|
78 |
:param realm: Realm (scope) to which access is being requested.
|
79 |
:param encoding: If you provide non-unicode input you may use this
|
80 |
to have oauthlib automatically convert.
|
81 |
:param decoding: If you wish that the returned uri, headers and body
|
82 |
from sign be encoded back from unicode, then set
|
83 |
decoding to your preferred encoding, i.e. utf-8.
|
84 |
:param nonce: Use this nonce instead of generating one. (Mainly for testing)
|
85 |
:param timestamp: Use this timestamp instead of using current. (Mainly for testing)
|
86 |
"""
|
87 |
# Convert to unicode using encoding if given, else assume unicode
|
88 |
encode = lambda x: to_unicode(x, encoding) if encoding else x |
89 |
|
90 |
self.client_key = encode(client_key)
|
91 |
self.client_secret = encode(client_secret)
|
92 |
self.resource_owner_key = encode(resource_owner_key)
|
93 |
self.resource_owner_secret = encode(resource_owner_secret)
|
94 |
self.signature_method = encode(signature_method)
|
95 |
self.signature_type = encode(signature_type)
|
96 |
self.callback_uri = encode(callback_uri)
|
97 |
self.rsa_key = encode(rsa_key)
|
98 |
self.verifier = encode(verifier)
|
99 |
self.realm = encode(realm)
|
100 |
self.encoding = encode(encoding)
|
101 |
self.decoding = encode(decoding)
|
102 |
self.nonce = encode(nonce)
|
103 |
self.timestamp = encode(timestamp)
|
104 |
|
105 |
def __repr__(self): |
106 |
attrs = vars(self).copy() |
107 |
attrs['client_secret'] = '****' if attrs['client_secret'] else None |
108 |
attrs[ |
109 |
'resource_owner_secret'] = '****' if attrs['resource_owner_secret'] else None |
110 |
attribute_str = ', '.join('%s=%s' % (k, v) for k, v in attrs.items()) |
111 |
return '<%s %s>' % (self.__class__.__name__, attribute_str) |
112 |
|
113 |
def get_oauth_signature(self, request): |
114 |
"""Get an OAuth signature to be used in signing a request
|
115 |
|
116 |
To satisfy `section 3.4.1.2`_ item 2, if the request argument's
|
117 |
headers dict attribute contains a Host item, its value will
|
118 |
replace any netloc part of the request argument's uri attribute
|
119 |
value.
|
120 |
|
121 |
.. _`section 3.4.1.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2
|
122 |
"""
|
123 |
if self.signature_method == SIGNATURE_PLAINTEXT: |
124 |
# fast-path
|
125 |
return signature.sign_plaintext(self.client_secret, |
126 |
self.resource_owner_secret)
|
127 |
|
128 |
uri, headers, body = self._render(request)
|
129 |
|
130 |
collected_params = signature.collect_parameters( |
131 |
uri_query=urlparse.urlparse(uri).query, |
132 |
body=body, |
133 |
headers=headers) |
134 |
log.debug("Collected params: {0}".format(collected_params))
|
135 |
|
136 |
normalized_params = signature.normalize_parameters(collected_params) |
137 |
normalized_uri = signature.normalize_base_string_uri(uri, |
138 |
headers.get('Host', None)) |
139 |
log.debug("Normalized params: {0}".format(normalized_params))
|
140 |
log.debug("Normalized URI: {0}".format(normalized_uri))
|
141 |
|
142 |
base_string = signature.construct_base_string(request.http_method, |
143 |
normalized_uri, normalized_params) |
144 |
|
145 |
log.debug("Base signing string: {0}".format(base_string))
|
146 |
|
147 |
if self.signature_method not in self.SIGNATURE_METHODS: |
148 |
raise ValueError('Invalid signature method.') |
149 |
|
150 |
sig = self.SIGNATURE_METHODS[self.signature_method](base_string, self) |
151 |
|
152 |
log.debug("Signature: {0}".format(sig))
|
153 |
return sig
|
154 |
|
155 |
def get_oauth_params(self, request): |
156 |
"""Get the basic OAuth parameters to be used in generating a signature.
|
157 |
"""
|
158 |
nonce = (generate_nonce() |
159 |
if self.nonce is None else self.nonce) |
160 |
timestamp = (generate_timestamp() |
161 |
if self.timestamp is None else self.timestamp) |
162 |
params = [ |
163 |
('oauth_nonce', nonce),
|
164 |
('oauth_timestamp', timestamp),
|
165 |
('oauth_version', '1.0'), |
166 |
('oauth_signature_method', self.signature_method), |
167 |
('oauth_consumer_key', self.client_key), |
168 |
] |
169 |
if self.resource_owner_key: |
170 |
params.append(('oauth_token', self.resource_owner_key)) |
171 |
if self.callback_uri: |
172 |
params.append(('oauth_callback', self.callback_uri)) |
173 |
if self.verifier: |
174 |
params.append(('oauth_verifier', self.verifier)) |
175 |
|
176 |
# providing body hash for requests other than x-www-form-urlencoded
|
177 |
# as described in http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html
|
178 |
# 4.1.1. When to include the body hash
|
179 |
# * [...] MUST NOT include an oauth_body_hash parameter on requests with form-encoded request bodies
|
180 |
# * [...] SHOULD include the oauth_body_hash parameter on all other requests.
|
181 |
content_type = request.headers.get('Content-Type', None) |
182 |
content_type_eligible = content_type and content_type.find('application/x-www-form-urlencoded') < 0 |
183 |
if request.body is not None and content_type_eligible: |
184 |
params.append(('oauth_body_hash', base64.b64encode(hashlib.sha1(request.body.encode('utf-8')).digest()).decode('utf-8'))) |
185 |
|
186 |
return params
|
187 |
|
188 |
def _render(self, request, formencode=False, realm=None): |
189 |
"""Render a signed request according to signature type
|
190 |
|
191 |
Returns a 3-tuple containing the request URI, headers, and body.
|
192 |
|
193 |
If the formencode argument is True and the body contains parameters, it
|
194 |
is escaped and returned as a valid formencoded string.
|
195 |
"""
|
196 |
# TODO what if there are body params on a header-type auth?
|
197 |
# TODO what if there are query params on a body-type auth?
|
198 |
|
199 |
uri, headers, body = request.uri, request.headers, request.body |
200 |
|
201 |
# TODO: right now these prepare_* methods are very narrow in scope--they
|
202 |
# only affect their little thing. In some cases (for example, with
|
203 |
# header auth) it might be advantageous to allow these methods to touch
|
204 |
# other parts of the request, like the headers—so the prepare_headers
|
205 |
# method could also set the Content-Type header to x-www-form-urlencoded
|
206 |
# like the spec requires. This would be a fundamental change though, and
|
207 |
# I'm not sure how I feel about it.
|
208 |
if self.signature_type == SIGNATURE_TYPE_AUTH_HEADER: |
209 |
headers = parameters.prepare_headers( |
210 |
request.oauth_params, request.headers, realm=realm) |
211 |
elif self.signature_type == SIGNATURE_TYPE_BODY and request.decoded_body is not None: |
212 |
body = parameters.prepare_form_encoded_body( |
213 |
request.oauth_params, request.decoded_body) |
214 |
if formencode:
|
215 |
body = urlencode(body) |
216 |
headers['Content-Type'] = 'application/x-www-form-urlencoded' |
217 |
elif self.signature_type == SIGNATURE_TYPE_QUERY: |
218 |
uri = parameters.prepare_request_uri_query( |
219 |
request.oauth_params, request.uri) |
220 |
else:
|
221 |
raise ValueError('Unknown signature type specified.') |
222 |
|
223 |
return uri, headers, body
|
224 |
|
225 |
def sign(self, uri, http_method='GET', body=None, headers=None, realm=None): |
226 |
"""Sign a request
|
227 |
|
228 |
Signs an HTTP request with the specified parts.
|
229 |
|
230 |
Returns a 3-tuple of the signed request's URI, headers, and body.
|
231 |
Note that http_method is not returned as it is unaffected by the OAuth
|
232 |
signing process. Also worth noting is that duplicate parameters
|
233 |
will be included in the signature, regardless of where they are
|
234 |
specified (query, body).
|
235 |
|
236 |
The body argument may be a dict, a list of 2-tuples, or a formencoded
|
237 |
string. The Content-Type header must be 'application/x-www-form-urlencoded'
|
238 |
if it is present.
|
239 |
|
240 |
If the body argument is not one of the above, it will be returned
|
241 |
verbatim as it is unaffected by the OAuth signing process. Attempting to
|
242 |
sign a request with non-formencoded data using the OAuth body signature
|
243 |
type is invalid and will raise an exception.
|
244 |
|
245 |
If the body does contain parameters, it will be returned as a properly-
|
246 |
formatted formencoded string.
|
247 |
|
248 |
Body may not be included if the http_method is either GET or HEAD as
|
249 |
this changes the semantic meaning of the request.
|
250 |
|
251 |
All string data MUST be unicode or be encoded with the same encoding
|
252 |
scheme supplied to the Client constructor, default utf-8. This includes
|
253 |
strings inside body dicts, for example.
|
254 |
"""
|
255 |
# normalize request data
|
256 |
request = Request(uri, http_method, body, headers, |
257 |
encoding=self.encoding)
|
258 |
|
259 |
# sanity check
|
260 |
content_type = request.headers.get('Content-Type', None) |
261 |
multipart = content_type and content_type.startswith('multipart/') |
262 |
should_have_params = content_type == CONTENT_TYPE_FORM_URLENCODED |
263 |
has_params = request.decoded_body is not None |
264 |
# 3.4.1.3.1. Parameter Sources
|
265 |
# [Parameters are collected from the HTTP request entity-body, but only
|
266 |
# if [...]:
|
267 |
# * The entity-body is single-part.
|
268 |
if multipart and has_params: |
269 |
raise ValueError( |
270 |
"Headers indicate a multipart body but body contains parameters.")
|
271 |
# * The entity-body follows the encoding requirements of the
|
272 |
# "application/x-www-form-urlencoded" content-type as defined by
|
273 |
# [W3C.REC-html40-19980424].
|
274 |
elif should_have_params and not has_params: |
275 |
raise ValueError( |
276 |
"Headers indicate a formencoded body but body was not decodable.")
|
277 |
# * The HTTP request entity-header includes the "Content-Type"
|
278 |
# header field set to "application/x-www-form-urlencoded".
|
279 |
elif not should_have_params and has_params: |
280 |
raise ValueError( |
281 |
"Body contains parameters but Content-Type header was {0} "
|
282 |
"instead of {1}".format(content_type or "not set", |
283 |
CONTENT_TYPE_FORM_URLENCODED)) |
284 |
|
285 |
# 3.5.2. Form-Encoded Body
|
286 |
# Protocol parameters can be transmitted in the HTTP request entity-
|
287 |
# body, but only if the following REQUIRED conditions are met:
|
288 |
# o The entity-body is single-part.
|
289 |
# o The entity-body follows the encoding requirements of the
|
290 |
# "application/x-www-form-urlencoded" content-type as defined by
|
291 |
# [W3C.REC-html40-19980424].
|
292 |
# o The HTTP request entity-header includes the "Content-Type" header
|
293 |
# field set to "application/x-www-form-urlencoded".
|
294 |
elif self.signature_type == SIGNATURE_TYPE_BODY and not ( |
295 |
should_have_params and has_params and not multipart): |
296 |
raise ValueError( |
297 |
'Body signatures may only be used with form-urlencoded content')
|
298 |
|
299 |
# We amend http://tools.ietf.org/html/rfc5849#section-3.4.1.3.1
|
300 |
# with the clause that parameters from body should only be included
|
301 |
# in non GET or HEAD requests. Extracting the request body parameters
|
302 |
# and including them in the signature base string would give semantic
|
303 |
# meaning to the body, which it should not have according to the
|
304 |
# HTTP 1.1 spec.
|
305 |
elif http_method.upper() in ('GET', 'HEAD') and has_params: |
306 |
raise ValueError('GET/HEAD requests should not include body.') |
307 |
|
308 |
# generate the basic OAuth parameters
|
309 |
request.oauth_params = self.get_oauth_params(request)
|
310 |
|
311 |
# generate the signature
|
312 |
request.oauth_params.append( |
313 |
('oauth_signature', self.get_oauth_signature(request))) |
314 |
|
315 |
# render the signed request and return it
|
316 |
uri, headers, body = self._render(request, formencode=True, |
317 |
realm=(realm or self.realm)) |
318 |
|
319 |
if self.decoding: |
320 |
log.debug('Encoding URI, headers and body to %s.', self.decoding) |
321 |
uri = uri.encode(self.decoding)
|
322 |
body = body.encode(self.decoding) if body else body |
323 |
new_headers = {} |
324 |
for k, v in headers.items(): |
325 |
new_headers[k.encode(self.decoding)] = v.encode(self.decoding) |
326 |
headers = new_headers |
327 |
return uri, headers, body
|