Revision 5a797434

/dev/null
1
"""
2
The MIT License
3

  
4
Copyright (c) 2007-2010 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel
5

  
6
Permission is hereby granted, free of charge, to any person obtaining a copy
7
of this software and associated documentation files (the "Software"), to deal
8
in the Software without restriction, including without limitation the rights
9
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
copies of the Software, and to permit persons to whom the Software is
11
furnished to do so, subject to the following conditions:
12

  
13
The above copyright notice and this permission notice shall be included in
14
all copies or substantial portions of the Software.
15

  
16
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
THE SOFTWARE.
23
"""
24

  
25
import base64
26
import urllib
27
import time
28
import random
29
import urlparse
30
import hmac
31
import binascii
32
import httplib2
33

  
34
try:
35
    from urlparse import parse_qs
36
    parse_qs # placate pyflakes
37
except ImportError:
38
    # fall back for Python 2.5
39
    from cgi import parse_qs
40

  
41
try:
42
    from hashlib import sha1
43
    sha = sha1
44
except ImportError:
45
    # hashlib was added in Python 2.5
46
    import sha
47

  
48
import _version
49

  
50
__version__ = _version.__version__
51

  
52
OAUTH_VERSION = '1.0'  # Hi Blaine!
53
HTTP_METHOD = 'GET'
54
SIGNATURE_METHOD = 'PLAINTEXT'
55

  
56

  
57
class Error(RuntimeError):
58
    """Generic exception class."""
59

  
60
    def __init__(self, message='OAuth error occurred.'):
61
        self._message = message
62

  
63
    @property
64
    def message(self):
65
        """A hack to get around the deprecation errors in 2.6."""
66
        return self._message
67

  
68
    def __str__(self):
69
        return self._message
70

  
71

  
72
class MissingSignature(Error):
73
    pass
74

  
75

  
76
def build_authenticate_header(realm=''):
77
    """Optional WWW-Authenticate header (401 error)"""
78
    return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
79

  
80

  
81
def build_xoauth_string(url, consumer, token=None):
82
    """Build an XOAUTH string for use in SMTP/IMPA authentication."""
83
    request = Request.from_consumer_and_token(consumer, token,
84
        "GET", url)
85

  
86
    signing_method = SignatureMethod_HMAC_SHA1()
87
    request.sign_request(signing_method, consumer, token)
88

  
89
    params = []
90
    for k, v in sorted(request.iteritems()):
91
        if v is not None:
92
            params.append('%s="%s"' % (k, escape(v)))
93

  
94
    return "%s %s %s" % ("GET", url, ','.join(params))
95

  
96

  
97
def to_unicode(s):
98
    """ Convert to unicode, raise exception with instructive error
99
    message if s is not unicode, ascii, or utf-8. """
100
    if not isinstance(s, unicode):
101
        if not isinstance(s, str):
102
            raise TypeError('You are required to pass either unicode or string here, not: %r (%s)' % (type(s), s))
103
        try:
104
            s = s.decode('utf-8')
105
        except UnicodeDecodeError, le:
106
            raise TypeError('You are required to pass either a unicode object or a utf-8 string here. You passed a Python string object which contained non-utf-8: %r. The UnicodeDecodeError that resulted from attempting to interpret it as utf-8 was: %s' % (s, le,))
107
    return s
108

  
109
def to_utf8(s):
110
    return to_unicode(s).encode('utf-8')
111

  
112
def to_unicode_if_string(s):
113
    if isinstance(s, basestring):
114
        return to_unicode(s)
115
    else:
116
        return s
117

  
118
def to_utf8_if_string(s):
119
    if isinstance(s, basestring):
120
        return to_utf8(s)
121
    else:
122
        return s
123

  
124
def to_unicode_optional_iterator(x):
125
    """
126
    Raise TypeError if x is a str containing non-utf8 bytes or if x is
127
    an iterable which contains such a str.
128
    """
129
    if isinstance(x, basestring):
130
        return to_unicode(x)
131

  
132
    try:
133
        l = list(x)
134
    except TypeError, e:
135
        assert 'is not iterable' in str(e)
