#!/usr/bin/python2.7 -u
# -*- coding: utf-8 -*-
#
# Univention openldap ntlm squid authenticator
#
# Copyright 2012-2021 Univention GmbH
# Copyright 2002 Yee Man Chan
# Copyright 2001 Dmitry A. Rozmanov
#
# https://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <https://www.gnu.org/licenses/>.
#
# see also
#   * http://code.google.com/p/python-ntlm/
#   * http://davenport.sourceforge.net/ntlm.html
#   * http://squid.sourceforge.net/ntlm/squid_helper_protocol.html
#   * http://www.innovation.ch/personal/ronald/ntlm.html
#   * http://www.koders.com/python/fidEE921E55A2EC9CF12BE39BFAA74346769EE6F2FF.aspx

# auth negotiate
# auth_param negotiate program /usr/lib/squid/squid_ldap_ntlm_auth --gss-spnego
# auth_param negotiate children 10
#
# auth ntlm
# auth_param ntlm program /usr/lib/squid/squid_ldap_ntlm_auth
# auth_param ntlm children 10
# auth_param ntlm keep_alive on
#
# debug
# squid/debug/level: ALL,1 29,9
# /usr/lib/squid/squid_ldap_ntlm_auth --debug

import sys
import time
import struct
import base64
import random
import hmac
import hashlib
import string
import Crypto.Cipher.DES
import optparse
import os
import subprocess

import univention.uldap
import univention.config_registry

NTLM_NegotiateUnicode = 0x00000001
NTLM_NegotiateOEM = 0x00000002
NTLM_RequestTarget = 0x00000004
NTLM_Unknown9 = 0x00000008
NTLM_NegotiateSign = 0x00000010
NTLM_NegotiateSeal = 0x00000020
NTLM_NegotiateDatagram = 0x00000040
NTLM_NegotiateLanManagerKey = 0x00000080
NTLM_Unknown8 = 0x00000100
NTLM_NegotiateNTLM = 0x00000200
NTLM_NegotiateNTOnly = 0x00000400
NTLM_Anonymous = 0x00000800
NTLM_NegotiateOemDomainSupplied = 0x00001000
NTLM_NegotiateOemWorkstationSupplied = 0x00002000
NTLM_Unknown6 = 0x00004000
NTLM_NegotiateAlwaysSign = 0x00008000
NTLM_TargetTypeDomain = 0x00010000
NTLM_TargetTypeServer = 0x00020000
NTLM_TargetTypeShare = 0x00040000
NTLM_NegotiateExtendedSecurity = 0x00080000
NTLM_NegotiateIdentify = 0x00100000
NTLM_Unknown5 = 0x00200000
NTLM_RequestNonNTSessionKey = 0x00400000
NTLM_NegotiateTargetInfo = 0x00800000
NTLM_Unknown4 = 0x01000000
NTLM_NegotiateVersion = 0x02000000
NTLM_Unknown3 = 0x04000000
NTLM_Unknown2 = 0x08000000
NTLM_Unknown1 = 0x10000000
NTLM_Negotiate128 = 0x20000000
NTLM_NegotiateKeyExchange = 0x40000000
NTLM_Negotiate56 = 0x80000000

NTLMSSP_CHALLENGE = 2

NTLM_TYPE2_FLAGS = (
	NTLM_NegotiateUnicode |
	NTLM_RequestTarget |
	NTLM_NegotiateNTLM |
	NTLM_NegotiateExtendedSecurity |
	NTLM_Negotiate128 |
	NTLM_NegotiateTargetInfo |
	NTLM_TargetTypeDomain |
	NTLM_Negotiate56)


