<commit_before>#!/usr/bin/env python2
# vim: set noexpandtab:ts=4

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# Contributors: <EMAIL>

import sys
import imp
import os

cfg_path = ['duo_openvpn.conf', '/etc/openvpn/duo_openvpn.conf', '/etc/duo_openvpn.conf']
config = None

for cfg in cfg_path:
	if os.path.isfile(cfg):
		try:
			config = imp.load_source('config', cfg)
		except:
			pass

if config == None:
	print("Failed to load config")
	sys.exit(1)

sys.path.append('duo_client')

import duo_client
import time
import socket
import traceback
import syslog
import cPickle as pickle
if config.LDAP_CONTROL_BIND_DN:
	import ldap
if config.LOG_METHOD == 'mozdef':
	import mozdef

def log(msg):
	if config.LOG_METHOD == 'mozdef':
		mozmsg = mozdef.MozDefMsg(config.MOZDEF_URL, tags=['openvpn', 'duosecurity'])
		mozmsg.send(msg)
	else:
		if config.LOG_METHOD == 'cef':
			msg = cef(msg)
		syslog.openlog('duo_openvpn', 0, syslog.LOG_DAEMON)
		syslog.syslog(syslog.LOG_INFO, msg)
		syslog.closelog()

def cef(title="DuoAPI", msg="", ext=""):
	hostname = socket.gethostname()
	cefmsg = 'CEF:{v}|{deviceVendor}|{deviceProduct}|{deviceVersion}|{signatureID}|{name}|{message}|{deviceSeverity}|{extension}'.format(
					v='0',
					deviceVendor='Mozilla',
					deviceProduct='OpenVPN',
					deviceVersion='1.0',
					signatureID='0',
					name=title,
					message=msg,
					deviceSeverity='5',
					extension=ext+' dhost=' + hostname,
				)
	return cefmsg

def ldap_get_dn(username):
	dn = ldap_attr_get(config.LDAP_URL, config.LDAP_CONTROL_BIND_DN, config.LDAP_CONTROL_PASSWORD, config.LDAP_BASE_DN, 'mail='+username, 'dn', True)
	return dn

def ldap_attr_get(url, binddn, password, basedn, value_filter, attr, attr_key=False):
	conn = ldap.initialize(url)
	try:
		conn.bind_s(binddn, password)
	except ldap.LDAPError, e:
		conn.unbind_s()
		log('LDAP bind failed' % e)
		return None

	try:
		res = conn.search_s(basedn, ldap.SCOPE_SUBTREE, value_filter, [attr])
		#list of attributes
		if attr_key:
			return res[0][0]
		else:
			return res[0][1][attr]
	except:
		log('ldap_attr_get() filter search failed for %s=>%s (returning key? %s)' % (value_filter, attr, attr_key))
		return None

def ldap_auth(username, user_dn, password):
	if (username == None) or (password == None) or (user_dn == None):
		log('User %s LDAP authentication failed' % username)
		return False

	conn = ldap.initialize(config.LDAP_URL)
	try:
		conn.bind_s(user_dn, password)
		conn.unbind_s()
		log('User %s successfully authenticated against LDAP' % username)
		return True
	except ldap.LDAPError:
		conn.unbind_s()
	log('User %s LDAP authentication failed' % username)
	return False