136
        return x
137
    else:
138
        return [ to_unicode(e) for e in l ]
139

  
140
def to_utf8_optional_iterator(x):
141
    """
142
    Raise TypeError if x is a str or if x is an iterable which
143
    contains a str.
144
    """
145
    if isinstance(x, basestring):
146
        return to_utf8(x)
147

  
148
    try:
149
        l = list(x)
150
    except TypeError, e:
151
        assert 'is not iterable' in str(e)
152
        return x
153
    else:
154
        return [ to_utf8_if_string(e) for e in l ]
155

  
156
def escape(s):
157
    """Escape a URL including any /."""
158
    return urllib.quote(s.encode('utf-8'), safe='~')
159

  
160
def generate_timestamp():
161
    """Get seconds since epoch (UTC)."""
162
    return int(time.time())
163

  
164

  
165
def generate_nonce(length=8):
166
    """Generate pseudorandom number."""
167
    return ''.join([str(random.randint(0, 9)) for i in range(length)])
168

  
169

  
170
def generate_verifier(length=8):
171
    """Generate pseudorandom number."""
172
    return ''.join([str(random.randint(0, 9)) for i in range(length)])
173

  
174

  
175
class Consumer(object):
176
    """A consumer of OAuth-protected services.
177
 
178
    The OAuth consumer is a "third-party" service that wants to access
179
    protected resources from an OAuth service provider on behalf of an end
180
    user. It's kind of the OAuth client.
181
 
182
    Usually a consumer must be registered with the service provider by the
183
    developer of the consumer software. As part of that process, the service
184
    provider gives the consumer a *key* and a *secret* with which the consumer
185
    software can identify itself to the service. The consumer will include its
186
    key in each request to identify itself, but will use its secret only when
187
    signing requests, to prove that the request is from that particular
188
    registered consumer.
189
 
190
    Once registered, the consumer can then use its consumer credentials to ask
191
    the service provider for a request token, kicking off the OAuth
192
    authorization process.
193
    """
194

  
195
    key = None
196
    secret = None
197

  
198
    def __init__(self, key, secret):
199
        self.key = key
200
        self.secret = secret
201

  
202
        if self.key is None or self.secret is None:
203
            raise ValueError("Key and secret must be set.")
204

  
205
    def __str__(self):
206
        data = {'oauth_consumer_key': self.key,
207
            'oauth_consumer_secret': self.secret}
208

  
209
        return urllib.urlencode(data)
210

  
211

  
212
class Token(object):
213
    """An OAuth credential used to request authorization or a protected
214
    resource.
215
 
216
    Tokens in OAuth comprise a *key* and a *secret*. The key is included in
217
    requests to identify the token being used, but the secret is used only in
218
    the signature, to prove that the requester is who the server gave the
219
    token to.
220
 
221
    When first negotiating the authorization, the consumer asks for a *request
222
    token* that the live user authorizes with the service provider. The
223
    consumer then exchanges the request token for an *access token* that can
224
    be used to access protected resources.
225
    """
226

  
227
    key = None
228
    secret = None
229
    callback = None
230
    callback_confirmed = None
231
    verifier = None
232

  
233
    def __init__(self, key, secret):
234
        self.key = key
235
        self.secret = secret
236

  
237
        if self.key is None or self.secret is None:
238
            raise ValueError("Key and secret must be set.")
239

  
240
    def set_callback(self, callback):
241
        self.callback = callback
242
        self.callback_confirmed = 'true'
243

  
244
    def set_verifier(self, verifier=None):
245
        if verifier is not None:
246
            self.verifier = verifier
247
        else:
248
            self.verifier = generate_verifier()
249

  
250
    def get_callback_url(self):
251
        if self.callback and self.verifier:
252
            # Append the oauth_verifier.
253
            parts = urlparse.urlparse(self.callback)
254
            scheme, netloc, path, params, query, fragment = parts[:6]
255
            if query:
256
                query = '%s&oauth_verifier=%s' % (query, self.verifier)
257
            else:
258
                query = 'oauth_verifier=%s' % self.verifier