def dumpNegotiateFlags(NegotiateFlags):

	flags = []

	if NegotiateFlags & NTLM_NegotiateUnicode:
		flags.append("NTLM_NegotiateUnicode set")
	if NegotiateFlags & NTLM_NegotiateOEM:
		flags.append("NTLM_NegotiateOEM set")
	if NegotiateFlags & NTLM_RequestTarget:
		flags.append("NTLM_RequestTarget set")
	if NegotiateFlags & NTLM_Unknown9:
		flags.append("NTLM_Unknown9 set")
	if NegotiateFlags & NTLM_NegotiateSign:
		flags.append("NTLM_NegotiateSign set")
	if NegotiateFlags & NTLM_NegotiateSeal:
		flags.append("NTLM_NegotiateSeal set")
	if NegotiateFlags & NTLM_NegotiateDatagram:
		flags.append("NTLM_NegotiateDatagram set")
	if NegotiateFlags & NTLM_NegotiateLanManagerKey:
		flags.append("NTLM_NegotiateLanManagerKey set")
	if NegotiateFlags & NTLM_Unknown8:
		flags.append("NTLM_Unknown8 set")
	if NegotiateFlags & NTLM_NegotiateNTLM:
		flags.append("NTLM_NegotiateNTLM set")
	if NegotiateFlags & NTLM_NegotiateNTOnly:
		flags.append("NTLM_NegotiateNTOnly set")
	if NegotiateFlags & NTLM_Anonymous:
		flags.append("NTLM_Anonymous set")
	if NegotiateFlags & NTLM_NegotiateOemDomainSupplied:
		flags.append("NTLM_NegotiateOemDomainSupplied set")
	if NegotiateFlags & NTLM_NegotiateOemWorkstationSupplied:
		flags.append("NTLM_NegotiateOemWorkstationSupplied set")
	if NegotiateFlags & NTLM_Unknown6:
		flags.append("NTLM_Unknown6 set")
	if NegotiateFlags & NTLM_NegotiateAlwaysSign:
		flags.append("NTLM_NegotiateAlwaysSign set")
	if NegotiateFlags & NTLM_TargetTypeDomain:
		flags.append("NTLM_TargetTypeDomain set")
	if NegotiateFlags & NTLM_TargetTypeServer:
		flags.append("NTLM_TargetTypeServer set")
	if NegotiateFlags & NTLM_TargetTypeShare:
		flags.append("NTLM_TargetTypeShare set")
	if NegotiateFlags & NTLM_NegotiateExtendedSecurity:
		flags.append("NTLM_NegotiateExtendedSecurity set")
	if NegotiateFlags & NTLM_NegotiateIdentify:
		flags.append("NTLM_NegotiateIdentify set")
	if NegotiateFlags & NTLM_Unknown5:
		flags.append("NTLM_Unknown5 set")
	if NegotiateFlags & NTLM_RequestNonNTSessionKey:
		flags.append("NTLM_RequestNonNTSessionKey set")
	if NegotiateFlags & NTLM_NegotiateTargetInfo:
		flags.append("NTLM_NegotiateTargetInfo set")
	if NegotiateFlags & NTLM_Unknown4:
		flags.append("NTLM_Unknown4 set")
	if NegotiateFlags & NTLM_NegotiateVersion:
		flags.append("NTLM_NegotiateVersion set")
	if NegotiateFlags & NTLM_Unknown3:
		flags.append("NTLM_Unknown3 set")
	if NegotiateFlags & NTLM_Unknown2:
		flags.append("NTLM_Unknown2 set")
	if NegotiateFlags & NTLM_Unknown1:
		flags.append("NTLM_Unknown1 set")
	if NegotiateFlags & NTLM_Negotiate128:
		flags.append("NTLM_Negotiate128 set")
	if NegotiateFlags & NTLM_NegotiateKeyExchange:
		flags.append("NTLM_NegotiateKeyExchange set")
	if NegotiateFlags & NTLM_Negotiate56:
		flags.append("NTLM_Negotiate56 set")

	return flags


def debug(msg):

	if options.debug:
		fh = open(options.debugFile, "a")
		os.chmod(options.debugFile, 0o600)
		fh.write("%s - %s\n" % (time.time(), msg))
		fh.close()


