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 / requests / auth.py @ 564

History | View | Annotate | Download (7.37 KB)

1
# -*- coding: utf-8 -*-
2

    
3
"""
4
requests.auth
5
~~~~~~~~~~~~~
6

7
This module contains the authentication handlers for Requests.
8
"""
9

    
10
import os
11
import re
12
import time
13
import hashlib
14
import threading
15

    
16
from base64 import b64encode
17

    
18
from .compat import urlparse, str
19
from .cookies import extract_cookies_to_jar
20
from .utils import parse_dict_header, to_native_string
21
from .status_codes import codes
22

    
23
CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'
24
CONTENT_TYPE_MULTI_PART = 'multipart/form-data'
25

    
26

    
27
def _basic_auth_str(username, password):
28
    """Returns a Basic Auth string."""
29

    
30
    authstr = 'Basic ' + to_native_string(
31
        b64encode(('%s:%s' % (username, password)).encode('latin1')).strip()
32
    )
33

    
34
    return authstr
35

    
36

    
37
class AuthBase(object):
38
    """Base class that all auth implementations derive from"""
39

    
40
    def __call__(self, r):
41
        raise NotImplementedError('Auth hooks must be callable.')
42

    
43

    
44
class HTTPBasicAuth(AuthBase):
45
    """Attaches HTTP Basic Authentication to the given Request object."""
46
    def __init__(self, username, password):
47
        self.username = username
48
        self.password = password
49

    
50
    def __call__(self, r):
51
        r.headers['Authorization'] = _basic_auth_str(self.username, self.password)
52
        return r
53

    
54

    
55
class HTTPProxyAuth(HTTPBasicAuth):
56
    """Attaches HTTP Proxy Authentication to a given Request object."""
57
    def __call__(self, r):
58
        r.headers['Proxy-Authorization'] = _basic_auth_str(self.username, self.password)
59
        return r
60

    
61

    
62
class HTTPDigestAuth(AuthBase):
63
    """Attaches HTTP Digest Authentication to the given Request object."""
64
    def __init__(self, username, password):
65
        self.username = username
66
        self.password = password
67
        # Keep state in per-thread local storage
68
        self._thread_local = threading.local()
69

    
70
    def init_per_thread_state(self):
71
        # Ensure state is initialized just once per-thread
72
        if not hasattr(self._thread_local, 'init'):
73
            self._thread_local.init = True
74
            self._thread_local.last_nonce = ''
75
            self._thread_local.nonce_count = 0
76
            self._thread_local.chal = {}
77
            self._thread_local.pos = None
78
            self._thread_local.num_401_calls = None
79

    
80
    def build_digest_header(self, method, url):
81

    
82
        realm = self._thread_local.chal['realm']
83
        nonce = self._thread_local.chal['nonce']
84
        qop = self._thread_local.chal.get('qop')
85
        algorithm = self._thread_local.chal.get('algorithm')
86
        opaque = self._thread_local.chal.get('opaque')
87

    
88
        if algorithm is None:
89
            _algorithm = 'MD5'
90
        else:
91
            _algorithm = algorithm.upper()
92
        # lambdas assume digest modules are imported at the top level
93
        if _algorithm == 'MD5' or _algorithm == 'MD5-SESS':
94
            def md5_utf8(x):
95
                if isinstance(x, str):
96
                    x = x.encode('utf-8')
97
                return hashlib.md5(x).hexdigest()
98
            hash_utf8 = md5_utf8
99
        elif _algorithm == 'SHA':
100
            def sha_utf8(x):
101
                if isinstance(x, str):
102
                    x = x.encode('utf-8')
103
                return hashlib.sha1(x).hexdigest()
104
            hash_utf8 = sha_utf8
105

    
106
        KD = lambda s, d: hash_utf8("%s:%s" % (s, d))
107

    
108
        if hash_utf8 is None:
109
            return None
110

    
111
        # XXX not implemented yet
112
        entdig = None
113
        p_parsed = urlparse(url)
114
        #: path is request-uri defined in RFC 2616 which should not be empty
115
        path = p_parsed.path or "/"
116
        if p_parsed.query:
117
            path += '?' + p_parsed.query
118

    
119
        A1 = '%s:%s:%s' % (self.username, realm, self.password)
120
        A2 = '%s:%s' % (method, path)
