~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/http/_urllib2_wrappers.py

  • Committer: Vincent Ladeuil
  • Date: 2007-04-21 20:39:06 UTC
  • mto: (2420.1.21 bzr.http.auth)
  • mto: This revision was merged to the branch mainline in revision 2463.
  • Revision ID: v.ladeuil+lp@free.fr-20070421203906-hta5jt0nmauyl9qy
Implement digest authentication. Test suite passes. Tested against apache-2.x.

* bzrlib/transport/http/_urllib2_wrappers.py:
(AbstractAuthHandler.auth_required): Do not attempt to
authenticate if we don't have a user. Rework the detection of
already tried authentications. Avoid building the auth header two
times, save the auth info at the right places.
(AbstractAuthHandler.build_auth_header): Add a request parameter
for digest needs.
(BasicAuthHandler.auth_match): Simplify.
(get_digest_algorithm_impls, DigestAuthHandler): Implements client
digest authentication. MD5 and SHA algorithms are supported. Only
'auth' qop is suppoted.
(HTTPBasicAuthHandler, ProxyBasicAuthHandler): Renamed HTTPHandler
and ProxyAuthHandler respectively.
(HTTPBasicAuthHandler, ProxyBasicAuthHandler,
HTTPDigestAuthHandler, ProxyDigestAuthHandler): New classes
implementing the combinations between (http, proxy) and (basic,
digest).
(Opener.__init__): No more handlers in comment ! One TODO less !

* bzrlib/transport/http/_urllib.py:
(HttpTransport_urllib.__init__): self.base is not suitable for an
auth uri, it can contain decorators.

* bzrlib/tests/test_http.py:
(TestAuth.test_no_user): New test to check the behavior with no
user when authentication is required.

* bzrlib/tests/HTTPTestUtil.py:
(DigestAuthRequestHandler.authorized): Delegate most of the work
to the server that control the needed persistent infos.
(AuthServer): Define an auth_relam attribute.
(DigestAuthServer): Implement a first version of digest
authentication. Only the MD5 algorithm and the 'auth' qop are
supported so far.
(HTTPAuthServer.init_http_auth): New method to simplify
the [http|proxy], [basic|digest] server combinations writing.

Show diffs side-by-side

added added

removed removed

Lines of Context:
51
51
# ensure that.
52
52
 
53
53
import httplib
 
54
import md5
 
55
import sha
54
56
import socket
55
57
import urllib
56
58
import urllib2
57
59
import urlparse
58
60
import re
59
61
import sys
 
62
import time
60
63
 
