package kerberos

// the following code is adapted from the original library
// https://github.com/jcmturner/gokrb5/blob/855dbc707a37a21467aef6c0245fcf3328dc39ed/v8/client/network.go
// it is copied here because the library does not export "SendToKDC()"

import (
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"io"
	"net"
	"strings"
	"time"

	"github.com/jcmturner/gokrb5/v8/messages"
	"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
)

// sendtokdc.go deals with actual sending and receiving responses from KDC
// SendToKDC sends a message to the KDC and returns the response.
// It first tries to send the message over TCP, and if that fails, it falls back to UDP.(and vice versa)
// @example
// ```javascript
// const kerberos = require('nuclei/kerberos');
// const client = new kerberos.Client('acme.com');
// const response = kerberos.SendToKDC(client, 'message');
// ```
func SendToKDC(kclient *Client, msg string) (string, error) {
	if kclient == nil || kclient.nj == nil || kclient.Krb5Config == nil || kclient.Realm == "" {
		return "", fmt.Errorf("kerberos client is not initialized")
	}
	if kclient.config.timeout == 0 {
		kclient.config.timeout = 5 // default timeout 5 seconds
	}
	var response []byte
	var err error

	response, err = sendToKDCTcp(kclient, msg)
	if err == nil {
		// if it related to tcp
		bin, err := CheckKrbError(response)
		if err == nil {
			return string(bin), nil
		}
		// if it is krb error no need to do udp
		if e, ok := err.(messages.KRBError); ok {
			return string(response), e
		}
	}

	// fallback to udp
	response, err = sendToKDCUdp(kclient, msg)
	if err == nil {
		// if it related to udp
		bin, err := CheckKrbError(response)
		if err == nil {
			return string(bin), nil
		}
	}
	return string(response), err
}

// sendToKDCTcp sends a message to the KDC via TCP.
func sendToKDCTcp(kclient *Client, msg string) ([]byte, error) {
	_, kdcs, err := kclient.Krb5Config.GetKDCs(kclient.Realm, true)
	kclient.nj.HandleError(err, "error getting KDCs")
	kclient.nj.Require(len(kdcs) > 0, "no KDCs found")

	executionId := kclient.nj.ExecutionId()
	dialers := protocolstate.GetDialersWithId(executionId)
	if dialers == nil {
		return nil, fmt.Errorf("dialers not initialized for %s", executionId)
	}

	dialCtx := kclient.nj.Context()
	var errs []string
	for i := 1; i <= len(kdcs); i++ {
		host, port, err := net.SplitHostPort(kdcs[i])
		if err == nil && kclient.config.ip != "" {
			// use that ip address instead of realm/domain for resolving
			host = kclient.config.ip
		}
		tcpConn, err := dialers.Fastdialer.Dial(dialCtx, "tcp", net.JoinHostPort(host, port))
		if err != nil {
			errs = append(errs, fmt.Sprintf("error establishing connection to %s: %v", kdcs[i], err))
			continue
		}
		defer func() {
			_ = tcpConn.Close()
		}()
		_ = tcpConn.SetDeadline(time.Now().Add(time.Duration(kclient.config.timeout) * time.Second)) //read and write deadline
		rb, err := sendTCP(tcpConn.(*net.TCPConn), []byte(msg))
		if err != nil {
			errs = append(errs, fmt.Sprintf("error sending to %s: %v", kdcs[i], err))
			continue
		}
		return rb, nil
	}
	if len(errs) > 0 {
		return nil, fmt.Errorf("error sending to a KDC: %s", strings.Join(errs, "; "))
	}
	return nil, nil
}

