Statistics
| Revision:

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