158
163
self.redirected_to = None
159
164
# Unless told otherwise, redirections are not followed
160
165
self.follow_redirections = False
162
def extract_auth(self, url):
163
"""Extracts authentification information from url.
165
Get user and password from url of the form: http://user:pass@host/path
167
scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
170
auth, netloc = netloc.split('@', 1)
172
user, password = auth.split(':', 1)
174
user, password = auth, None
175
user = urllib.unquote(user)
176
if password is not None:
177
password = urllib.unquote(password)
182
url = urlparse.urlunsplit((scheme, netloc, path, query, fragment))
184
return url, user, password
166
# auth and proxy_auth are dicts containing, at least
167
# (scheme, url, realm, user, password).
168
# The dict entries are mostly handled by the AuthHandler.
169
# Some authentication schemes may add more entries.
186
173
def get_method(self):
187
174
return self.method
190
# The urlib2.xxxAuthHandler handle the authentification of the
177
def extract_credentials(url):
178
"""Extracts credentials information from url.
180
Get user and password from url of the form: http://user:pass@host/path
181
:returns: (clean_url, user, password)
183
scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
186
auth, netloc = netloc.split('@', 1)
188
user, password = auth.split(':', 1)
190
user, password = auth, None
191
user = urllib.unquote(user)
192
if password is not None:
193
password = urllib.unquote(password)
198
# Build the clean url
199
clean_url = urlparse.urlunsplit((scheme, netloc, path, query, fragment))
201
return clean_url, user, password
203
def extract_authentication_uri(url):
204
"""Extract the authentication uri from any url.
206
In the context of bzr, we simplified the authentication uri
207
to the host only. For the transport lifetime, we allow only
208
one user by realm on a given host. I.e. handling several
209
users for different paths for the same realm should be done
212
scheme, host, path, query, fragment = urlparse.urlsplit(url)
213
return '%s://%s' % (scheme, host)
216
# The urlib2.xxxAuthHandler handle the authentication of the
191
217
# requests, to do that, they need an urllib2 PasswordManager *at
192
# build time*. We also need one to reuse the passwords already
218
# build time*. We also need one to reuse the passwords entered by
194
220
class PasswordManager(urllib2.HTTPPasswordMgrWithDefaultRealm):
196
222
def __init__(self):
720
class HTTPBasicAuthHandler(urllib2.HTTPBasicAuthHandler):
721
"""Custom basic authentification handler.
723
Send the authentification preventively to avoid the the
724
roundtrip associated with the 401 error.
727
# def http_request(self, request):
728
# """Insert an authentification header if information is available"""
729
# if request.auth == 'basic' and request.password is not None:
746
class AbstractAuthHandler(urllib2.BaseHandler):
747
"""A custom abstract authentication handler for all http authentications.
749
Provides the meat to handle authentication errors and
750
preventively set authentication headers after the first
751
successful authentication.
753
This can be used for http and proxy, as well as for basic and
754
digest authentications.
756
This provides an unified interface for all authentication handlers
757
(urllib2 provides far too many with different policies).
759
The interaction between this handler and the urllib2
760
framework is not obvious, it works as follow:
762
opener.open(request) is called:
764
- that may trigger http_request which will add an authentication header
765
(self.build_header) if enough info is available.
767
- the request is sent to the server,
769
- if an authentication error is received self.auth_required is called,
770
we acquire the authentication info in the error headers and call
771
self.auth_match to check that we are able to try the
772
authentication and complete the authentication parameters,
774
- we call parent.open(request), that may trigger http_request
775
and will add a header (self.build_header), but here we have
776
all the required info (keep in mind that the request and
777
authentication used in the recursive calls are really (and must be)
780
- if the call returns a response, the authentication have been
781
successful and the request authentication parameters have been updated.
784
# The following attributes should be defined by daughter
786
# - auth_required_header: the header received from the server
787
# - auth_header: the header sent in the request
789
def __init__(self, password_manager):
790
self.password_manager = password_manager
791
self.find_user_password = password_manager.find_user_password
792
self.add_password = password_manager.add_password
794
def update_auth(self, auth, key, value):
795
"""Update a value in auth marking the auth as modified if needed"""
796
old_value = auth.get(key, None)
797
if old_value != value:
799
auth['modified'] = True
801
def auth_required(self, request, headers):
802
"""Retry the request if the auth scheme is ours.
804
:param request: The request needing authentication.
805
:param headers: The headers for the authentication error response.
806
:return: None or the response for the authenticated request.
808
server_header = headers.get(self.auth_required_header, None)
809
if server_header is None:
810
# The http error MUST have the associated
811
# header. This must never happen in production code.
812
raise KeyError('%s not found' % self.auth_required_header)
814
auth = self.get_auth(request)
815
if auth.get('user', None) is None:
816
# Without a known user, we can't authenticate
819
auth['modified'] = False
820
if self.auth_match(server_header, auth):
821
# auth_match may have modified auth (by adding the
822
# password or changing the realm, for example)
823
if request.get_header(self.auth_header, None) is not None \
824
and not auth['modified']:
825
# We already tried that, give up
828
response = self.parent.open(request)
830
self.auth_successful(request, response)
832
# We are not qualified to handle the authentication.
833
# Note: the authentication error handling will try all
834
# available handlers. If one of them authenticates
835
# successfully, a response will be returned. If none of
836
# them succeeds, None will be returned and the error
837
# handler will raise the 401 'Unauthorized' or the 407
838
# 'Proxy Authentication Required' error.
841
def add_auth_header(self, request, header):
842
"""Add the authentication header to the request"""
843
request.add_unredirected_header(self.auth_header, header)
845
def auth_match(self, header, auth):
846
"""Check that we are able to handle that authentication scheme.
848
The request authentication parameters may need to be
849
updated with info from the server. Some of these
850
parameters, when combined, are considered to be the
851
authentication key, if one of them change the
852
authentication result may change. 'user' and 'password'
853
are exampls, but some auth schemes may have others
854
(digest's nonce is an example, digest's nonce_count is a
855
*counter-example*). Such parameters must be updated by
856
using the update_auth() method.
858
:param header: The authentication header sent by the server.
859
:param auth: The auth parameters already known. They may be
861
:returns: True if we can try to handle the authentication.
863
raise NotImplementedError(self.auth_match)
865
def build_auth_header(self, auth, request):
866
"""Build the value of the header used to authenticate.
868
:param auth: The auth parameters needed to build the header.
869
:param request: The request needing authentication.
871
:return: None or header.
873
raise NotImplementedError(self.build_auth_header)
875
def auth_successful(self, request, response):
876
"""The authentification was successful for the request.
878
Additional infos may be available in the response.
880
:param request: The succesfully authenticated request.
881
:param response: The server response (may contain auth info).
885
def get_password(self, user, authuri, realm=None):
886
"""Ask user for a password if none is already available."""
887
user_found, password = self.find_user_password(realm, authuri)
888
if user_found != user:
889
# FIXME: write a test for that case
893
# Prompt user only if we can't find a password
895
realm_prompt = " Realm: '%s'" % realm
898
scheme, host, path, query, fragment = urlparse.urlsplit(authuri)
899
password = ui.ui_factory.get_password(prompt=self.password_prompt,
900
user=user, host=host,
902
if password is not None:
903
self.add_password(realm, authuri, user, password)
906
def http_request(self, request):
907
"""Insert an authentication header if information is available"""
908
auth = self.get_auth(request)
909
if self.auth_params_reusable(auth):
910
self.add_auth_header(request, self.build_auth_header(auth, request))
913
https_request = http_request # FIXME: Need test
916
class BasicAuthHandler(AbstractAuthHandler):
917
"""A custom basic authentication handler."""
919
auth_regexp = re.compile('realm="([^"]*)"', re.I)
921
def build_auth_header(self, auth, request):
922
raw = '%s:%s' % (auth['user'], auth['password'])
923
auth_header = 'Basic ' + raw.encode('base64').strip()
926
def auth_match(self, header, auth):
927
scheme, raw_auth = header.split(None, 1)
928
scheme = scheme.lower()
929
if scheme != 'basic':
932
match = self.auth_regexp.search(raw_auth)
934
realm = match.groups()
935
if scheme != 'basic':
938
# Put useful info into auth
939
self.update_auth(auth, 'scheme', scheme)
940
self.update_auth(auth, 'realm', realm)
941
if auth.get('password',None) is None:
942
password = self.get_password(auth['user'], auth['authuri'],
944
self.update_auth(auth, 'password', password)
945
return match is not None
947
def auth_params_reusable(self, auth):
948
# If the auth scheme is known, it means a previous
949
# authentication was successful, all information is
950
# available, no further checks are needed.
951
return auth.get('scheme', None) == 'basic'
954
def get_digest_algorithm_impls(algorithm):
957
if algorithm == 'MD5':
958
H = lambda x: md5.new(x).hexdigest()
959
elif algorithm == 'SHA':
960
H = lambda x: sha.new(x).hexdigest()
962
KD = lambda secret, data: H("%s:%s" % (secret, data))
966
def get_new_cnonce(nonce, nonce_count):
967
raw = '%s:%d:%s:%s' % (nonce, nonce_count, time.ctime(),
968
urllib2.randombytes(8))
969
return sha.new(raw).hexdigest()[:16]
972
class DigestAuthHandler(AbstractAuthHandler):
973
"""A custom digest authentication handler."""
975
def auth_params_reusable(self, auth):
976
# If the auth scheme is known, it means a previous
977
# authentication was successful, all information is
978
# available, no further checks are needed.
979
return auth.get('scheme', None) == 'digest'
981
def auth_match(self, header, auth):
982
scheme, raw_auth = header.split(None, 1)
983
scheme = scheme.lower()
984
if scheme != 'digest':
987
# Put the requested authentication info into a dict
988
req_auth = urllib2.parse_keqv_list(urllib2.parse_http_list(raw_auth))
990
# Check that we can handle that authentication
991
qop = req_auth.get('qop', None)
992
if qop != 'auth': # No auth-int so far
995
H, KD = get_digest_algorithm_impls(req_auth.get('algorithm', 'MD5'))
999
realm = req_auth.get('realm', None)
1000
if auth.get('password',None) is None:
1001
auth['password'] = self.get_password(auth['user'],
1004
# Put useful info into auth
1006
self.update_auth(auth, 'scheme', scheme)
1007
if req_auth.get('algorithm', None) is not None:
1008
self.update_auth(auth, 'algorithm', req_auth.get('algorithm'))
1009
self.update_auth(auth, 'realm', realm)
1010
nonce = req_auth['nonce']
1011
if auth.get('nonce', None) != nonce:
1012
# A new nonce, never used
1013
self.update_auth(auth, 'nonce_count', 0)
1014
self.update_auth(auth, 'nonce', nonce)
1015
self.update_auth(auth, 'qop', qop)
1016
auth['opaque'] = req_auth.get('opaque', None)
1018
# Some required field is not there
1023
def build_auth_header(self, auth, request):
1024
url_scheme, url_selector = urllib.splittype(request.get_selector())
1025
sel_host, uri = urllib.splithost(url_selector)
1027
A1 = '%s:%s:%s' % (auth['user'], auth['realm'], auth['password'])
1028
A2 = '%s:%s' % (request.get_method(), uri)
1030
nonce = auth['nonce']
1033
nonce_count = auth['nonce_count'] + 1
1034
ncvalue = '%08x' % nonce_count
1035
cnonce = get_new_cnonce(nonce, nonce_count)
1037
H, KD = get_digest_algorithm_impls(auth.get('algorithm', 'MD5'))
1038
nonce_data = '%s:%s:%s:%s:%s' % (nonce, ncvalue, cnonce, qop, H(A2))
1039
request_digest = KD(H(A1), nonce_data)
1042
header += 'username="%s", realm="%s", nonce="%s"' % (auth['user'],
1045
header += ', uri="%s"' % uri
1046
header += ', cnonce="%s", nc=%s' % (cnonce, ncvalue)
1047
header += ', qop="%s"' % qop
1048
header += ', response="%s"' % request_digest
1049
# Append the optional fields
1050
opaque = auth.get('opaque', None)
1052
header += ', opaque="%s"' % opaque
1053
if auth.get('algorithm', None):
1054
header += ', algorithm="%s"' % auth.get('algorithm')
1056
# We have used the nonce once more, update the count
1057
auth['nonce_count'] = nonce_count
1062
class HTTPAuthHandler(AbstractAuthHandler):
1063
"""Custom http authentication handler.
1065
Send the authentication preventively to avoid the roundtrip
1066
associated with the 401 error and keep the revelant info in
1067
the auth request attribute.
1070
password_prompt = 'HTTP %(user)s@%(host)s%(realm)s password'
1071
auth_required_header = 'www-authenticate'
1072
auth_header = 'Authorization'
1074
def get_auth(self, request):
1075
"""Get the auth params from the request"""
1078
def set_auth(self, request, auth):
1079
"""Set the auth params for the request"""
1082
def http_error_401(self, req, fp, code, msg, headers):
1083
return self.auth_required(req, headers)
1086
class ProxyAuthHandler(AbstractAuthHandler):
1087
"""Custom proxy authentication handler.
1089
Send the authentication preventively to avoid the roundtrip
1090
associated with the 407 error and keep the revelant info in
1091
the proxy_auth request attribute..
1094
password_prompt = 'Proxy %(user)s@%(host)s%(realm)s password'
1095
auth_required_header = 'proxy-authenticate'
1096
# FIXME: the correct capitalization is Proxy-Authorization,
1097
# but python-2.4 urllib2.Request insist on using capitalize()
1098
# instead of title().
1099
auth_header = 'Proxy-authorization'
1101
def get_auth(self, request):
1102
"""Get the auth params from the request"""
1103
return request.proxy_auth
1105
def set_auth(self, request, auth):
1106
"""Set the auth params for the request"""
1107
request.proxy_auth = auth
1109
def http_error_407(self, req, fp, code, msg, headers):
1110
return self.auth_required(req, headers)
1113
class HTTPBasicAuthHandler(BasicAuthHandler, HTTPAuthHandler):
1114
"""Custom http basic authentication handler"""
1117
class ProxyBasicAuthHandler(BasicAuthHandler, ProxyAuthHandler):
1118
"""Custom proxy basic authentication handler"""
1121
class HTTPDigestAuthHandler(DigestAuthHandler, HTTPAuthHandler):
1122
"""Custom http basic authentication handler"""
1125
class ProxyDigestAuthHandler(DigestAuthHandler, ProxyAuthHandler):
1126
"""Custom proxy basic authentication handler"""
734
1129
class HTTPErrorProcessor(urllib2.HTTPErrorProcessor):