// sendToKDCUdp sends a message to the KDC via UDP.
func sendToKDCUdp(kclient *Client, msg string) ([]byte, error) {
	_, kdcs, err := kclient.Krb5Config.GetKDCs(kclient.Realm, true)
	kclient.nj.HandleError(err, "error getting KDCs")
	kclient.nj.Require(len(kdcs) > 0, "no KDCs found")

	executionId := kclient.nj.ExecutionId()
	dialers := protocolstate.GetDialersWithId(executionId)
	if dialers == nil {
		return nil, fmt.Errorf("dialers not initialized for %s", executionId)
	}
	dialCtx := kclient.nj.Context()
	var errs []string
	for i := 1; i <= len(kdcs); i++ {
		host, port, err := net.SplitHostPort(kdcs[i])
		if err == nil && kclient.config.ip != "" {
			// use that ip address instead of realm/domain for resolving
			host = kclient.config.ip
		}
		udpConn, err := dialers.Fastdialer.Dial(dialCtx, "udp", net.JoinHostPort(host, port))
		if err != nil {
			errs = append(errs, fmt.Sprintf("error establishing connection to %s: %v", kdcs[i], err))
			continue
		}
		defer func() {
			_ = udpConn.Close()
		}()
		_ = udpConn.SetDeadline(time.Now().Add(time.Duration(kclient.config.timeout) * time.Second)) //read and write deadline
		rb, err := sendUDP(udpConn.(*net.UDPConn), []byte(msg))
		if err != nil {
			errs = append(errs, fmt.Sprintf("error sending to %s: %v", kdcs[i], err))
			continue
		}
		return rb, nil
	}
	if len(errs) > 0 {
		// fallback to tcp
		return nil, fmt.Errorf("error sending to a KDC: %s", strings.Join(errs, "; "))
	}
	return nil, nil
}

// sendUDP sends bytes to connection over UDP.
func sendUDP(conn *net.UDPConn, b []byte) ([]byte, error) {
	var r []byte
	defer func() {
		_ = conn.Close()
	}()
	_, err := conn.Write(b)
	if err != nil {
		return r, fmt.Errorf("error sending to (%s): %v", conn.RemoteAddr().String(), err)
	}
	udpbuf := make([]byte, 4096)
	n, _, err := conn.ReadFrom(udpbuf)
	r = udpbuf[:n]
	if err != nil {
		return r, fmt.Errorf("sending over UDP failed to %s: %v", conn.RemoteAddr().String(), err)
	}
	if len(r) < 1 {
		return r, fmt.Errorf("no response data from %s", conn.RemoteAddr().String())
	}
	return r, nil
}

// sendTCP sends bytes to connection over TCP.
func sendTCP(conn *net.TCPConn, b []byte) ([]byte, error) {
	defer func() {
		_ = conn.Close()
	}()
	var r []byte
	// RFC 4120 7.2.2 specifies the first 4 bytes indicate the length of the message in big endian order.
	hb := make([]byte, 4)
	binary.BigEndian.PutUint32(hb, uint32(len(b)))
	b = append(hb, b...)

	_, err := conn.Write(b)
	if err != nil {
		return r, fmt.Errorf("error sending to KDC (%s): %v", conn.RemoteAddr().String(), err)
	}

	sh := make([]byte, 4)
	_, err = conn.Read(sh)
	if err != nil {
		return r, fmt.Errorf("error reading response size header: %v", err)
	}
	s := binary.BigEndian.Uint32(sh)

	rb := make([]byte, s)
	_, err = io.ReadFull(conn, rb)
	if err != nil {
		return r, fmt.Errorf("error reading response: %v", err)
	}
	if len(rb) < 1 {
		return r, fmt.Errorf("no response data from KDC %s", conn.RemoteAddr().String())
	}
	return rb, nil
}

// CheckKrbError checks if the response bytes from the KDC are a KRBError.
func CheckKrbError(b []byte) ([]byte, error) {
	var KRBErr messages.KRBError
	if err := KRBErr.Unmarshal(b); err == nil {
		return b, KRBErr
	}
	return b, nil
}

// TGStoHashcat converts a TGS to a hashcat format.
func TGStoHashcat(tgs messages.Ticket, username string) (string, error) {
	return fmt.Sprintf("$krb5tgs$%d$*%s$%s$%s*$%s$%s",
		tgs.EncPart.EType,
		username,
		tgs.Realm,
		strings.Join(tgs.SName.NameString[:], "/"),
		hex.EncodeToString(tgs.EncPart.Cipher[:16]),
		hex.EncodeToString(tgs.EncPart.Cipher[16:]),
	), nil
}

// ASRepToHashcat converts an AS-REP message to a hashcat format
func ASRepToHashcat(asrep messages.ASRep) (string, error) {
	return fmt.Sprintf("$krb5asrep$%d$%s@%s:%s$%s",
		asrep.EncPart.EType,
		asrep.CName.PrincipalNameString(),
		asrep.CRealm,
		hex.EncodeToString(asrep.EncPart.Cipher[:16]),
		hex.EncodeToString(asrep.EncPart.Cipher[16:])), nil
}