class DuoAPIAuth:
	def __init__(self, ikey, skey, host, username, client_ipaddr, factor, passcode, username_hack, failmode, cache_path, cache_time):
		self.failmode = failmode
		self.passcode = passcode
		if username_hack:
			self.username = self.clean_username(username)
		else:
			self.username = username
		self.client_ipaddr = client_ipaddr
		self.hostname = socket.gethostname()
		self.username = username
		self.client_ipaddr = client_ipaddr
		self.factor = factor
		self.user_cache_time = cache_time

		self.auth_api = duo_client.Auth(
			ikey=ikey,
			skey=skey,
			host=host)

		if os.path.isfile(cache_path):
			self.cache_path = cache_path
			self.user_cache = pickle.load(open(cache_path, "rb"))
		else:
			self.cache_path = None
			self.user_cache = {}

	def fail_open(self):
		return self.failmode

	def is_auth_cached(self):
		now = time.time()
		if self.user_cache.has_key(self.username):
			try:
				tleft = self.user_cache[self.username]['timestamp']
				ipaddr = self.user_cache[self.username]['ipaddr']
			except:
				return False
			if self.client_ipaddr == ipaddr and tleft > now:
				return True
			del self.user_cache[self.username]
		return False

	def add_auth_cache(self):
		# No IP? No cache for you.
		if self.client_ipaddr == '0.0.0.0':
			return
		if self.cache_path == None
			return
		now = time.time()
		self.user_cache[self.username] = {'timestamp': now+self.user_cache_time, 'ipaddr': self.client_ipaddr}
		pickle.dump(self.user_cache, open(self.cache_path, "wb"))

	def ping(self):
		now = time.time()
		if not self.auth_api.ping():
			log('DuoAPI not responding')
		end = time.time()-now
		log('DuoAPI responded in %s seconds' % end)

	def check(self):
		if not self.auth_api.check():
			log('DuoAPI IKEY, SKEY or HOST are invalid')

	def clean_username(self, username):
		# use first part of email if an email is present
		try:
			if (username.find('@') != -1):
				username = username.split('@')[0]
		except:
			log('Failed to clean_username()')
			return username
		return username

	def preauth(self):
		res = self.auth_api.preauth(self.username)
		return res['result']

	def doauth(self):
		if self.passcode:
			res = self.auth_api.auth(username=self.username, factor=self.factor, ipaddr=self.client_ipaddr,
								passcode=self.passcode)
		else:
			res = self.auth_api.auth(username=self.username, factor=self.factor, ipaddr=self.client_ipaddr,
								type="OpenVPN login", pushinfo="From%20server="+self.hostname, device="auto")
		return res

	def auth(self):
		try:
			self.ping()
			self.check()
			auth = self.preauth()
		except socket.error, s:
			log('DuoAPI contact failed %s' % (s))
			return self.fail_open()

		if auth == "allow":
			return True
		elif auth == "enroll":
			log('User %s needs to enroll first' % self.username)
			return False
		elif auth == "auth":
			log('User %s is known - authenticating' % self.username)

			# Auth bypass for cached usernames
			if self.is_auth_cached():
				log('User %s cached authentication success' % self.username)
				return True

			try:
				res = self.doauth()
			except socket.error, s:
				log('DuoAPI contact failed %s' % (s))
				return self.fail_open()

			if res['result'] == 'allow':
				log('User %s is now authenticated with DuoAPI using %s' % (self.username, self.factor))
				self.add_auth_cache()
				return True

			log('User %s authentication failed: %s' % (self.username, res['status_msg']))
			return False
		else:
			log('User %s is not allowed to authenticate' % self.username)
			return False

def main():
	username_trusted = True
	username = os.environ.get('common_name')
	if (username == None):
# take username as provided by the user - it cannot be trusted
		username = os.environ.get('username')
		username_trusted = True
	client_ipaddr = os.environ.get('untrusted_ip', '0.0.0.0')
	password = os.environ.get('password', 'auto')
	passcode = None
	factor = None

# Nope? then nope.
	if username == None or password == None or password == '':
		log("User %s (%s) Missing username or password, (reported username may be None due to this)" % (username,
			client_ipaddr))
		return False

# If your password is push/sms/phone/auto then you don't deserve to use this anyway :P
	if password not in ['push', 'sms', 'phone', 'auto']:
		if (password.isdigit() and len(password) == 6 or len(password) == 8):<commit_msg>SECURITY: fix username_trusted handling Also fail if username is empty (nicer exit msg)<commit_after>#!/usr/bin/env python2
# vim: set noexpandtab:ts=4

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# Contributors: <EMAIL>

import sys
import imp
import os