259
            return urlparse.urlunparse((scheme, netloc, path, params,
260
                query, fragment))
261
        return self.callback
262

  
263
    def to_string(self):
264
        """Returns this token as a plain string, suitable for storage.
265
 
266
        The resulting string includes the token's secret, so you should never
267
        send or store this string where a third party can read it.
268
        """
269

  
270
        data = {
271
            'oauth_token': self.key,
272
            'oauth_token_secret': self.secret,
273
        }
274

  
275
        if self.callback_confirmed is not None:
276
            data['oauth_callback_confirmed'] = self.callback_confirmed
277
        return urllib.urlencode(data)
278
 
279
    @staticmethod
280
    def from_string(s):
281
        """Deserializes a token from a string like one returned by
282
        `to_string()`."""
283

  
284
        if not len(s):
285
            raise ValueError("Invalid parameter string.")
286

  
287
        params = parse_qs(s, keep_blank_values=False)
288
        if not len(params):
289
            raise ValueError("Invalid parameter string.")
290

  
291
        try:
292
            key = params['oauth_token'][0]
293
        except Exception:
294
            raise ValueError("'oauth_token' not found in OAuth request.")
295

  
296
        try:
297
            secret = params['oauth_token_secret'][0]
298
        except Exception:
299
            raise ValueError("'oauth_token_secret' not found in " 
300
                "OAuth request.")
301

  
302
        token = Token(key, secret)
303
        try:
304
            token.callback_confirmed = params['oauth_callback_confirmed'][0]
305
        except KeyError:
306
            pass  # 1.0, no callback confirmed.
307
        return token
308

  
309
    def __str__(self):
310
        return self.to_string()
311

  
312

  
313
def setter(attr):
314
    name = attr.__name__
315
 
316
    def getter(self):
317
        try:
318
            return self.__dict__[name]
319
        except KeyError:
320
            raise AttributeError(name)
321
 
322
    def deleter(self):
323
        del self.__dict__[name]
324
 
325
    return property(getter, attr, deleter)
326

  
327

  
328
class Request(dict):
329
 
330
    """The parameters and information for an HTTP request, suitable for
331
    authorizing with OAuth credentials.
332
 
333
    When a consumer wants to access a service's protected resources, it does
334
    so using a signed HTTP request identifying itself (the consumer) with its
335
    key, and providing an access token authorized by the end user to access
336
    those resources.
337
 
338
    """
339
 
340
    version = OAUTH_VERSION
341

  
342
    def __init__(self, method=HTTP_METHOD, url=None, parameters=None,
343
                 body='', is_form_encoded=False):
344
        if url is not None:
345
            self.url = to_unicode(url)
346
        self.method = method
347
        if parameters is not None:
348
            for k, v in parameters.iteritems():
349
                k = to_unicode(k)
350
                v = to_unicode_optional_iterator(v)
351
                self[k] = v
352
        self.body = body
353
        self.is_form_encoded = is_form_encoded
354

  
355

  
356
    @setter
357
    def url(self, value):
358
        self.__dict__['url'] = value
359
        if value is not None:
360
            scheme, netloc, path, params, query, fragment = urlparse.urlparse(value)
361

  
362
            # Exclude default port numbers.
363
            if scheme == 'http' and netloc[-3:] == ':80':
364
                netloc = netloc[:-3]
365
            elif scheme == 'https' and netloc[-4:] == ':443':
366
                netloc = netloc[:-4]
367
            if scheme not in ('http', 'https'):
368
                raise ValueError("Unsupported URL %s (%s)." % (value, scheme))
369

  
370
            # Normalized URL excludes params, query, and fragment.
371
            self.normalized_url = urlparse.urlunparse((scheme, netloc, path, None, None, None))
372
        else:
373
            self.normalized_url = None
374
            self.__dict__['url'] = None
375
 
376
    @setter
377
    def method(self, value):
378
        self.__dict__['method'] = value.upper()
379
 
380
    def _get_timestamp_nonce(self):
381
        return self['oauth_timestamp'], self['oauth_nonce']
382
 
383
    def get_nonoauth_parameters(self):