def parseNtlmTypeThree(data):

	data = data.replace("KK ", "", 1)
	data = base64.b64decode(data)
	signature = data[0:8]
	type = struct.unpack("<I", data[8:12])[0]
	lmLen = struct.unpack("<h", data[12:14])[0]
	lmOffset = struct.unpack("<l", data[16:20])[0]
	ntLen = struct.unpack("<h", data[20:22])[0]
	ntOffset = struct.unpack("<l", data[24:28])[0]

	domainOffset = struct.unpack("<l", data[32:36])[0]
	usernameOffset = struct.unpack("<l", data[40:44])[0]
	hostLength = struct.unpack("<h", data[44:46])[0]
	hostOffset = struct.unpack("<l", data[48:52])[0]
	flags = struct.unpack("<I", data[60:64])[0]

	username = data[usernameOffset:hostOffset]
	domain = data[domainOffset:usernameOffset]
	host = data[hostOffset:hostOffset + hostLength]
	lmResp = data[lmOffset:(lmOffset + lmLen)]
	ntResp = data[ntOffset:(ntOffset + ntLen)]

	if domainOffset == 0:
		domain = ""
	if hostOffset == 0:
		host = ""
	if usernameOffset == 0:
		username = ""

	if flags & NTLM_NegotiateUnicode:
		username = username.decode("utf-16")
		domain = domain.decode("utf-16")
		host = host.decode("utf-16")

	return (username, domain, host, lmResp, ntResp, flags)


def convertKey(key):
	"""
	Converts a 7-bytes key to an 8-bytes key based on an algorithm.
	"""

	assert len(key) == 7, "NTLM convertKey needs 7-byte key"

	bytes = [
		key[0],
		chr(((ord(key[0]) << 7) & 0xFF) | (ord(key[1]) >> 1)),
		chr(((ord(key[1]) << 6) & 0xFF) | (ord(key[2]) >> 2)),
		chr(((ord(key[2]) << 5) & 0xFF) | (ord(key[3]) >> 3)),
		chr(((ord(key[3]) << 4) & 0xFF) | (ord(key[4]) >> 4)),
		chr(((ord(key[4]) << 3) & 0xFF) | (ord(key[5]) >> 5)),
		chr(((ord(key[5]) << 2) & 0xFF) | (ord(key[6]) >> 6)),
		chr((ord(key[6]) << 1) & 0xFF),
	]

	return "".join([setOddParity(b) for b in bytes])


def setOddParity(byte):
	"""
	Turns one-byte into odd parity. Odd parity means that a number in
	binary has odd number of 1's.
	"""

	assert len(byte) == 1

	parity = 0
	ordbyte = ord(byte)
	for dummy in range(8):
		if (ordbyte & 0x01) != 0:
			parity += 1
		ordbyte >>= 1
	ordbyte = ord(byte)
	if parity % 2 == 0:
		if (ordbyte & 0x01) != 0:
			ordbyte &= 0xFE
		else:
			ordbyte |= 0x01

	return chr(ordbyte)


def verifyNtlm(key, nonce):
	"""
	Takes a 21 byte array and treats it as 3 56-bit DES keys. The
	8 byte plaintext is encrypted with each key and the resulting 24
	bytes are stored in the result array
	"""

	assert len(key) == 21, "key must be 21 bytes long"
	assert len(nonce) == 8, "nonce must be 8 bytes long"

	res1 = Crypto.Cipher.DES.new(convertKey(key[0:7])).encrypt(nonce)
	res2 = Crypto.Cipher.DES.new(convertKey(key[7:14])).encrypt(nonce)
	res3 = Crypto.Cipher.DES.new(convertKey(key[14:21])).encrypt(nonce)

	return "%s%s%s" % (res1, res2, res3)


def verifyNtlm2(ResponseKeyNT, ServerChallenge, ClientChallenge):
	"""
	http://davenport.sourceforge.net/ntlm.html#theNtlm2SessionResponse
	"""

	nonce = ServerChallenge + ClientChallenge
	sess = hashlib.md5(nonce).digest()[0:8]
	NtChallengeResponse = verifyNtlm(ResponseKeyNT, sess)

	return NtChallengeResponse


