onmcp/scripts/test_cpp_security.sh

104 lines
3.9 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
"$ROOT/scripts/build_cpp.sh" >/dev/null
mkdir -p "$ROOT/reports"
MOCK_PORT=19481
MCPD_PORT=19480
"$ROOT/build/cpp/mock_mcp_server" --port "$MOCK_PORT" >"$ROOT/reports/security_mock.log" 2>&1 &
MOCK_PID=$!
"$ROOT/build/cpp/mcpd" --port "$MCPD_PORT" --upstream "http://127.0.0.1:$MOCK_PORT/rpc" >"$ROOT/reports/security_mcpd.log" 2>&1 &
MCPD_PID=$!
cleanup() {
kill "$MCPD_PID" "$MOCK_PID" >/dev/null 2>&1 || true
wait "$MCPD_PID" "$MOCK_PID" 2>/dev/null || true
rm -f "$ROOT/reports/security_large_payload.json"
}
trap cleanup EXIT
# Wait until mcpd has bound its HTTP port before invoking the client.
for _ in $(seq 1 50); do
if curl -fsS "http://127.0.0.1:$MCPD_PORT/healthz" >/dev/null 2>&1; then
break
fi
sleep 0.1
done
CLIENT="$ROOT/build/cpp/oh_mcp_client"
ENDPOINT="http://127.0.0.1:$MCPD_PORT/rpc"
pass=0
fail=0
record() {
local name="$1" expected="$2" actual="$3"
if [[ "$actual" == "$expected" ]]; then
printf '[PASS] %-28s expected=%s actual=%s\n' "$name" "$expected" "$actual"
pass=$((pass+1))
else
printf '[FAIL] %-28s expected=%s actual=%s\n' "$name" "$expected" "$actual"
fail=$((fail+1))
fi
}
status_of_client() {
set +e
"$@" >/tmp/ohmcp_security_body.json 2>/tmp/ohmcp_security_err.log
local code=$?
set -e
if [[ "$code" == "0" ]]; then
echo 200
return
fi
if grep -q 'unauthorized agent' /tmp/ohmcp_security_body.json; then echo 401; return; fi
if grep -q 'tool not allowed' /tmp/ohmcp_security_body.json; then echo 403; return; fi
if grep -q 'replay detected' /tmp/ohmcp_security_body.json; then echo 409; return; fi
echo "exit-$code"
}
# 1. no token: pass empty token, mcpd should reject.
NO_TOKEN_STATUS=$(status_of_client "$CLIENT" --endpoint "$ENDPOINT" --token "" --raw call-tool amap.maps_weather city=苏州)
record "no token rejected" 401 "$NO_TOKEN_STATUS"
# 2. bad token.
BAD_TOKEN_STATUS=$(status_of_client "$CLIENT" --endpoint "$ENDPOINT" --token bad-token --raw call-tool amap.maps_weather city=苏州)
record "bad token rejected" 401 "$BAD_TOKEN_STATUS"
# 3. ACL: weather-token can only call weather, not hotel.
ACL_STATUS=$(status_of_client "$CLIENT" --endpoint "$ENDPOINT" --token weather-token --raw call-tool amap.hotel_search city=苏州)
record "acl denied" 403 "$ACL_STATUS"
# 4. nonce replay: first succeeds, second with same nonce is rejected.
FIRST_NONCE_STATUS=$(status_of_client "$CLIENT" --endpoint "$ENDPOINT" --token demo-token --nonce replay-001 --raw call-tool amap.maps_weather city=苏州)
SECOND_NONCE_STATUS=$(status_of_client "$CLIENT" --endpoint "$ENDPOINT" --token demo-token --nonce replay-001 --raw call-tool amap.maps_weather city=苏州)
record "nonce first accepted" 200 "$FIRST_NONCE_STATUS"
record "nonce replay rejected" 409 "$SECOND_NONCE_STATUS"
# 5. payload limit: send >256KB JSON body directly with curl.
python3 - <<PY
import json
payload={"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"amap.maps_weather","arguments":{"city":"苏州","blob":"x"*300000}}}
open('$ROOT/reports/security_large_payload.json','w').write(json.dumps(payload))
PY
PAYLOAD_STATUS=$(curl -s -o /tmp/ohmcp_large_body.json -w '%{http_code}' -H 'Content-Type: application/json' -H 'X-Agent-Token: demo-token' --data-binary "@$ROOT/reports/security_large_payload.json" "$ENDPOINT")
record "large payload rejected" 413 "$PAYLOAD_STATUS"
STATS=$(curl -s "http://127.0.0.1:$MCPD_PORT/stats")
printf '%s' "$STATS" > "$ROOT/reports/security_stats.json"
cat > "$ROOT/reports/security_summary.json" <<JSON
{
"pass": $pass,
"fail": $fail,
"tests": {
"no_token": "$NO_TOKEN_STATUS",
"bad_token": "$BAD_TOKEN_STATUS",
"acl_denied": "$ACL_STATUS",
"nonce_first": "$FIRST_NONCE_STATUS",
"nonce_replay": "$SECOND_NONCE_STATUS",
"large_payload": "$PAYLOAD_STATUS"
}
}
JSON
printf 'summary: pass=%s fail=%s report=%s\n' "$pass" "$fail" "$ROOT/reports/security_summary.json"
if [[ "$fail" != "0" ]]; then
exit 1
fi