384
        """Get any non-OAuth parameters."""
385
        return dict([(k, v) for k, v in self.iteritems() 
386
                    if not k.startswith('oauth_')])
387
 
388
    def to_header(self, realm=''):
389
        """Serialize as a header for an HTTPAuth request."""
390
        oauth_params = ((k, v) for k, v in self.items() 
391
                            if k.startswith('oauth_'))
392
        stringy_params = ((k, escape(str(v))) for k, v in oauth_params)
393
        header_params = ('%s="%s"' % (k, v) for k, v in stringy_params)
394
        params_header = ', '.join(header_params)
395
 
396
        auth_header = 'OAuth realm="%s"' % realm
397
        if params_header:
398
            auth_header = "%s, %s" % (auth_header, params_header)
399
 
400
        return {'Authorization': auth_header}
401
 
402
    def to_postdata(self):
403
        """Serialize as post data for a POST request."""
404
        d = {}
405
        for k, v in self.iteritems():
406
            d[k.encode('utf-8')] = to_utf8_optional_iterator(v)
407

  
408
        # tell urlencode to deal with sequence values and map them correctly
409
        # to resulting querystring. for example self["k"] = ["v1", "v2"] will
410
        # result in 'k=v1&k=v2' and not k=%5B%27v1%27%2C+%27v2%27%5D
411
        return urllib.urlencode(d, True).replace('+', '%20')
412
 
413
    def to_url(self):
414
        """Serialize as a URL for a GET request."""
415
        base_url = urlparse.urlparse(self.url)
416
        try:
417
            query = base_url.query
418
        except AttributeError:
419
            # must be python <2.5
420
            query = base_url[4]
421
        query = parse_qs(query)
422
        for k, v in self.items():
423
            query.setdefault(k, []).append(v)
424
        
425
        try:
426
            scheme = base_url.scheme
427
            netloc = base_url.netloc
428
            path = base_url.path
429
            params = base_url.params
430
            fragment = base_url.fragment
431
        except AttributeError:
432
            # must be python <2.5
433
            scheme = base_url[0]
434
            netloc = base_url[1]
435
            path = base_url[2]
436
            params = base_url[3]
437
            fragment = base_url[5]
438
        
439
        url = (scheme, netloc, path, params,
440
               urllib.urlencode(query, True), fragment)
441
        return urlparse.urlunparse(url)
442

  
443
    def get_parameter(self, parameter):
444
        ret = self.get(parameter)
445
        if ret is None:
446
            raise Error('Parameter not found: %s' % parameter)
447

  
448
        return ret
449

  
450
    def get_normalized_parameters(self):
451
        """Return a string that contains the parameters that must be signed."""
452
        items = []
453
        for key, value in self.iteritems():
454
            if key == 'oauth_signature':
455
                continue
456
            # 1.0a/9.1.1 states that kvp must be sorted by key, then by value,
457
            # so we unpack sequence values into multiple items for sorting.
458
            if isinstance(value, basestring):
459
                items.append((to_utf8_if_string(key), to_utf8(value)))
460
            else:
461
                try:
462
                    value = list(value)
463
                except TypeError, e:
464
                    assert 'is not iterable' in str(e)
465
                    items.append((to_utf8_if_string(key), to_utf8_if_string(value)))
466
                else:
467
                    items.extend((to_utf8_if_string(key), to_utf8_if_string(item)) for item in value)
468

  
469
        # Include any query string parameters from the provided URL
470
        query = urlparse.urlparse(self.url)[4]
471

  
472
        url_items = self._split_url_string(query).items()
473
        url_items = [(to_utf8(k), to_utf8(v)) for k, v in url_items if k != 'oauth_signature' ]
474
        items.extend(url_items)
475

  
476
        items.sort()
477
        encoded_str = urllib.urlencode(items)
478
        # Encode signature parameters per Oauth Core 1.0 protocol
479
        # spec draft 7, section 3.6
480
        # (http://tools.ietf.org/html/draft-hammer-oauth-07#section-3.6)
481
        # Spaces must be encoded with "%20" instead of "+"
482
        return encoded_str.replace('+', '%20').replace('%7E', '~')