def verifyNtlmV2(ntResp, challenge, user, domain, ntHashV1):
	"""
	http://davenport.sourceforge.net/ntlm.html#theType3Message
	"""

	# nt v2 hash
	inf = (user.upper() + domain).encode('utf-16le')
	ntHashV2 = hmac.new(ntHashV1, inf).digest()

	# get data from nt resp
	clientChallenge = ntResp[32:40]
	timestamp = ntResp[24:32]
	targetInformation = ntResp[44:]

	# const
	version = u'\x01' + u'\x01' + u'\x00' * 2
	reservered = u'\x00' * 4
	unknown = u'\x00' * 4

	# create blob
	blob = version.encode("hex") + reservered.encode("hex") + timestamp.encode("hex")
	blob = blob + clientChallenge.encode("hex") + unknown.encode("hex") + targetInformation.encode("hex")
	challengeBlob = challenge.encode("hex") + blob

	# secret
	secret = hmac.new(ntHashV2, challengeBlob.decode("hex")).digest()

	# response
	myResp = secret.encode("hex") + blob

	return myResp.decode("hex")


def getNtHash(user):

	global users
	user = user.lower()

	# check "cache"
	cacheHit = users.get(user)
	if cacheHit:
		if options.debug:
			debug("  found cache entry for %s" % user)
			debug("  time: %s" % time.time())
			debug("  cache time: %s" % cacheHit[1])
			debug("  cache lifetime: %s" % options.cacheLifeTime)
		if time.time() - cacheHit[1] < options.cacheLifeTime:
			if options.debug:
				debug("  cache entry for %s valid" % user)
			return cacheHit[0]
		else:
			if options.debug:
				debug("  cache entry for %s invalid -> new ldapsearch" % user)

	# search for ntHash and sambaAcctFlags of user
	ntHash = ""
	ldapFilter = "(uid=%s)" % user
	attr = ["sambaNTPassword", "sambaAcctFlags"]
	ldap = univention.uldap.getMachineConnection(ldap_master=False, secret_file="/etc/squid.secret")
	result = ldap.search(base=cr["ldap/base"], filter=ldapFilter, attr=attr)
	if options.debug:
		debug("  ldapsearch for sambaNTPassword of user %s" % user)
	if len(result) == 1:
		tmp = result[0][1].get("sambaNTPassword", [""])[0]
		sambaAcctFlags = result[0][1].get("sambaAcctFlags", [""])[0]
		if tmp and sambaAcctFlags:
			if options.debug:
				debug("  found sambaNTPassword in ldap for user %s with sambaAcctFlags %s" % (user, sambaAcctFlags))
			ntHash = tmp
			users[user] = [ntHash, time.time(), sambaAcctFlags]
	else:
		users[user] = ["", time.time(), ""]

	return ntHash


def verifyNtlmTypeThree(data, challenge):

	if options.debug:
		debug("NTLM Type 3 Message: ")

	# parse ntlm 3 message
	try:
		username, domain, host, lmResp, ntResp, flags = parseNtlmTypeThree(data)
	except Exception as e:
		return "NA could not parse ntlmTypeThree: %s" % e

	# get nt hash
	ntHash = getNtHash(username)
	if not ntHash:
		return "NA no ntHash found for user %s" % username
	else:
		ntHash = ntHash.decode("hex") + '\x00' * 5

	# nthash is valid, check if user account is disabled/locked
	global users
	userflags = users.get(username.lower())[2]
	if userflags:
		if "D" in userflags:
			return "NA Account is disabled"
		elif "L" in userflags:
			return "NA Account has been auto-locked"

	# debug
	if options.debug:
		debug("  server challenge " + challenge.encode("hex"))
		debug("  user: " + username)
		debug("  domain: " + domain)
		debug("  host: " + host)
		debug("  flags:")
		for i in dumpNegotiateFlags(flags):
			debug("    %s" % i)
		debug("  ntHash: " + ntHash.encode("hex"))
		debug("  lm response: " + lmResp.encode("hex"))
		debug("  nt response: " + ntResp.encode("hex"))

	# test domain if specified
	if options.domain:
		if not options.domain == domain:
			return "BH wrong domain"

	myResp = ""
	mode = "NTLM"
	# indicates that the NTLM2/NTLMv2 signing and sealing scheme should be used
	if flags & NTLM_NegotiateExtendedSecurity:
		# NTLM2
		if len(ntResp) == 24:
			# if last 16 byte are NULL, then we have NTLM2
			# otherwise still NTLM
			if lmResp[8:24] == "\x00" * 16:
				myResp = verifyNtlm2(ntHash, challenge, lmResp[0:8])
				mode = "NTLM2"
			else:
				myResp = verifyNtlm(ntHash, challenge)
				mode = "NTLM"
		# NTLMv2
		else:
			myResp = verifyNtlmV2(ntResp, challenge, username, domain, ntHash)
			mode = "NTLMv2"
	else:
		# NTLM
		myResp = verifyNtlm(ntHash, challenge)

	if options.debug:
		debug("  mode: " + mode)
		debug("  my response: " + myResp.encode("hex"))

	# return username if respones are equal
	if ntResp == myResp:
		if options.gssSpnego:
			return "AF * %s" % username
		else:
			return "AF %s" % username

	# not authenticated
	return "NA end of verifyNtlmTypeThree() and still not authenticated"