cfg_path = ['duo_openvpn.conf', '/etc/openvpn/duo_openvpn.conf', '/etc/duo_openvpn.conf']
config = None

for cfg in cfg_path:
	if os.path.isfile(cfg):
		try:
			config = imp.load_source('config', cfg)
		except:
			pass

if config == None:
	print("Failed to load config")
	sys.exit(1)

sys.path.append('duo_client')

import duo_client
import time
import socket
import traceback
import syslog
import cPickle as pickle
if config.LDAP_CONTROL_BIND_DN:
	import ldap
if config.LOG_METHOD == 'mozdef':
	import mozdef

def log(msg):
	if config.LOG_METHOD == 'mozdef':
		mozmsg = mozdef.MozDefMsg(config.MOZDEF_URL, tags=['openvpn', 'duosecurity'])
		mozmsg.send(msg)
	else:
		if config.LOG_METHOD == 'cef':
			msg = cef(msg)
		syslog.openlog('duo_openvpn', 0, syslog.LOG_DAEMON)
		syslog.syslog(syslog.LOG_INFO, msg)
		syslog.closelog()

def cef(title="DuoAPI", msg="", ext=""):
	hostname = socket.gethostname()
	cefmsg = 'CEF:{v}|{deviceVendor}|{deviceProduct}|{deviceVersion}|{signatureID}|{name}|{message}|{deviceSeverity}|{extension}'.format(
					v='0',
					deviceVendor='Mozilla',
					deviceProduct='OpenVPN',
					deviceVersion='1.0',
					signatureID='0',
					name=title,
					message=msg,
					deviceSeverity='5',
					extension=ext+' dhost=' + hostname,
				)
	return cefmsg

def ldap_get_dn(username):
	dn = ldap_attr_get(config.LDAP_URL, config.LDAP_CONTROL_BIND_DN, config.LDAP_CONTROL_PASSWORD, config.LDAP_BASE_DN, 'mail='+username, 'dn', True)
	return dn

def ldap_attr_get(url, binddn, password, basedn, value_filter, attr, attr_key=False):
	conn = ldap.initialize(url)
	try:
		conn.bind_s(binddn, password)
	except ldap.LDAPError, e:
		conn.unbind_s()
		log('LDAP bind failed' % e)
		return None

	try:
		res = conn.search_s(basedn, ldap.SCOPE_SUBTREE, value_filter, [attr])
		#list of attributes
		if attr_key:
			return res[0][0]
		else:
			return res[0][1][attr]
	except:
		log('ldap_attr_get() filter search failed for %s=>%s (returning key? %s)' % (value_filter, attr, attr_key))
		return None

def ldap_auth(username, user_dn, password):
	if (username == None) or (password == None) or (user_dn == None):
		log('User %s LDAP authentication failed' % username)
		return False

	conn = ldap.initialize(config.LDAP_URL)
	try:
		conn.bind_s(user_dn, password)
		conn.unbind_s()
		log('User %s successfully authenticated against LDAP' % username)
		return True
	except ldap.LDAPError:
		conn.unbind_s()
	log('User %s LDAP authentication failed' % username)
	return False