61
64
from bzrlib import __version__ as bzrlib_version
62
65
from bzrlib import (
760
763
 
761
764
    # The following attributes should be defined by daughter
762
765
    # classes:
763
 
    # - auth_reqed_header:  the header received from the server
 
766
    # - auth_required_header:  the header received from the server
764
767
    # - auth_header: the header sent in the request
765
768
 
766
769
    def __init__(self, password_manager):
775
778
        :param headers: The headers for the authentication error response.
776
779
        :return: None or the response for the authenticated request.
777
780
        """
778
 
        server_header = headers.get(self.auth_reqed_header, None)
 
781
        server_header = headers.get(self.auth_required_header, None)
779
782
        if server_header is None:
780
783
            # The http error MUST have the associated
781
784
            # header. This must never happen in production code.
782
 
            raise KeyError('%s not found' % self.auth_reqed_header)
783
 
 
784
 
        auth = self.get_auth(request)
 
785
            raise KeyError('%s not found' % self.auth_required_header)
 
786
 
 
787
        auth = self.get_auth(request).copy()
 
788
        if auth.get('user', None) is None:
 
789
            # Without a known user, we can't authenticate
 
790
            return None
 
791
 
785
792
        if self.auth_match(server_header, auth):
786
 
            client_header = self.build_auth_header(auth)
787
 
            if client_header == request.get_header(self.auth_header, None):
 
793
            # auth_match may have modified auth (by adding the
 
794
            # password or changing the realm, for example)
 
795
            old = self.get_auth(request)
 
796
            if request.get_header(self.auth_header, None) is not None \
 
797
                    and old.get('user') == auth.get('user') \
 
798
                    and old.get('realm') == auth.get('realm') \
 
799
                    and old.get('password') == auth.get('password'):
788
800
                # We already tried that, give up
789
801
                return None
790
802
 
791
 
            self.add_auth_header(request, client_header)
792
 
            request.add_unredirected_header(self.auth_header, client_header)
 
803
            # We will try to authenticate, save the auth so that
 
804
            # the build_auth_header that will be called during
 
805
            # parent.open use the right values
 
806
            self.set_auth(request, auth)
793
807
            response = self.parent.open(request)
794
808
            if response:
795
809
                self.auth_successful(request, response, auth)
803
817
        # 'Proxy Authentication Required' error.
804
818
        return None
805
819
 
806
 
    def get_auth(self, request):
807
 
        """Get the auth params from the request"""
808
 
        raise NotImplementedError(self.get_auth)
809
 
 
810
 
    def set_auth(self, request, auth):
811
 
        """Set the auth params for the request"""
812
 
        raise NotImplementedError(self.set_auth)
813
 
 
814
820
    def add_auth_header(self, request, header):
 
821
        """Add the authentication header to the request"""
815
822
        request.add_unredirected_header(self.auth_header, header)
816
823
 
817
824
    def auth_match(self, header, auth):
827
834
        """
828
835
        raise NotImplementedError(self.auth_match)
829
836
 
830
 
    def build_auth_header(self, auth):
 
837
    def build_auth_header(self, auth, request):
831
838
        """Build the value of the header used to authenticate.
832
839
 
833
840
        :param auth: The auth parameters needed to build the header.
 
841
        :param request: The request needing authentication.
834
842
 
835
843
        :return: None or header.
836
844
        """
875
883
        """Insert an authentication header if information is available"""
876
884
        auth = self.get_auth(request)
877
885
        if self.auth_params_reusable(auth):
878
 
            self.add_auth_header(request, self.build_auth_header(auth))
 
886
            self.add_auth_header(request, self.build_auth_header(auth, request))
879
887
        return request
880
888
 
881
889
    https_request = http_request # FIXME: Need test
882
890
 
883
891
 
884
 
class AbstractBasicAuthHandler(AbstractAuthHandler):
885
 
    """A custom basic auth handler."""
886
 
 
887
 
    auth_regexp = re.compile('[ \t]*([^ \t]+)[ \t]+realm="([^"]*)"', re.I)
888
 
 
889
 
    def build_auth_header(self, auth):
 
892
class BasicAuthHandler(AbstractAuthHandler):
 
893
    """A custom basic authentication handler."""
 
894
 
 
895
    auth_regexp = re.compile('realm="([^"]*)"', re.I)
 
896
 
 
897
    def build_auth_header(self, auth, request):
890
898
        raw = '%s:%s' % (auth['user'], auth['password'])
891
899
        auth_header = 'Basic ' + raw.encode('base64').strip()
892
900
        return auth_header
893
901
 
894
902
    def auth_match(self, header, auth):
895
 
        match = self.auth_regexp.search(header)
 
903
        scheme, raw_auth = header.split(None, 1)
 
904
        scheme = scheme.lower()
 
905
        if scheme != 'basic':
 
906
            return False
 
907
 
 
908
        match = self.auth_regexp.search(raw_auth)
896
909
        if match:
897
 
            scheme, auth['realm'] = match.groups()
898
 
            auth['scheme'] = scheme.lower()
899
 
            if auth['scheme'] != 'basic':
900
 
                match = None
901
 
            else:
902
 
                if auth.get('password',None) is None:
903
 
                    auth['password'] = self.get_password(auth['user'],
904
 
                                                         auth['authuri'],
905
 
                                                         auth['realm'])
 
910
            realm = match.groups()
 
911
            if scheme != 'basic':
 
912
                return False
906
913
 
 
914
            # Put useful info into auth
 
915
            auth['scheme'] = scheme
 
916
            auth['realm'] = realm
 
917
            if auth.get('password',None) is None:
 
918
                auth['password'] = self.get_password(auth['user'],
 
919
                                                     auth['authuri'],
 
920
                                                     auth['realm'])
907
921
        return match is not None
908
922
 
909
923
    def auth_params_reusable(self, auth):
910
924
        # If the auth scheme is known, it means a previous
911
925
        # authentication was succesful, all information is
912
926
        # available, no further checks are needed.
913
 
        return auth.get('scheme',None) == 'basic'
914
 
 
915
 
 
916
 
class HTTPBasicAuthHandler(AbstractBasicAuthHandler):
917
 
    """Custom basic authentication handler.
 
927
        return auth.get('scheme', None) == 'basic'
 
928
 
 
929
 
 
930
def get_digest_algorithm_impls(algorithm):
 
931
    H = None
 
932
    if algorithm == 'MD5':
 
933
        H = lambda x: md5.new(x).hexdigest()
 
934
    elif algorithm == 'SHA':
 
935
        H = lambda x: sha.new(x).hexdigest()
 
936
    if H is not None:
 
937
        KD = lambda secret, data: H("%s:%s" % (secret, data))
 
938
    return H, KD
 
939
 
 
940
 
 
941
class DigestAuthHandler(AbstractAuthHandler):
 
942
    """A custom digest authentication handler."""
 
943
 
 
944
    def auth_params_reusable(self, auth):
 
945
        # If the auth scheme is known, it means a previous
 
946
        # authentication was succesful, all information is
 
947
        # available, no further checks are needed.
 
948
        return auth.get('scheme', None) == 'digest'
 
949
 
 
950
    def auth_match(self, header, auth):
 
951
        scheme, raw_auth = header.split(None, 1)
 
952
        scheme = scheme.lower()
 
953
        if scheme != 'digest':
 
954
            return False
 
955
 
 
956
        # Put the requested authentication info into a dict
 
957
        req_auth = urllib2.parse_keqv_list(urllib2.parse_http_list(raw_auth))
 
958
 
 
959
        # Check that we can handle that authentication
 
960
        qop = req_auth.get('qop', None)
 
961
        if qop != 'auth': # No auth-int so far
 
962
            return False
 
963
 
 
964
        nonce = req_auth.get('nonce', None)
 
965
        old_nonce = auth.get('nonce', None)
 
966
        if nonce and old_nonce and nonce == old_nonce:
 
967
            # We already tried that
 
968
            return False
 
969
 
 
970
        algorithm = req_auth.get('algorithm', 'MD5')
 
971
        H, KD = get_digest_algorithm_impls(algorithm)
 
972
        if H is None:
 
973
            return False
 
974
 
 
975
        realm = req_auth.get('realm', None)
 
976
        if auth.get('password',None) is None:
 
977
            auth['password'] = self.get_password(auth['user'],
 
978
                                                 auth['authuri'],
 
979
                                                 realm)
 
980
        # Put useful info into auth
 
981
        try:
 
982
            auth['scheme'] = scheme
 
983
            auth['algorithm'] = algorithm
 
984
            auth['realm'] = req_auth['realm']
 
985
            auth['nonce'] = req_auth['nonce']
 
986
            auth['qop'] = qop
 
987
            auth['opaque'] = req_auth.get('opaque', None)
 
988
        except KeyError:
 
989
            return False
 
990
 
 
991
        return True
 
992
 
 
993
    def build_auth_header(self, auth, request):
 
994
        uri = request.get_selector()
 
995
        A1 = '%s:%s:%s' % (auth['user'], auth['realm'], auth['password'])
 
996
        A2 = '%s:%s' % (request.get_method(), uri)
 
997
        nonce = auth['nonce']
 
998
        qop = auth['qop']
 
999
 
 
1000
        H, KD = get_digest_algorithm_impls(auth['algorithm'])
 
1001
        nonce_count = auth.get('nonce_count',0)
 
1002
        nonce_count += 1
 
1003
        ncvalue = '%08x' % nonce_count
 
1004
        cnonce = sha.new("%s:%s:%s:%s" % (nonce_count, nonce,
 
1005
                                          time.ctime(), urllib2.randombytes(8))
 
1006
                         ).hexdigest()[:16]
 
1007
        noncebit = '%s:%s:%s:%s:%s' % (nonce, ncvalue, cnonce, qop, H(A2))
 
1008
        response_digest = KD(H(A1), noncebit)
 
1009
 
 
1010
        header = 'Digest '
 
1011
        header += 'username="%s", realm="%s", nonce="%s",' % (auth['user'],
 
1012
                                                             auth['realm'],
 
1013
                                                             nonce)
 
1014
        header += ' uri="%s", response="%s"' % (uri, response_digest)
 
1015
        opaque = auth.get('opaque', None)
 
1016
        if opaque:
 
1017
            header += ', opaque="%s"' % opaque
 
1018
        header += ', algorithm="%s"' % auth['algorithm']
 
1019
        header += ', qop="%s", nc="%s", cnonce="%s"' % (qop, ncvalue, cnonce)
 
1020
 
 
1021
        # We have used the nonce once more, update the count
 
1022
        auth['nonce_count'] = nonce_count
 
1023
 
 
1024
        return header
 
1025
 
 
1026
 
 
1027
class HTTPAuthHandler(AbstractAuthHandler):
 
1028
    """Custom http authentication handler.
918
1029
 
919
1030
    Send the authentication preventively to avoid the roundtrip
920
 
    associated with the 401 error.
 
1031
    associated with the 401 error and keep the revelant info in
 
1032
    the auth request attribute.
921
1033
    """
922
1034
 
923
1035
    password_prompt = 'HTTP %(user)s@%(host)s%(realm)s password'
924
 
    auth_reqed_header = 'www-authenticate'
 
1036
    auth_required_header = 'www-authenticate'
925
1037
    auth_header = 'Authorization'
926
1038
 
927
1039
    def get_auth(self, request):
 
1040
        """Get the auth params from the request"""
928
1041
        return request.auth
929
1042
 
930
1043
    def set_auth(self, request, auth):
 
1044
        """Set the auth params for the request"""
931
1045
        request.auth = auth
932
1046
 
933
1047
    def http_error_401(self, req, fp, code, msg, headers):
934
1048
        return self.auth_required(req, headers)
935
1049
 
936
1050
 
937
 
class ProxyBasicAuthHandler(AbstractBasicAuthHandler):
938
 
    """Custom proxy basic authentication handler.
 
1051
class ProxyAuthHandler(AbstractAuthHandler):
 
1052
    """Custom proxy authentication handler.
939
1053
 
940
1054
    Send the authentication preventively to avoid the roundtrip
941
 
    associated with the 407 error.
 
1055
    associated with the 407 error and keep the revelant info in
 
1056
    the proxy_auth request attribute..
942
1057
    """
943
1058
 
944
1059
    password_prompt = 'Proxy %(user)s@%(host)s%(realm)s password'
945
 
    auth_reqed_header = 'proxy-authenticate'
 
1060
    auth_required_header = 'proxy-authenticate'
946
1061
    # FIXME: the correct capitalization is Proxy-Authorization,
947
1062
    # but python-2.4 urllib2.Request insist on using capitalize()
948
1063
    # instead of title().
949
1064
    auth_header = 'Proxy-authorization'
950
1065
 
951
1066
    def get_auth(self, request):
 
1067
        """Get the auth params from the request"""
952
1068
        return request.proxy_auth
953
1069
 
954
1070
    def set_auth(self, request, auth):
 
1071
        """Set the auth params for the request"""
955
1072
        request.proxy_auth = auth
956
1073
 
957
1074
    def http_error_407(self, req, fp, code, msg, headers):
958
1075
        return self.auth_required(req, headers)
959
1076
 
960
1077
 
 
1078
class HTTPBasicAuthHandler(BasicAuthHandler, HTTPAuthHandler):
 
1079
    """Custom http basic authentication handler"""
 
1080
 
 
1081
 
 
1082
class ProxyBasicAuthHandler(BasicAuthHandler, ProxyAuthHandler):
 
1083
    """Custom proxy basic authentication handler"""
 
1084
 
 
1085
 
 
1086
class HTTPDigestAuthHandler(DigestAuthHandler, HTTPAuthHandler):
 
1087
    """Custom http basic authentication handler"""
 
1088
 
 
1089
 
 
1090
class ProxyDigestAuthHandler(DigestAuthHandler, ProxyAuthHandler):
 
1091
    """Custom proxy basic authentication handler"""
 
1092
 
961
1093
 
962
1094
class HTTPErrorProcessor(urllib2.HTTPErrorProcessor):
963
1095
    """Process HTTP error responses.
1013
1145
                 redirect=HTTPRedirectHandler,
1014
1146
                 error=HTTPErrorProcessor,):
1015
1147
        self.password_manager = PasswordManager()
1016
 
        # TODO: Implements the necessary wrappers for the handlers
1017
 
        # commented out below
1018
1148
        self._opener = urllib2.build_opener( \
1019
1149
            connection, redirect, error,
1020
1150
            ProxyHandler(self.password_manager),
1021
1151
            HTTPBasicAuthHandler(self.password_manager),
1022
 
            #urllib2.HTTPDigestAuthHandler(self.password_manager),
 
1152
            HTTPDigestAuthHandler(self.password_manager),
1023
1153
            ProxyBasicAuthHandler(self.password_manager),
1024
 
            #urllib2.ProxyDigestAuthHandler,
 
1154
            ProxyDigestAuthHandler(self.password_manager),
1025
1155
            HTTPHandler,
1026
1156
            HTTPSHandler,
1027
1157
            HTTPDefaultErrorHandler,