def parseNtlmTypeTwo(msg):

	msg = base64.decodestring(msg)

	signature = msg[0:8]
	msgtype = struct.unpack("<I", msg[8:12])[0]
	flags = struct.unpack("<I", msg[20:24])[0]
	challenge = msg[24:32]

	print challenge
	print flags
	print dumpNegotiateFlags(flags)
	print msgtype
	print signature

	return


def createNtlmTypeTwo():

	challenge = "".join(random.sample(string.printable + "0123456789", 8))

	domain = cr.get("windows/domain", "").upper()
	domain = domain.encode('utf-16le')
	target = cr.get("windows/domain", "").upper()
	target = target.encode('utf-16le')
	server = cr.get("hostname", "").upper()
	server = server.encode('utf-16le')
	dns = cr.get("domainname", "")
	dns = dns.encode('utf-16le')
	fqdn = cr.get("hostname", "") + "." + cr.get("domainname", "")
	fqdn = fqdn.encode('utf-16le')

	ms = 'NTLMSSP\x00'
	# 12 -> l
	ms += struct.pack("<l", NTLMSSP_CHALLENGE)
	# 20 Target Name Security Buffer:
	ms += struct.pack("<H", len(target))
	ms += struct.pack("<H", len(target))
	ms += struct.pack("<I", 48)
	# 20 flags
	ms += struct.pack('<I', NTLM_TYPE2_FLAGS)
	# 24 challenge
	ms += challenge
	# 32 context?
	ms += struct.pack("<l", 0)
	ms += struct.pack("<l", 0)
	# 40 Target Information Security Buffer
	mylen = len(domain) + len(server) + len(dns) + len(fqdn) + 20
	ms += struct.pack("<H", mylen)
	ms += struct.pack("<H", mylen)
	ms += struct.pack("<I", 48 + len(target))
	# 48 target-type-domain
	ms += target
	# Target Information Data
	ms += struct.pack("<H", 2)
	ms += struct.pack("<H", len(domain))
	ms += domain
	ms += struct.pack("<H", 1)
	ms += struct.pack("<H", len(server))
	ms += server
	ms += struct.pack("<H", 4)
	ms += struct.pack("<H", len(dns))
	ms += dns
	ms += struct.pack("<H", 3)
	ms += struct.pack("<H", len(fqdn))
	ms += fqdn

	ms += '\0' * 4

	tt = "TT " + base64.encodestring(ms).replace('\n', '')

	if options.debug:
		debug("NTLM Type 2 Message:")
		debug("  challenge: " + challenge.encode("hex"))
		debug("  flags:")
		for i in dumpNegotiateFlags(NTLM_TYPE2_FLAGS):
			debug("    %s" % i)

	return tt, challenge


def ntlmType(data):

	if options.debug:
		debug("Checking NTLM Type: ")

	if data.startswith("YR "):
		data = data.replace("YR ", "", 1)
	elif data.startswith("KK "):
		data = data.replace("KK ", "", 1)

	signature = ""
	type = 0
	flags = ""

	try:
		data = base64.b64decode(data)
		signature = data[0:8]
		type = struct.unpack("<I", data[8:12])[0]
		flags = struct.unpack("<I", data[12:16])[0]
		if options.debug:
			debug("  signature: " + signature)
			debug("  type: %s" % type)
			debug("  flags:")
			for i in dumpNegotiateFlags(flags):
				debug("    %s" % i)
	except:
		pass

	if signature.startswith("NTLMSSP") and type:
		return type
	return 0