class DuoAPIAuth:
	def __init__(self, ikey, skey, host, username, client_ipaddr, factor, passcode, username_hack, failmode, cache_path, cache_time):
		self.failmode = failmode
		self.passcode = passcode
		if username_hack:
			self.username = self.clean_username(username)
		else:
			self.username = username
		self.client_ipaddr = client_ipaddr
		self.hostname = socket.gethostname()
		self.username = username
		self.client_ipaddr = client_ipaddr
		self.factor = factor
		self.user_cache_time = cache_time

		self.auth_api = duo_client.Auth(
			ikey=ikey,
			skey=skey,
			host=host)

		if os.path.isfile(cache_path):
			self.cache_path = cache_path
			self.user_cache = pickle.load(open(cache_path, "rb"))
		else:
			self.cache_path = None
			self.user_cache = {}

	def fail_open(self):
		return self.failmode

	def is_auth_cached(self):
		now = time.time()
		if self.user_cache.has_key(self.username):
			try:
				tleft = self.user_cache[self.username]['timestamp']
				ipaddr = self.user_cache[self.username]['ipaddr']
			except:
				return False
			if self.client_ipaddr == ipaddr and tleft > now:
				return True
			del self.user_cache[self.username]
		return False

	def add_auth_cache(self):
		# No IP? No cache for you.
		if self.client_ipaddr == '0.0.0.0':
			return
		if self.cache_path == None
			return
		now = time.time()
		self.user_cache[self.username] = {'timestamp': now+self.user_cache_time, 'ipaddr': self.client_ipaddr}
		pickle.dump(self.user_cache, open(self.cache_path, "wb"))

	def ping(self):
		now = time.time()
		if not self.auth_api.ping():
			log('DuoAPI not responding')
		end = time.time()-now
		log('DuoAPI responded in %s seconds' % end)

	def check(self):
		if not self.auth_api.check():
			log('DuoAPI IKEY, SKEY or HOST are invalid')

	def clean_username(self, username):
		# use first part of email if an email is present
		try:
			if (username.find('@') != -1):
				username = username.split('@')[0]
		except:
			log('Failed to clean_username()')
			return username
		return username

	def preauth(self):
		res = self.auth_api.preauth(self.username)
		return res['result']

	def doauth(self):
		if self.passcode:
			res = self.auth_api.auth(username=self.username, factor=self.factor, ipaddr=self.client_ipaddr,
								passcode=self.passcode)
		else:
			res = self.auth_api.auth(username=self.username, factor=self.factor, ipaddr=self.client_ipaddr,
								type="OpenVPN login", pushinfo="From%20server="+self.hostname, device="auto")
		return res

	def auth(self):
		try:
			self.ping()
			self.check()
			auth = self.preauth()
		except socket.error, s:
			log('DuoAPI contact failed %s' % (s))
			return self.fail_open()

		if auth == "allow":
			return True
		elif auth == "enroll":
			log('User %s needs to enroll first' % self.username)
			return False
		elif auth == "auth":
			log('User %s is known - authenticating' % self.username)

			# Auth bypass for cached usernames
			if self.is_auth_cached():
				log('User %s cached authentication success' % self.username)
				return True

			try:
				res = self.doauth()
			except socket.error, s:
				log('DuoAPI contact failed %s' % (s))
				return self.fail_open()

			if res['result'] == 'allow':
				log('User %s is now authenticated with DuoAPI using %s' % (self.username, self.factor))
				self.add_auth_cache()
				return True

			log('User %s authentication failed: %s' % (self.username, res['status_msg']))
			return False
		else:
			log('User %s is not allowed to authenticate' % self.username)
			return False

def main():
	username_trusted = True
	username = os.environ.get('common_name')
	if (username == None):
# take username as provided by the user - it cannot be trusted
		username = os.environ.get('username')
		username_trusted = False
	client_ipaddr = os.environ.get('untrusted_ip', '0.0.0.0')
	password = os.environ.get('password', 'auto')
	passcode = None
	factor = None

# Nope? then nope.
	if username == None or username == '' or password == None or password == '':
		log("User %s (%s) Missing username or password, (reported username may be None due to this)" % (username,
			client_ipaddr))
		return False

# If your password is push/sms/phone/auto then you don't deserve to use this anyway :P
	if password not in ['push', 'sms', 'phone', 'auto']:
		if (password.isdigit() and len(password) == 6 or len(password) == 8):
{'fraction_non_alphanumeric': 0.13137213992628138, 'fraction_numerical': 0.00431184366089436, 'mean_word_length': 4.1338807568725455, 'pattern_counts': {'":': 6, '<': 5, '<?xml version=': 0, '>': 9, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 4, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2703697', 'n_tokens_mistral': 5656, 'n_tokens_neox': 5092, 'n_words': 1311}