483

  
484
    def sign_request(self, signature_method, consumer, token):
485
        """Set the signature parameter to the result of sign."""
486

  
487
        if not self.is_form_encoded:
488
            # according to
489
            # http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html
490
            # section 4.1.1 "OAuth Consumers MUST NOT include an
491
            # oauth_body_hash parameter on requests with form-encoded
492
            # request bodies."
493
            self['oauth_body_hash'] = base64.b64encode(sha(self.body).digest())
494

  
495
        if 'oauth_consumer_key' not in self:
496
            self['oauth_consumer_key'] = consumer.key
497

  
498
        if token and 'oauth_token' not in self:
499
            self['oauth_token'] = token.key
500

  
501
        self['oauth_signature_method'] = signature_method.name
502
        self['oauth_signature'] = signature_method.sign(self, consumer, token)
503
 
504
    @classmethod
505
    def make_timestamp(cls):
506
        """Get seconds since epoch (UTC)."""
507
        return str(int(time.time()))
508
 
509
    @classmethod
510
    def make_nonce(cls):
511
        """Generate pseudorandom number."""
512
        return str(random.randint(0, 100000000))
513
 
514
    @classmethod
515
    def from_request(cls, http_method, http_url, headers=None, parameters=None,
516
            query_string=None):
517
        """Combines multiple parameter sources."""
518
        if parameters is None:
519
            parameters = {}
520
 
521
        # Headers
522
        if headers and 'Authorization' in headers:
523
            auth_header = headers['Authorization']
524
            # Check that the authorization header is OAuth.
525
            if auth_header[:6] == 'OAuth ':
526
                auth_header = auth_header[6:]
527
                try:
528
                    # Get the parameters from the header.
529
                    header_params = cls._split_header(auth_header)
530
                    parameters.update(header_params)
531
                except:
532
                    raise Error('Unable to parse OAuth parameters from '
533
                        'Authorization header.')
534
 
535
        # GET or POST query string.
536
        if query_string:
537
            query_params = cls._split_url_string(query_string)
538
            parameters.update(query_params)
539
 
540
        # URL parameters.
541
        param_str = urlparse.urlparse(http_url)[4] # query
542
        url_params = cls._split_url_string(param_str)
543
        parameters.update(url_params)
544
 
545
        if parameters:
546
            return cls(http_method, http_url, parameters)
547
 
548
        return None
549
 
550
    @classmethod
551
    def from_consumer_and_token(cls, consumer, token=None,
552
            http_method=HTTP_METHOD, http_url=None, parameters=None,
553
            body='', is_form_encoded=False):
554
        if not parameters:
555
            parameters = {}
556
 
557
        defaults = {
558
            'oauth_consumer_key': consumer.key,
559
            'oauth_timestamp': cls.make_timestamp(),
560
            'oauth_nonce': cls.make_nonce(),
561
            'oauth_version': cls.version,
562
        }
563
 
564
        defaults.update(parameters)
565
        parameters = defaults
566
 
567
        if token:
568
            parameters['oauth_token'] = token.key
569
            if token.verifier:
570
                parameters['oauth_verifier'] = token.verifier
571
 
572
        return Request(http_method, http_url, parameters, body=body, 
573
                       is_form_encoded=is_form_encoded)
574
 
575
    @classmethod
576
    def from_token_and_callback(cls, token, callback=None, 
577
        http_method=HTTP_METHOD, http_url=None, parameters=None):
578

  
579
        if not parameters:
580
            parameters = {}
581
 
582
        parameters['oauth_token'] = token.key
583
 
584
        if callback:
585
            parameters['oauth_callback'] = callback
586
 
587
        return cls(http_method, http_url, parameters)
588
 
589
    @staticmethod
590
    def _split_header(header):
591
        """Turn Authorization: header into parameters."""
592
        params = {}
593
        parts = header.split(',')
594
        for param in parts:
595
            # Ignore realm parameter.
596
            if param.find('realm') > -1:
597
                continue
598
            # Remove whitespace.
599
            param = param.strip()
600
            # Split key-value.