# tests

# test parse ntml msg 3


kk = "KK TlRMTVNTUAADAAAAGAAYAHQAAAAYABgAjAAAAAoACgBIAAAAGgAaAFIAAAAIAAgAbAAAAAAAAACkAAAABYKIogUBKAoAAAAPUwBRAFUASQBEAEEAZABtAGkAbgBpAHMAdAByAGEAdABvAHIAVABFAFMAVAAzm2w/FR6FjQAAAAAAAAAAAAAAAAAAAABB+vMG7iLgUM+rJXuo/uwPwWPX84XA64c="
a, b, c, d, e, f = parseNtlmTypeThree(kk)
assert a == "Administrator"
assert b == "SQUID"
assert c == "TEST"
assert d.encode("hex") == "339b6c3f151e858d00000000000000000000000000000000"
assert e.encode("hex") == "41faf306ee22e050cfab257ba8feec0fc163d7f385c0eb87"
assert f == 2726855173

# test NTML
resp = "1cffa87d8b48ce73a71e3e6c9a9dd80f112d48dfeea8792c"
assert resp == verifyNtlm("CAA1239D44DA7EDF926BCE39F5C65D0F".decode("hex") + '\x00' * 5, "L)eNCnxD").encode("hex")

# test NTML2
a = verifyNtlm2(
	"CAA1239D44DA7EDF926BCE39F5C65D0F".decode("hex") + '\x00' * 5,
	"83219623",
	"d6e6507e3e5be1e700000000000000000000000000000000".decode("hex")[0:8])
assert a.encode("hex") == "42f0cfd6fcbb4660dbace7fab6e5d82cff1572ad8fd72b5a"

# test NTMLv2
resp = "96484569c3bb18aa3f4f6ba687dfe5c0010100000000000068b3d5e4e2facc014c5e8784508b1cfc00000000020000000000000000000000"
assert resp.decode("hex") == verifyNtlmV2(resp.decode("hex"),
	"11111111", "Administrator", "UNIVENTION", "CAA1239D44DA7EDF926BCE39F5C65D0F".decode("hex"))

resp = "28a9be400eed0d5d362c590616754d320101000000000000a89da1c1e2facc01874554ba437df9fb00000000020000000000000000000000"
assert resp.decode("hex") == verifyNtlmV2(resp.decode("hex"),
	"11111111", "Administrator", "UNIVENTION", "CAA1239D44DA7EDF926BCE39F5C65D0F".decode("hex"))

resp = "16bae73559708bcd091f34a43f21bcf30101000000000000a826ec08dcfacc01e45112e4c3192b9a00000000020000000000000000000000"
assert resp.decode("hex") == verifyNtlmV2(resp.decode("hex"),
	"11111111", "Administrator", "UNIVENTION", "CAA1239D44DA7EDF926BCE39F5C65D0F".decode("hex"))

# konquerer
kk = "TlRMTVNTUAADAAAAGAAYAFgAAAAYABgAQAAAAAgACABwAAAACgAKAHgAAAAWABYAggAAAAAAAAAAAAAABQKJoKjmNy4NeB+jX9LglfZCgLox0goV9JvPzYSVXuDFp+pqwDYcEKPhwLWqOaxk1HGQI0gAQQBOAFMAdABlAHMAdAAxAFcATwBSAEsAUwBUAEEAVABJAE8ATgA="
challenge = "5e39365709660d4c"
a, b, c, d, e, f = parseNtlmTypeThree(kk)
assert "test1" == a
assert "HANS" == b
assert "WORKSTATION" == c
assert "84955ee0c5a7ea6ac0361c10a3e1c0b5aa39ac64d4719023".decode("hex") == d
assert "a8e6372e0d781fa35fd2e095f64280ba31d20a15f49bcfcd".decode("hex") == e
assert "2693333509" == "%s" % f

# ipad
kk = "TlRMTVNTUAADAAAAGAAYAEAAAAAYABgAWAAAAAAAAAAAAAAAGgAaAHAAAAAIAAgAigAAAAAAAAAAAAAABQIIAJEPdnUglFZTAAAAAAAAAAAAAAAAAAAAADMFtY9sMPhP1jShGSWXgOE2e8gvtwdPAGEAZABtAGkAbgBpAHMAdAByAGEAdABvAHIAaQBQAGEAZAA="
a, b, c, d, e, f = parseNtlmTypeThree(kk)
assert "3305b58f6c30f84fd634a119259780e1367bc82fb7074f00".decode("hex") == e