121

    
122
        HA1 = hash_utf8(A1)
123
        HA2 = hash_utf8(A2)
124

    
125
        if nonce == self._thread_local.last_nonce:
126
            self._thread_local.nonce_count += 1
127
        else:
128
            self._thread_local.nonce_count = 1
129
        ncvalue = '%08x' % self._thread_local.nonce_count
130
        s = str(self._thread_local.nonce_count).encode('utf-8')
131
        s += nonce.encode('utf-8')
132
        s += time.ctime().encode('utf-8')
133
        s += os.urandom(8)
134

    
135
        cnonce = (hashlib.sha1(s).hexdigest()[:16])
136
        if _algorithm == 'MD5-SESS':
137
            HA1 = hash_utf8('%s:%s:%s' % (HA1, nonce, cnonce))
138

    
139
        if not qop:
140
            respdig = KD(HA1, "%s:%s" % (nonce, HA2))
141
        elif qop == 'auth' or 'auth' in qop.split(','):
142
            noncebit = "%s:%s:%s:%s:%s" % (
143
                nonce, ncvalue, cnonce, 'auth', HA2
144
                )
145
            respdig = KD(HA1, noncebit)
146
        else:
147
            # XXX handle auth-int.
148
            return None
149

    
150
        self._thread_local.last_nonce = nonce
151

    
152
        # XXX should the partial digests be encoded too?
153
        base = 'username="%s", realm="%s", nonce="%s", uri="%s", ' \
154
               'response="%s"' % (self.username, realm, nonce, path, respdig)
155
        if opaque:
156
            base += ', opaque="%s"' % opaque
157
        if algorithm:
158
            base += ', algorithm="%s"' % algorithm
159
        if entdig:
160
            base += ', digest="%s"' % entdig
161
        if qop:
162
            base += ', qop="auth", nc=%s, cnonce="%s"' % (ncvalue, cnonce)
163

    
164
        return 'Digest %s' % (base)
165

    
166
    def handle_redirect(self, r, **kwargs):
167
        """Reset num_401_calls counter on redirects."""
168
        if r.is_redirect:
169
            self._thread_local.num_401_calls = 1
170

    
171
    def handle_401(self, r, **kwargs):
172
        """Takes the given response and tries digest-auth, if needed."""
173

    
174
        if self._thread_local.pos is not None:
175
            # Rewind the file position indicator of the body to where
176
            # it was to resend the request.
177
            r.request.body.seek(self._thread_local.pos)
178
        s_auth = r.headers.get('www-authenticate', '')
179

    
180
        if 'digest' in s_auth.lower() and self._thread_local.num_401_calls < 2:
181

    
182
            self._thread_local.num_401_calls += 1
183
            pat = re.compile(r'digest ', flags=re.IGNORECASE)
184
            self._thread_local.chal = parse_dict_header(pat.sub('', s_auth, count=1))
185

    
186
            # Consume content and release the original connection
187
            # to allow our new request to reuse the same one.
188
            r.content
189
            r.close()
190
            prep = r.request.copy()
191
            extract_cookies_to_jar(prep._cookies, r.request, r.raw)
192
            prep.prepare_cookies(prep._cookies)
193

    
194
            prep.headers['Authorization'] = self.build_digest_header(
195
                prep.method, prep.url)
196
            _r = r.connection.send(prep, **kwargs)
197
            _r.history.append(r)
198
            _r.request = prep
199

    
200
            return _r
201

    
202
        self._thread_local.num_401_calls = 1
203
        return r
204

    
205
    def __call__(self, r):
206
        # Initialize per-thread state, if needed
207
        self.init_per_thread_state()
208
        # If we have a saved nonce, skip the 401
209
        if self._thread_local.last_nonce:
210
            r.headers['Authorization'] = self.build_digest_header(r.method, r.url)
211
        try:
212
            self._thread_local.pos = r.body.tell()
213
        except AttributeError:
214
            # In the case of HTTPDigestAuth being reused and the body of
215
            # the previous request was a file-like object, pos has the
216
            # file position of the previous body. Ensure it's set to
217
            # None.
218
            self._thread_local.pos = None
219
        r.register_hook('response', self.handle_401)
220
        r.register_hook('response', self.handle_redirect)
221
        self._thread_local.num_401_calls = 1
222

    
223
        return r