#!/usr/bin/python3 -u
#
# Univention openldap ntlm squid authenticator
#
# SPDX-FileCopyrightText: 2012-2025 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only
#
# 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

from __future__ import annotations

import argparse
import base64
import binascii
import hashlib
import hmac
import os
import random
import string
import struct
import subprocess
import sys
import time

import passlib.crypto.des
from ldap.filter import filter_format

import univention.config_registry
import univention.uldap


options = argparse.Namespace(debug=False, debug_file='')

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: int) -> list[str]:
    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: str) -> None:
    if options.debug:
        with open(options.debug_file, "a") as fh:
            os.chmod(options.debug_file, 0o600)
            fh.write("%s - %s\n" % (time.time(), msg))


def parseNtlmTypeThree(data: bytes) -> tuple[str, str, str, bytes, bytes, int]:
    data = data.replace(b"KK ", b"", 1)
    data = base64.b64decode(data)
    # signature = data[0:8]
    # lm_type = struct.unpack("<I", data[8:12])[0]
    lm_len = struct.unpack("<h", data[12:14])[0]
    lm_offset = struct.unpack("<l", data[16:20])[0]
    nt_len = struct.unpack("<h", data[20:22])[0]
    nt_offset = struct.unpack("<l", data[24:28])[0]

    domain_offset = struct.unpack("<l", data[32:36])[0]
    username_offset = struct.unpack("<l", data[40:44])[0]
    host_length = struct.unpack("<h", data[44:46])[0]
    host_offset = struct.unpack("<l", data[48:52])[0]
    flags = struct.unpack("<I", data[60:64])[0]

    username = data[username_offset:host_offset]
    domain = data[domain_offset:username_offset]
    host = data[host_offset:host_offset + host_length]
    lm_resp = data[lm_offset:(lm_offset + lm_len)]
    nt_resp = data[nt_offset:(nt_offset + nt_len)]

    if domain_offset == 0:
        domain = b""
    if host_offset == 0:
        host = b""
    if username_offset == 0:
        username = b""

    encoding = 'utf-16' if flags & NTLM_NegotiateUnicode else 'ASCII'
    return username.decode(encoding), domain.decode(encoding), host.decode(encoding), lm_resp, nt_resp, flags


def DesEncrypt(data: bytes, key: bytes) -> bytes:
    return passlib.crypto.des.des_encrypt_block(key, data)


def verifyNtlm(password_hash: bytes, challenge: bytes) -> bytes:
    """
    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
    """
    z_password_hash = password_hash.ljust(21, b"\0")
    response = DesEncrypt(challenge, z_password_hash[0:7])
    response += DesEncrypt(challenge, z_password_hash[7:14])
    response += DesEncrypt(challenge, z_password_hash[14:21])
    return response


def verifyNtlm2(ResponseKeyNT: bytes, ServerChallenge: bytes, ClientChallenge: bytes) -> bytes:
    """http://davenport.sourceforge.net/ntlm.html#theNtlm2SessionResponse"""
    nonce = ServerChallenge + ClientChallenge
    sess = hashlib.md5(nonce).digest()[0:8]
    nt_challenge_response = verifyNtlm(ResponseKeyNT, sess)

    return nt_challenge_response


def verifyNtlmV2(ntResp: bytes, challenge: bytes, user: str, domain: str, ntHashV1: bytes) -> bytes:
    """http://davenport.sourceforge.net/ntlm.html#theType3Message"""
    # nt v2 hash
    inf = (user.upper() + domain).encode("utf-16le")
    nt_hash_v2 = hmac.new(ntHashV1, inf, digestmod=hashlib.md5).digest()

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

    # const
    version = b'\x01\x01' + b'\x00' * 2
    reserved = b'\x00' * 4
    unknown = b'\x00' * 4

    # create blob
    blob = version + reserved + timestamp
    blob += client_challenge + unknown + target_information
    challenge_blob = challenge + blob

    # secret
    secret = hmac.new(nt_hash_v2, challenge_blob, digestmod=hashlib.md5).digest()

    # response
    my_resp = secret + blob

    return my_resp


