Merge pull request #83 from rouli/master

Adding some basic proxy authentication code
This commit is contained in:
Aldo Cortesi 2012-12-30 11:27:04 -08:00
commit cfab272321
5 changed files with 288 additions and 29 deletions

View File

@ -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

View File

@ -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)

View File

@ -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/

View File

@ -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

View File

@ -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
)