601
            param_parts = param.split('=', 1)
602
            # Remove quotes and unescape the value.
603
            params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
604
        return params
605
 
606
    @staticmethod
607
    def _split_url_string(param_str):
608
        """Turn URL string into parameters."""
609
        parameters = parse_qs(param_str.encode('utf-8'), keep_blank_values=True)
610
        for k, v in parameters.iteritems():
611
            parameters[k] = urllib.unquote(v[0])
612
        return parameters
613

  
614

  
615
class Client(httplib2.Http):
616
    """OAuthClient is a worker to attempt to execute a request."""
617

  
618
    def __init__(self, consumer, token=None, cache=None, timeout=None,
619
        proxy_info=None):
620

  
621
        if consumer is not None and not isinstance(consumer, Consumer):
622
            raise ValueError("Invalid consumer.")
623

  
624
        if token is not None and not isinstance(token, Token):
625
            raise ValueError("Invalid token.")
626

  
627
        self.consumer = consumer
628
        self.token = token
629
        self.method = SignatureMethod_HMAC_SHA1()
630

  
631
        httplib2.Http.__init__(self, cache=cache, timeout=timeout, proxy_info=proxy_info)
632

  
633
    def set_signature_method(self, method):
634
        if not isinstance(method, SignatureMethod):
635
            raise ValueError("Invalid signature method.")
636

  
637
        self.method = method
638

  
639
    def request(self, uri, method="GET", body='', headers=None, 
640
        redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None):
641
        DEFAULT_POST_CONTENT_TYPE = 'application/x-www-form-urlencoded'
642

  
643
        if not isinstance(headers, dict):
644
            headers = {}
645

  
646
        if method == "POST":
647
            headers['Content-Type'] = headers.get('Content-Type', 
648
                DEFAULT_POST_CONTENT_TYPE)
649

  
650
        is_form_encoded = \
651
            headers.get('Content-Type') == 'application/x-www-form-urlencoded'
652

  
653
        if is_form_encoded and body:
654
            parameters = parse_qs(body)
655
        else:
656
            parameters = None
657

  
658
        req = Request.from_consumer_and_token(self.consumer, 
659
            token=self.token, http_method=method, http_url=uri, 
660
            parameters=parameters, body=body, is_form_encoded=is_form_encoded)
661

  
662
        req.sign_request(self.method, self.consumer, self.token)
663

  
664
        schema, rest = urllib.splittype(uri)
665
        if rest.startswith('//'):
666
            hierpart = '//'
667
        else:
668
            hierpart = ''
669
        host, rest = urllib.splithost(rest)
670

  
671
        realm = schema + ':' + hierpart + host
672

  
673
        if is_form_encoded:
674
            body = req.to_postdata()
675
        elif method == "GET":
676
            uri = req.to_url()
677
        else:
678
            headers.update(req.to_header(realm=realm))
679

  
680
        return httplib2.Http.request(self, uri, method=method, body=body,
681
            headers=headers, redirections=redirections,
682
            connection_type=connection_type)
683

  
684

  
685
class Server(object):
686
    """A skeletal implementation of a service provider, providing protected
687
    resources to requests from authorized consumers.
688
 
689
    This class implements the logic to check requests for authorization. You
690
    can use it with your web server or web framework to protect certain
691
    resources with OAuth.
692
    """
693

  
694
    timestamp_threshold = 300 # In seconds, five minutes.
695
    version = OAUTH_VERSION
696
    signature_methods = None
697

  
698
    def __init__(self, signature_methods=None):
699
        self.signature_methods = signature_methods or {}
700

  
701
    def add_signature_method(self, signature_method):
702
        self.signature_methods[signature_method.name] = signature_method
703
        return self.signature_methods
704

  
705
    def verify_request(self, request, consumer, token):
706
        """Verifies an api call and checks all the parameters."""
707

  
708
        self._check_version(request)
709
        self._check_signature(request, consumer, token)
710
        parameters = request.get_nonoauth_parameters()
711
        return parameters
712

  
713
    def build_authenticate_header(self, realm=''):
714
        """Optional support for the authenticate header."""