# main
if __name__ == '__main__':

	usage = '%prog [options]'
	parser = optparse.OptionParser(usage=usage)
	parser.add_option("-d", "--domain", dest="domain", help="domain in ntlm authentication")
	parser.add_option("-c", "--cache-lifetime", dest="cacheLifeTime", type="int",
		default=3600, help="password cache lifetime (3600)")
	parser.add_option("-t", "--debug", dest="debug", help="debug modus (Attention, cleartext password hashes!)",
		action="store_true", default=False)
	parser.add_option("-f", "--debug-file", dest="debugFile", help="debug file (/tmp/squid-ntlm-auth.log)",
		action="store_true", default="/tmp/squid-ntlm-auth.log")
	parser.add_option("-g", "--gss-spnego", dest="gssSpnego", help="gss spnego mode for ntlm answer",
		action="store_true", default=False)
	parser.add_option("-s", "--gss-spnego-strip-realm", dest="gssSpnegoStripRealm",
		help="strip realm from login name if gss spnego mode is uses for authentication",
		action="store_true", default=False)
	(options, args) = parser.parse_args()

	cr = univention.config_registry.ConfigRegistry()
	cr.load()
	users = {}
	challenge = ""

	# open pipe to squid_kerb_auth for kerberos stuff
	kerbPipe = None
	if options.gssSpnego:
		kerbPipe = subprocess.Popen(['/usr/lib/squid/negotiate_kerberos_auth'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)

	while True:

		# python -u is required for unbufferd streams
		data = sys.stdin.readline()
		data = data.strip()
		answer = "BH internal error"

		if options.debug:
			debug("from squid -> " + data)

		if data:
			if data.startswith("YR "):
				ntype = ntlmType(data)
				if ntype == 1:
					try:
						answer, challenge = createNtlmTypeTwo()
					except Exception as e:
						answer = "BH failed to createNtlmTypeTwo(): %s" % e
				# office 2013 workaround
				elif ntype == 3:
					try:
						data = data.replace("YR ", "", 1)
						answer = verifyNtlmTypeThree(data, challenge)
					except Exception as e:
						answer = "BH failed to verifyNtlmTypeThree(): %s" % e
				# kerberos
				else:
					if options.debug:
						debug("negotiate kerberos authentication" + data)
					try:
						if kerbPipe:
							if options.debug:
								debug("asking kerb tool")
							kerbPipe.stdin.write(data + "\n")
							kerbPipe.stdin.flush()
							answer = kerbPipe.stdout.readline()
							if options.debug:
								debug("real answer %s" % answer)
							# remove realm from login
							if options.gssSpnegoStripRealm:
								tmp = answer.split()
								if len(tmp) > 2 and tmp[0] == "AF":
									login = tmp[2].split("@", 1)[0]
									answer = "%s %s %s" % (tmp[0], tmp[1], login)
								if options.debug:
									debug("fixed answer %s" % answer)
						# this whole stuff could also be done by
						# python kerberos
						#  result, context = kerberos.authGSSServerInit('HTTP')
						#  r = kerberos.authGSSServerStep(context, data.replace("YR ", "", 1))
						#  gssstring = kerberos.authGSSServerResponse(context)
						#  login = kerberos.authGSSServerUserName(context)
						#  login = login.split("@", 1)[0]
						#  kerberos.authGSSServerClean(context)
						#  answer = "AF %s %s" % (gssstring, login)
					except Exception as e:
						answer = "BH failed doing kerberos: %s" % e

			if data.startswith("KK "):
				try:
					answer = verifyNtlmTypeThree(data, challenge)
				except Exception as e:
					answer = "BH failed to verifyNtlmTypeThree(): %s" % e
		else:
			answer = "ERR"

		if options.debug:
			debug("to squid <- " + answer)
		sys.stdout.write(answer + "\n")
		sys.stdout.flush()

sys.exit(0)
