diff options
| author | Aldo Cortesi <aldo@corte.si> | 2012-12-30 11:27:04 -0800 | 
|---|---|---|
| committer | Aldo Cortesi <aldo@corte.si> | 2012-12-30 11:27:04 -0800 | 
| commit | cfab27232127437cca8ac3699065db653da97dba (patch) | |
| tree | 60a92d02b45daf421a0329e4d404652b8f6b3baa | |
| parent | 3c8dcf880860191ef3bd000f95cf7ea980c6bda9 (diff) | |
| parent | 440a9f6bda8d645945e8c056a5e869c712dd2d69 (diff) | |
| download | mitmproxy-cfab27232127437cca8ac3699065db653da97dba.tar.gz mitmproxy-cfab27232127437cca8ac3699065db653da97dba.tar.bz2 mitmproxy-cfab27232127437cca8ac3699065db653da97dba.zip  | |
Merge pull request #83 from rouli/master
Adding some basic proxy authentication code
| -rw-r--r-- | libmproxy/authentication.py | 95 | ||||
| -rw-r--r-- | libmproxy/cmdline.py | 48 | ||||
| -rw-r--r-- | libmproxy/contrib/README | 1 | ||||
| -rw-r--r-- | libmproxy/contrib/md5crypt.py | 94 | ||||
| -rw-r--r-- | libmproxy/proxy.py | 79 | 
5 files changed, 288 insertions, 29 deletions
diff --git a/libmproxy/authentication.py b/libmproxy/authentication.py new file mode 100644 index 00000000..e5383f5a --- /dev/null +++ b/libmproxy/authentication.py @@ -0,0 +1,95 @@ +import binascii +import contrib.md5crypt as md5crypt + +class NullProxyAuth(): +    """ No proxy auth at all (returns empty challange headers) """ +    def __init__(self, password_manager=None): +        self.password_manager = password_manager +        self.username = "" + +    def authenticate(self, auth_value): +        """ Tests that the specified user is allowed to use the proxy (stub) """ +        return True + +    def auth_challenge_headers(self): +        """ Returns a dictionary containing the headers require to challenge the user """ +        return {} + +    def get_username(self): +        return self.username + + +class BasicProxyAuth(NullProxyAuth): + +    def __init__(self, password_manager, realm="mitmproxy"): +        NullProxyAuth.__init__(self, password_manager) +        self.realm = "mitmproxy" + +    def authenticate(self, auth_value): +        if (not auth_value) or (not auth_value[0]): +            return False; +        try: +            scheme, username, password = self.parse_authorization_header(auth_value[0]) +        except: +            return False +        if scheme.lower()!='basic': +            return False +        if not self.password_manager.test(username, password): +            return False +        self.username = username +        return True + +    def auth_challenge_headers(self): +        return {'Proxy-Authenticate':'Basic realm="%s"'%self.realm} + +    def parse_authorization_header(self, auth_value): +        words = auth_value.split() +        scheme = words[0] +        user = binascii.a2b_base64(words[1]) +        username, password = user.split(':') +        return scheme, username, password + +class PasswordManager(): +    def __init__(self): +        pass + +    def test(self, username, password_token): +        return False + +class PermissivePasswordManager(PasswordManager): + +    def __init__(self): +        PasswordManager.__init__(self) + +    def test(self, username, password_token): +        if username: +            return True +        return False + +class HtpasswdPasswordManager(PasswordManager): +    """ Read usernames and passwords from a file created by Apache htpasswd""" + +    def __init__(self, filehandle): +        PasswordManager.__init__(self) +        entries = (line.strip().split(':') for line in filehandle) +        valid_entries = (entry for entry in entries if len(entry)==2) +        self.usernames = {username:token for username,token in valid_entries} + + +    def test(self, username, password_token): +        if username not in self.usernames: +            return False +        full_token = self.usernames[username] +        dummy, magic, salt, hashed_password = full_token.split('$') +        expected = md5crypt.md5crypt(password_token, salt, '$'+magic+'$') +        return expected==full_token + +class SingleUserPasswordManager(PasswordManager): + +    def __init__(self, username, password): +        PasswordManager.__init__(self) +        self.username = username +        self.password = password + +    def test(self, username, password_token): +        return self.username==username and self.password==password_token diff --git a/libmproxy/cmdline.py b/libmproxy/cmdline.py index 279c3cb4..db1ebf0d 100644 --- a/libmproxy/cmdline.py +++ b/libmproxy/cmdline.py @@ -15,7 +15,7 @@  import proxy  import re, filt - +import argparse  class ParseException(Exception): pass  class OptionException(Exception): pass @@ -334,4 +334,50 @@ def common_options(parser):          help="Header set pattern."      ) + +    group = parser.add_argument_group( +        "Proxy Authentication", +        """ +            Specification of which users are allowed to access the proxy and the method used for authenticating them. +            If authscheme is specified, one must specify a list of authorized users and their passwords. +            In case that authscheme is not specified, or set to None, any list of authorized users will be ignored. +        """.strip() +    ) + +    group.add_argument( +        "--authscheme", type=str, +        action="store", dest="authscheme", default=None, choices=["none", "basic"], +        help=""" +            Specify the scheme used by the proxy to identify users. +            If not none, requires the specification of a list of authorized users. +            This option is ignored if the proxy is in transparent or reverse mode. +            """.strip() + +    ) + +    user_specification_group = group.add_mutually_exclusive_group() + + +    user_specification_group.add_argument( +        "--nonanonymous", +        action="store_true", dest="auth_nonanonymous", +        help="Allow access to any user as long as a username is specified. Ignores the provided password." +    ) + +    user_specification_group.add_argument( +        "--singleuser", +        action="store", dest="auth_singleuser", type=str, +        help="Allows access to a single user as specified by the option value. Specify a username and password in the form username:password." +    ) + +    user_specification_group.add_argument( +        "--htpasswd", +        action="store", dest="auth_htpasswd", type=argparse.FileType('r'), +        help="Allow access to users specified in an Apache htpasswd file." +    ) + + + + +      proxy.certificate_option_group(parser) diff --git a/libmproxy/contrib/README b/libmproxy/contrib/README index 61ae29e2..2506ad6d 100644 --- a/libmproxy/contrib/README +++ b/libmproxy/contrib/README @@ -11,4 +11,5 @@ jsbeautifier, git checkout 25/03/12, MIT license  html2text, git checkout 18/08/12, GPLv3 +md5crypt, PSF license, http://code.activestate.com/recipes/325204/ diff --git a/libmproxy/contrib/md5crypt.py b/libmproxy/contrib/md5crypt.py new file mode 100644 index 00000000..d64ea8ac --- /dev/null +++ b/libmproxy/contrib/md5crypt.py @@ -0,0 +1,94 @@ +# Based on FreeBSD src/lib/libcrypt/crypt.c 1.2 +# http://www.freebsd.org/cgi/cvsweb.cgi/~checkout~/src/lib/libcrypt/crypt.c?rev=1.2&content-type=text/plain + +# Original license: +# * "THE BEER-WARE LICENSE" (Revision 42): +# * <phk@login.dknet.dk> wrote this file.  As long as you retain this notice you +# * can do whatever you want with this stuff. If we meet some day, and you think +# * this stuff is worth it, you can buy me a beer in return.   Poul-Henning Kamp + +# This port adds no further stipulations.  I forfeit any copyright interest. + +import md5 + +def md5crypt(password, salt, magic='$1$'): +    # /* The password first, since that is what is most unknown */ /* Then our magic string */ /* Then the raw salt */ +    m = md5.new() +    m.update(password + magic + salt) + +    # /* Then just as many characters of the MD5(pw,salt,pw) */ +    mixin = md5.md5(password + salt + password).digest() +    for i in range(0, len(password)): +        m.update(mixin[i % 16]) + +    # /* Then something really weird... */ +    # Also really broken, as far as I can tell.  -m +    i = len(password) +    while i: +        if i & 1: +            m.update('\x00') +        else: +            m.update(password[0]) +        i >>= 1 + +    final = m.digest() + +    # /* and now, just to make sure things don't run too fast */ +    for i in range(1000): +        m2 = md5.md5() +        if i & 1: +            m2.update(password) +        else: +            m2.update(final) + +        if i % 3: +            m2.update(salt) + +        if i % 7: +            m2.update(password) + +        if i & 1: +            m2.update(final) +        else: +            m2.update(password) + +        final = m2.digest() + +    # This is the bit that uses to64() in the original code. + +    itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' + +    rearranged = '' +    for a, b, c in ((0, 6, 12), (1, 7, 13), (2, 8, 14), (3, 9, 15), (4, 10, 5)): +        v = ord(final[a]) << 16 | ord(final[b]) << 8 | ord(final[c]) +        for i in range(4): +            rearranged += itoa64[v & 0x3f]; v >>= 6 + +    v = ord(final[11]) +    for i in range(2): +        rearranged += itoa64[v & 0x3f]; v >>= 6 + +    return magic + salt + '$' + rearranged + +if __name__ == '__main__': + +    def test(clear_password, the_hash): +        magic, salt = the_hash[1:].split('$')[:2] +        magic = '$' + magic + '$' +        return md5crypt(clear_password, salt, magic) == the_hash + +    test_cases = ( +        (' ', '$1$yiiZbNIH$YiCsHZjcTkYd31wkgW8JF.'), +        ('pass', '$1$YeNsbWdH$wvOF8JdqsoiLix754LTW90'), +        ('____fifteen____', '$1$s9lUWACI$Kk1jtIVVdmT01p0z3b/hw1'), +        ('____sixteen_____', '$1$dL3xbVZI$kkgqhCanLdxODGq14g/tW1'), +        ('____seventeen____', '$1$NaH5na7J$j7y8Iss0hcRbu3kzoJs5V.'), +        ('__________thirty-three___________', '$1$HO7Q6vzJ$yGwp2wbL5D7eOVzOmxpsy.'), +        ('apache', '$apr1$J.w5a/..$IW9y6DR0oO/ADuhlMF5/X1') +    ) + +    for clearpw, hashpw in test_cases: +        if test(clearpw, hashpw): +            print '%s: pass' % clearpw +        else: +            print '%s: FAIL' % clearpw diff --git a/libmproxy/proxy.py b/libmproxy/proxy.py index 961ee465..b1ce310c 100644 --- a/libmproxy/proxy.py +++ b/libmproxy/proxy.py @@ -18,16 +18,18 @@ import SocketServer  from OpenSSL import SSL  from netlib import odict, tcp, http, wsgi, certutils, http_status  import utils, flow, version, platform, controller +import authentication  class ProxyError(Exception): -    def __init__(self, code, msg): -        self.code, self.msg = code, msg +    def __init__(self, code, msg, headers=None): +        self.code, self.msg, self.headers = code, msg, headers      def __str__(self):          return "ProxyError(%s, %s)"%(self.code, self.msg) +  class Log(controller.Msg):      def __init__(self, msg):          controller.Msg.__init__(self) @@ -36,7 +38,7 @@ class Log(controller.Msg):  class ProxyConfig: -    def __init__(self, certfile = None, cacert = None, clientcerts = None, cert_wait_time=0, no_upstream_cert=False, body_size_limit = None, reverse_proxy=None, transparent_proxy=None, certdir = None): +    def __init__(self, certfile = None, cacert = None, clientcerts = None, cert_wait_time=0, no_upstream_cert=False, body_size_limit = None, reverse_proxy=None, transparent_proxy=None, certdir = None, authenticator=None):          assert not (reverse_proxy and transparent_proxy)          self.certfile = certfile          self.cacert = cacert @@ -47,7 +49,7 @@ class ProxyConfig:          self.body_size_limit = body_size_limit          self.reverse_proxy = reverse_proxy          self.transparent_proxy = transparent_proxy - +        self.authenticator = authenticator  class RequestReplayThread(threading.Thread):      def __init__(self, config, flow, masterq): @@ -217,7 +219,7 @@ class ProxyHandler(tcp.BaseHandler):                  self.log(cc, cc.error)              if isinstance(e, ProxyError): -                self.send_error(e.code, e.msg) +                self.send_error(e.code, e.msg, e.headers)          else:              return True @@ -283,9 +285,7 @@ class ProxyHandler(tcp.BaseHandler):              if not r:                  raise ProxyError(400, "Bad HTTP request line: %s"%repr(line))              method, path, httpversion = r -            headers = http.read_headers(self.rfile) -            if headers is None: -                raise ProxyError(400, "Invalid headers") +            headers = self.read_headers(authenticate=False)              content = http.read_http_body_request(                          self.rfile, self.wfile, headers, httpversion, self.config.body_size_limit                      ) @@ -299,9 +299,7 @@ class ProxyHandler(tcp.BaseHandler):              if not r:                  raise ProxyError(400, "Bad HTTP request line: %s"%repr(line))              method, path, httpversion = r -            headers = http.read_headers(self.rfile) -            if headers is None: -                raise ProxyError(400, "Invalid headers") +            headers = self.read_headers(authenticate=False)              content = http.read_http_body_request(                          self.rfile, self.wfile, headers, httpversion, self.config.body_size_limit                      ) @@ -315,12 +313,9 @@ class ProxyHandler(tcp.BaseHandler):                  if not r:                      raise ProxyError(400, "Bad HTTP request line: %s"%repr(line))                  host, port, httpversion = r -                # FIXME: Discard additional headers sent to the proxy. Should I expose -                # these to users? -                while 1: -                    d = self.rfile.readline() -                    if d == '\r\n' or d == '\n': -                        break + +                headers = self.read_headers(authenticate=True) +                  self.wfile.write(                              'HTTP/1.1 200 Connection established\r\n' +                              ('Proxy-agent: %s\r\n'%self.server_version) + @@ -340,9 +335,8 @@ class ProxyHandler(tcp.BaseHandler):                  if not r:                      raise ProxyError(400, "Bad HTTP request line: %s"%repr(line))                  method, path, httpversion = r -                headers = http.read_headers(self.rfile) -                if headers is None: -                    raise ProxyError(400, "Invalid headers") +                headers = self.read_headers(authenticate=False) +                  content = http.read_http_body_request(                      self.rfile, self.wfile, headers, httpversion, self.config.body_size_limit                  ) @@ -352,14 +346,20 @@ class ProxyHandler(tcp.BaseHandler):                  if not r:                      raise ProxyError(400, "Bad HTTP request line: %s"%repr(line))                  method, scheme, host, port, path, httpversion = http.parse_init_proxy(line) -                headers = http.read_headers(self.rfile) -                if headers is None: -                    raise ProxyError(400, "Invalid headers") +                headers = self.read_headers(authenticate=True)                  content = http.read_http_body_request(                      self.rfile, self.wfile, headers, httpversion, self.config.body_size_limit                  )                  return flow.Request(client_conn, httpversion, host, port, scheme, method, path, headers, content) +    def read_headers(self, authenticate=False): +        headers = http.read_headers(self.rfile) +        if headers is None: +            raise ProxyError(400, "Invalid headers") +        if authenticate and self.config.authenticator and not self.config.authenticator.authenticate(headers.get('Proxy-Authorization', [])): +            raise ProxyError(407, "Proxy Authentication Required", self.config.authenticator.auth_challenge_headers()) +        return headers +      def send_response(self, response):          d = response._assemble()          if not d: @@ -367,16 +367,19 @@ class ProxyHandler(tcp.BaseHandler):          self.wfile.write(d)          self.wfile.flush() -    def send_error(self, code, body): +    def send_error(self, code, body, headers):          try:              response = http_status.RESPONSES.get(code, "Unknown") +            html_content = '<html><head>\n<title>%d %s</title>\n</head>\n<body>\n%s\n</body>\n</html>'%(code, response, body)              self.wfile.write("HTTP/1.1 %s %s\r\n" % (code, response))              self.wfile.write("Server: %s\r\n"%self.server_version) -            self.wfile.write("Connection: close\r\n")              self.wfile.write("Content-type: text/html\r\n") +            self.wfile.write("Content-Length: %d\r\n"%len(html_content)) +            for key, value in headers.items(): +                self.wfile.write("%s: %s\r\n"%(key, value)) +            self.wfile.write("Connection: close\r\n")              self.wfile.write("\r\n") -            self.wfile.write('<html><head>\n<title>%d %s</title>\n</head>\n' -                    '<body>\n%s\n</body>\n</html>' % (code, response, body)) +            self.wfile.write(html_content)              self.wfile.flush()          except:              pass @@ -531,6 +534,25 @@ def process_proxy_options(parser, options):          if not os.path.exists(options.certdir) or not os.path.isdir(options.certdir):              parser.error("Dummy cert directory does not exist or is not a directory: %s"%options.certdir) +    if options.authscheme and (options.authscheme!='none'): +        if not (options.auth_nonanonymous or options.auth_singleuser or options.auth_htpasswd): +            parser.error("Proxy authentication scheme is specified, but no allowed user list is given.") +        if options.auth_singleuser and len(options.auth_singleuser.split(':'))!=2: +            parser.error("Authorized user is not given in correct format username:password") +        if options.auth_nonanonymous: +            password_manager = authentication.PermissivePasswordManager() +        elif options.auth_singleuser: +            username, password = options.auth_singleuser.split(':') +            password_manager = authentication.SingleUserPasswordManager(username, password) +        elif options.auth_htpasswd: +            password_manager = authentication.HtpasswdPasswordManager(options.auth_htpasswd) +        # in the meanwhile, basic auth is the only true authentication scheme we support +        # so just use it +        authenticator = authentication.BasicProxyAuth(password_manager) +    else: +        authenticator = authentication.NullProxyAuth(None) + +      return ProxyConfig(          certfile = options.cert,          cacert = cacert, @@ -540,5 +562,6 @@ def process_proxy_options(parser, options):          no_upstream_cert = options.no_upstream_cert,          reverse_proxy = rp,          transparent_proxy = trans, -        certdir = options.certdir +        certdir = options.certdir, +        authenticator = authenticator      )  | 