715
        return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
716

  
717
    def _check_version(self, request):
718
        """Verify the correct version of the request for this server."""
719
        version = self._get_version(request)
720
        if version and version != self.version:
721
            raise Error('OAuth version %s not supported.' % str(version))
722

  
723
    def _get_version(self, request):
724
        """Return the version of the request for this server."""
725
        try:
726
            version = request.get_parameter('oauth_version')
727
        except:
728
            version = OAUTH_VERSION
729

  
730
        return version
731

  
732
    def _get_signature_method(self, request):
733
        """Figure out the signature with some defaults."""
734
        try:
735
            signature_method = request.get_parameter('oauth_signature_method')
736
        except:
737
            signature_method = SIGNATURE_METHOD
738

  
739
        try:
740
            # Get the signature method object.
741
            signature_method = self.signature_methods[signature_method]
742
        except:
743
            signature_method_names = ', '.join(self.signature_methods.keys())
744
            raise Error('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names))
745

  
746
        return signature_method
747

  
748
    def _get_verifier(self, request):
749
        return request.get_parameter('oauth_verifier')
750

  
751
    def _check_signature(self, request, consumer, token):
752
        timestamp, nonce = request._get_timestamp_nonce()
753
        self._check_timestamp(timestamp)
754
        signature_method = self._get_signature_method(request)
755

  
756
        try:
757
            signature = request.get_parameter('oauth_signature')
758
        except:
759
            raise MissingSignature('Missing oauth_signature.')
760

  
761
        # Validate the signature.
762
        valid = signature_method.check(request, consumer, token, signature)
763

  
764
        if not valid:
765
            key, base = signature_method.signing_base(request, consumer, token)
766

  
767
            raise Error('Invalid signature. Expected signature base ' 
768
                'string: %s' % base)
769

  
770
    def _check_timestamp(self, timestamp):
771
        """Verify that timestamp is recentish."""
772
        timestamp = int(timestamp)
773
        now = int(time.time())
774
        lapsed = now - timestamp
775
        if lapsed > self.timestamp_threshold:
776
            raise Error('Expired timestamp: given %d and now %s has a '
777
                'greater difference than threshold %d' % (timestamp, now, 
778
                    self.timestamp_threshold))
779

  
780

  
781
class SignatureMethod(object):
782
    """A way of signing requests.
783
 
784
    The OAuth protocol lets consumers and service providers pick a way to sign
785
    requests. This interface shows the methods expected by the other `oauth`
786
    modules for signing requests. Subclass it and implement its methods to
787
    provide a new way to sign requests.
788
    """
789

  
790
    def signing_base(self, request, consumer, token):
791
        """Calculates the string that needs to be signed.
792

  
793
        This method returns a 2-tuple containing the starting key for the
794
        signing and the message to be signed. The latter may be used in error
795
        messages to help clients debug their software.
796

  
797
        """
798
        raise NotImplementedError
799

  
800
    def sign(self, request, consumer, token):
801
        """Returns the signature for the given request, based on the consumer
802
        and token also provided.
803

  
804
        You should use your implementation of `signing_base()` to build the
805
        message to sign. Otherwise it may be less useful for debugging.
806

  
807
        """
808
        raise NotImplementedError
809

  
810
    def check(self, request, consumer, token, signature):
811
        """Returns whether the given signature is the correct signature for
812
        the given consumer and token signing the given request."""
813
        built = self.sign(request, consumer, token)
814
        return built == signature
815

  
816

  
817
class SignatureMethod_HMAC_SHA1(SignatureMethod):
818
    name = 'HMAC-SHA1'
819

  
820
    def signing_base(self, request, consumer, token):
821
        if not hasattr(request, 'normalized_url') or request.normalized_url is None:
822
            raise ValueError("Base URL for request is not set.")
823

  
824
        sig = (
825
            escape(request.method),
826
            escape(request.normalized_url),
827
            escape(request.get_normalized_parameters()),
828
        )
829

  
830
        key = '%s&' % escape(consumer.secret)
831
        if token:
832
            key += escape(token.secret)
833
        raw = '&'.join(sig)
834
        return key, raw
835

  
836
    def sign(self, request, consumer, token):
837
        """Builds the base signature string."""
838
        key, raw = self.signing_base(request, consumer, token)
839

  
840
        hashed = hmac.new(key, raw, sha)
841

  
842
        # Calculate the digest base 64.
843
        return binascii.b2a_base64(hashed.digest())[:-1]
844

  
845

  
846
class SignatureMethod_PLAINTEXT(SignatureMethod):
847

  
848
    name = 'PLAINTEXT'
849

  
850
    def signing_base(self, request, consumer, token):
851
        """Concatenates the consumer key and secret with the token's
852
        secret."""
853
        sig = '%s&' % escape(consumer.secret)
854
        if token:
855
            sig = sig + escape(token.secret)
856
        return sig, sig
857

  
858
    def sign(self, request, consumer, token):
859
        key, raw = self.signing_base(request, consumer, token)
860
        return raw
/dev/null
1
# This is the version of this source code.
2

  
3
manual_verstr = "1.5"
4

  
5

  
6

  
7
auto_build_num = "170"
8

  
9

  
10

  
11
verstr = manual_verstr + "." + auto_build_num
12
try:
13
    from pyutil.version_class import Version as pyutil_Version
14
    __version__ = pyutil_Version(verstr)
15
except (ImportError, ValueError):
16
    # Maybe there is no pyutil installed.
17
    from distutils.version import LooseVersion as distutils_Version
18
    __version__ = distutils_Version(verstr)
/dev/null
1
"""
2
The MIT License
3

  
4
Copyright (c) 2007-2010 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel
5

  
6
Permission is hereby granted, free of charge, to any person obtaining a copy
7
of this software and associated documentation files (the "Software"), to deal
8
in the Software without restriction, including without limitation the rights
9
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
copies of the Software, and to permit persons to whom the Software is
11
furnished to do so, subject to the following conditions:
12

  
13
The above copyright notice and this permission notice shall be included in
14
all copies or substantial portions of the Software.
15

  
16
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
THE SOFTWARE.
23
"""
24

  
25
import oauth2
26
import imaplib
27

  
28

  
29
class IMAP4_SSL(imaplib.IMAP4_SSL):
30
    """IMAP wrapper for imaplib.IMAP4_SSL that implements XOAUTH."""
31

  
32
    def authenticate(self, url, consumer, token):
33
        if consumer is not None and not isinstance(consumer, oauth2.Consumer):
34
            raise ValueError("Invalid consumer.")
35

  
36
        if token is not None and not isinstance(token, oauth2.Token):
37
            raise ValueError("Invalid token.")
38

  
39
        imaplib.IMAP4_SSL.authenticate(self, 'XOAUTH',
40
            lambda x: oauth2.build_xoauth_string(url, consumer, token))
/dev/null
1
"""
2
The MIT License
3

  
4
Copyright (c) 2007-2010 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel
5

  
6
Permission is hereby granted, free of charge, to any person obtaining a copy
7
of this software and associated documentation files (the "Software"), to deal
8
in the Software without restriction, including without limitation the rights
9
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
copies of the Software, and to permit persons to whom the Software is
11
furnished to do so, subject to the following conditions:
12

  
13
The above copyright notice and this permission notice shall be included in
14
all copies or substantial portions of the Software.
15

  
16
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
THE SOFTWARE.
23
"""
24

  
25
import oauth2
26
import smtplib
27
import base64
28

  
29

  
30
class SMTP(smtplib.SMTP):
31
    """SMTP wrapper for smtplib.SMTP that implements XOAUTH."""
32

  
33
    def authenticate(self, url, consumer, token):
34
        if consumer is not None and not isinstance(consumer, oauth2.Consumer):
35
            raise ValueError("Invalid consumer.")
36

  
37
        if token is not None and not isinstance(token, oauth2.Token):
38
            raise ValueError("Invalid token.")
39

  
40
        self.docmd('AUTH', 'XOAUTH %s' % \
41
            base64.b64encode(oauth2.build_xoauth_string(url, consumer, token)))

Also available in: Unified diff