aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAldo Cortesi <aldo@corte.si>2012-12-30 11:27:04 -0800
committerAldo Cortesi <aldo@corte.si>2012-12-30 11:27:04 -0800
commitcfab27232127437cca8ac3699065db653da97dba (patch)
tree60a92d02b45daf421a0329e4d404652b8f6b3baa
parent3c8dcf880860191ef3bd000f95cf7ea980c6bda9 (diff)
parent440a9f6bda8d645945e8c056a5e869c712dd2d69 (diff)
downloadmitmproxy-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.py95
-rw-r--r--libmproxy/cmdline.py48
-rw-r--r--libmproxy/contrib/README1
-rw-r--r--libmproxy/contrib/md5crypt.py94
-rw-r--r--libmproxy/proxy.py79
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
)