def getNtHash(user: str) -> bytes:
    user = user.lower()

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

    # search for ntHash and sambaAcctFlags of user
    nt_hash = b""
    ldap_filter = filter_format("(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=ldap_filter, attr=attr)
    debug("  ldapsearch for sambaNTPassword of user %s" % user)

    if len(result) == 1:
        tmp = result[0][1].get("sambaNTPassword", [b""])[0]
        samba_acct_flags = result[0][1].get("sambaAcctFlags", [b""])[0]
        if tmp and samba_acct_flags:
            debug("  found sambaNTPassword in ldap for user %r with sambaAcctFlags %r" % (user, samba_acct_flags))
            nt_hash = tmp
            users[user] = (nt_hash, time.time(), samba_acct_flags)
    else:
        users[user] = (b"", time.time(), b"")
    return nt_hash


def verifyNtlmTypeThree(data: bytes, challenge: bytes) -> str:
    debug("NTLM Type 3 Message: ")

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

    # get nt hash
    nt_hash = getNtHash(username)
    if not nt_hash:
        return "NA no ntHash found for user %s" % (username,)
    else:
        nt_hash = binascii.unhexlify(nt_hash) + b'\x00\x00\x00\x00\x00'
    # nt_hash is valid, check if user account is disabled/locked
    user_flags = users.get(username.lower(), (b"", 0, b""))[2]
    if user_flags:
        if b"D" in user_flags:
            return "NA Account is disabled"
        elif b"L" in user_flags:
            return "NA Account has been auto-locked"

    # debug
    if options.debug:
        debug("  server challenge %r" % (binascii.hexlify(challenge),))
        debug("  user: %r" % (username,))
        debug("  domain: %r" % (domain,))
        debug("  host: %r" % (host,))
        debug("  flags:")
        for i in dumpNegotiateFlags(flags):
            debug("    %s" % (i,))
        debug("  ntHash: %r" % (binascii.hexlify(nt_hash),))
        debug("  lm response: %r" % (binascii.hexlify(nt_hash),))
        debug("  nt response: %r" % (binascii.hexlify(nt_resp),))

    # test domain if specified
    if options.domain and options.domain != domain:
        return "BH wrong domain"
    my_resp = b""
    mode = "NTLM"
    # indicates that the NTLM2/NTLMv2 signing and sealing scheme should be used
    if flags & NTLM_NegotiateExtendedSecurity:
        # NTLM2
        if len(nt_resp) == 24:
            # if last 16 byte are NULL, then we have NTLM2
            # otherwise still NTLM
            if lm_resp[8:24] == b"\x00" * 16:
                my_resp = verifyNtlm2(nt_hash, challenge, lm_resp[0:8])
                mode = "NTLM2"
            else:
                my_resp = verifyNtlm(nt_hash, challenge)
                mode = "NTLM"
        # NTLMv2
        else:
            my_resp = verifyNtlmV2(nt_resp, challenge, username, domain, nt_hash)
            mode = "NTLMv2"
    else:
        # NTLM
        my_resp = verifyNtlm(nt_hash, challenge)

    debug("  mode: %r" % (mode,))
    debug("  my response: %r" % (binascii.hexlify(my_resp),))

    # return username if responses are equal
    if nt_resp == my_resp:
        if options.gss_spnego:
            return "AF * %s" % username
        else:
            return "AF %s" % username

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


def parseNtlmTypeTwo(msg: bytes) -> None:
    msg = base64.b64decode(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)


def createNtlmTypeTwo() -> tuple[str, bytes]:
    challenge = "".join(random.sample(string.printable + string.digits, 8)).encode('ASCII')

    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 = b'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
    my_len = len(domain) + len(server) + len(dns) + len(fqdn) + 20
    ms += struct.pack("<H", my_len)
    ms += struct.pack("<H", my_len)
    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 += b'\0' * 4

    tt = "TT " + base64.b64encode(ms).decode('ASCII')
    if options.debug:
        debug("NTLM Type 2 Message:")
        debug("  challenge: %r" % (challenge,))
        debug("  flags:")
        for i in dumpNegotiateFlags(NTLM_TYPE2_FLAGS):
            debug("    %s" % i)

    return tt, challenge


def ntlmType(data: bytes) -> int:
    debug("Checking NTLM Type: ")

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

    signature = b""
    ntlm_type = 0
    flags = 0

    try:
        data = base64.b64decode(data)
        signature = data[0:8]
        ntlm_type = struct.unpack("<I", data[8:12])[0]
        flags = struct.unpack("<I", data[12:16])[0]
        if options.debug:
            debug("  signature: %r" % (signature,))
            debug("  type: %s" % (ntlm_type,))
            debug("  flags:")
            for i in dumpNegotiateFlags(flags):
                debug("    %s" % (i,))
    except Exception as exc:
        debug('ntlmType: silently caught exception: %r' % (exc,))

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

# tests

# test parse ntml msg 3


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

# test NTML
assert binascii.hexlify(verifyNtlm(binascii.unhexlify(b"CAA1239D44DA7EDF926BCE39F5C65D0F") + b'\x00' * 5, b"L)eNCnxD")) == b"1cffa87d8b48ce73a71e3e6c9a9dd80f112d48dfeea8792c"

# test NTML2
g = verifyNtlm2(
    binascii.unhexlify(b"CAA1239D44DA7EDF926BCE39F5C65D0F") + b'\x00' * 5,
    b"83219623",
    binascii.unhexlify(b"d6e6507e3e5be1e700000000000000000000000000000000")[0:8])
assert binascii.hexlify(g) == b"42f0cfd6fcbb4660dbace7fab6e5d82cff1572ad8fd72b5a"

# test NTMLv2
resp = binascii.unhexlify(b"96484569c3bb18aa3f4f6ba687dfe5c0010100000000000068b3d5e4e2facc014c5e8784508b1cfc00000000020000000000000000000000")
assert resp == verifyNtlmV2(resp, b"11111111", "Administrator", "UNIVENTION", binascii.unhexlify(b"CAA1239D44DA7EDF926BCE39F5C65D0F"))

resp = binascii.unhexlify(b"28a9be400eed0d5d362c590616754d320101000000000000a89da1c1e2facc01874554ba437df9fb00000000020000000000000000000000")
assert resp == verifyNtlmV2(resp, b"11111111", "Administrator", "UNIVENTION", binascii.unhexlify(b"CAA1239D44DA7EDF926BCE39F5C65D0F"))

resp = binascii.unhexlify(b"16bae73559708bcd091f34a43f21bcf30101000000000000a826ec08dcfacc01e45112e4c3192b9a00000000020000000000000000000000")
assert resp == verifyNtlmV2(resp, b"11111111", "Administrator", "UNIVENTION", binascii.unhexlify(b"CAA1239D44DA7EDF926BCE39F5C65D0F"))

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

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

# main
if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("-d", "--domain", help="domain in ntlm authentication")
    parser.add_argument("-c", "--cache-lifetime", type=int, default=3600, help="password cache lifetime (3600)")
    parser.add_argument("-t", "--debug", help="debug modus (Attention, cleartext password hashes!)", action="store_true")
    parser.add_argument("-f", "--debug-file", help="debug file (%(default)s)", default="/tmp/squid-ntlm-auth.log")
    parser.add_argument("-g", "--gss-spnego", help="gss spnego mode for ntlm answer", action="store_true")
    parser.add_argument("-s", "--gss-spnego-strip-realm", help="strip realm from login name if gss spnego mode is uses for authentication", action="store_true")
    options = parser.parse_args()

    cr = univention.config_registry.ConfigRegistry()
    cr.load()
    users: dict[str, tuple[bytes, float, bytes]] = {}
    challenge = b""

    # open pipe to squid_kerb_auth for kerberos stuff
    kerbPipe = None
    if options.gss_spnego:
        cmd = [
            '/usr/lib/squid/negotiate_kerberos_auth', '-k',
            '/var/lib/samba/private/http-proxy-%(hostname)s.keytab' % cr,
        ]
        if options.gss_spnego_strip_realm:
            cmd.append('-r')
        kerbPipe = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)

    while True:
        # python3 -u is required for unbuffered streams
        data = sys.stdin.buffer.readline()
        data = data.strip()
        answer = "BH internal error"

        if options.debug:
            debug("from squid -> %r" % (data,))

        if data:
            if data.startswith(b"YR "):
                ntype = ntlmType(data)
                if ntype == 1:
                    try:
                        answer, challenge = createNtlmTypeTwo()
                    except Exception as exc:
                        answer = "BH failed to createNtlmTypeTwo(): %s" % (exc,)
                # office 2013 workaround
                elif ntype == 3:
                    try:
                        data = data.replace(b"YR ", b"", 1)
                        answer = verifyNtlmTypeThree(data, challenge)
                    except Exception as exc:
                        answer = "BH failed to verifyNtlmTypeThree(): %s" % (exc,)
                # kerberos
                else:
                    debug("negotiate kerberos authentication: %r" % (data,))
                    try:
                        if kerbPipe:
                            debug("asking kerb tool")
                            kerbPipe.stdin.write(data + b"\n")
                            kerbPipe.stdin.flush()
                            answer = kerbPipe.stdout.readline().decode("UTF-8")
                            debug("answer %r" % (answer,))
                        # this whole stuff could also be done by
                        # Python Kerberos
                        #  result, context = kerberos.authGSSServerInit('HTTP')
                        #  r = kerberos.authGSSServerStep(context, data.replace(b"YR ", b"", 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 exc:
                        answer = "BH failed doing kerberos: %s" % (exc,)

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

        debug("to squid <- %r" % (answer,))
        sys.stdout.write(answer + "\n")
        sys.stdout.flush()

sys.exit(0)
