linux-negative-test.sh 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. HOST="169.254.100.2"
  4. PORT="48888"
  5. PASSWORD="2026"
  6. INTERFACE=""
  7. usage() {
  8. cat <<'EOF'
  9. Usage:
  10. linux-negative-test.sh [options]
  11. Purpose:
  12. Safe negative checks for the Linux server.
  13. This script does not call /api/network/apply and does not modify netplan.
  14. Checks:
  15. - Wrong password should be rejected
  16. - Missing interface should fail validate
  17. - Invalid IP should fail validate
  18. - Gateway outside subnet should fail validate
  19. - Invalid DNS should fail validate
  20. Options:
  21. --host <ip> Server host, default: 169.254.100.2
  22. --port <port> Server port, default: 48888
  23. --password <value> Correct admin password, default: 2026
  24. --interface <name> Existing target interface, default: suggested_target_interface
  25. --help Show this help
  26. EOF
  27. }
  28. while [[ $# -gt 0 ]]; do
  29. case "$1" in
  30. --host)
  31. HOST="$2"
  32. shift 2
  33. ;;
  34. --port)
  35. PORT="$2"
  36. shift 2
  37. ;;
  38. --password)
  39. PASSWORD="$2"
  40. shift 2
  41. ;;
  42. --interface)
  43. INTERFACE="$2"
  44. shift 2
  45. ;;
  46. --help|-h)
  47. usage
  48. exit 0
  49. ;;
  50. *)
  51. echo "Unknown argument: $1" >&2
  52. usage >&2
  53. exit 2
  54. ;;
  55. esac
  56. done
  57. for cmd in curl python3; do
  58. if ! command -v "$cmd" >/dev/null 2>&1; then
  59. echo "Missing required command: $cmd" >&2
  60. exit 2
  61. fi
  62. done
  63. BASE_URL="http://${HOST}:${PORT}"
  64. TMP_DIR="$(mktemp -d)"
  65. trap 'rm -rf "$TMP_DIR"' EXIT
  66. log() {
  67. printf '[nettool-negative] %s\n' "$*"
  68. }
  69. request() {
  70. local method="$1"
  71. local path="$2"
  72. local password="$3"
  73. local payload="${4:-}"
  74. local body_file="$TMP_DIR/body.json"
  75. local http_code
  76. if [[ -n "$payload" ]]; then
  77. http_code="$(curl -sS -o "$body_file" -w "%{http_code}" -X "$method" \
  78. -H "X-Admin-Password: ${password}" \
  79. -H "Content-Type: application/json" \
  80. --data "$payload" \
  81. --connect-timeout 3 \
  82. --max-time 15 \
  83. "${BASE_URL}${path}")"
  84. else
  85. http_code="$(curl -sS -o "$body_file" -w "%{http_code}" -X "$method" \
  86. -H "X-Admin-Password: ${password}" \
  87. --connect-timeout 3 \
  88. --max-time 15 \
  89. "${BASE_URL}${path}")"
  90. fi
  91. printf '%s\n' "$http_code"
  92. cat "$body_file"
  93. }
  94. read_http_code() {
  95. sed -n '1p'
  96. }
  97. read_body() {
  98. sed '1d'
  99. }
  100. json_get() {
  101. local body="$1"
  102. local expr="$2"
  103. BODY_JSON="$body" python3 - "$expr" <<'PY'
  104. import json
  105. import os
  106. import sys
  107. expr = sys.argv[1]
  108. data = json.loads(os.environ["BODY_JSON"])
  109. value = data
  110. for part in expr.split('.'):
  111. if part:
  112. value = value[part]
  113. if isinstance(value, list):
  114. import json as _json
  115. print(_json.dumps(value, ensure_ascii=False))
  116. else:
  117. print(value)
  118. PY
  119. }
  120. expect_failure() {
  121. local name="$1"
  122. local expected_http="$2"
  123. local expected_code="$3"
  124. local result="$4"
  125. local http_code body actual_code
  126. http_code="$(printf '%s\n' "$result" | read_http_code)"
  127. body="$(printf '%s\n' "$result" | read_body)"
  128. actual_code="$(json_get "$body" "code")"
  129. if [[ "$http_code" != "$expected_http" ]]; then
  130. echo "${name} failed: expected http=${expected_http}, actual=${http_code}, body=${body}" >&2
  131. exit 1
  132. fi
  133. if [[ "$actual_code" != "$expected_code" ]]; then
  134. echo "${name} failed: expected code=${expected_code}, actual=${actual_code}, body=${body}" >&2
  135. exit 1
  136. fi
  137. log "${name} -> expected failure confirmed (http=${http_code}, code=${actual_code})"
  138. }
  139. build_payload() {
  140. python3 - "$1" "$2" "$3" "$4" "$5" <<'PY'
  141. import json
  142. import sys
  143. interface, ip, prefix, gateway, dns_csv = sys.argv[1:6]
  144. dns = [item.strip() for item in dns_csv.split(',') if item.strip()]
  145. payload = {
  146. "interface": interface,
  147. "ip": ip,
  148. "prefix": int(prefix),
  149. "gateway": gateway,
  150. "dns": dns,
  151. }
  152. print(json.dumps(payload, ensure_ascii=False))
  153. PY
  154. }
  155. log "discover interface"
  156. result="$(request GET "/api/network/interfaces" "$PASSWORD")"
  157. http_code="$(printf '%s\n' "$result" | read_http_code)"
  158. body="$(printf '%s\n' "$result" | read_body)"
  159. if [[ "$http_code" != "200" ]]; then
  160. echo "Failed to query interfaces: http=${http_code} body=${body}" >&2
  161. exit 1
  162. fi
  163. if [[ -z "$INTERFACE" ]]; then
  164. INTERFACE="$(json_get "$body" "data.suggested_target_interface")"
  165. fi
  166. if [[ -z "$INTERFACE" || "$INTERFACE" == "None" ]]; then
  167. echo "Could not determine target interface. Pass --interface explicitly." >&2
  168. exit 1
  169. fi
  170. log "target interface=${INTERFACE}"
  171. log "wrong password should be rejected"
  172. result="$(request GET "/api/health" "wrong-password")"
  173. expect_failure "wrong password health" "401" "1002" "$result"
  174. log "missing interface should fail validate"
  175. payload="$(build_payload "not-a-real-iface" "192.168.10.20" "24" "192.168.10.1" "8.8.8.8")"
  176. result="$(request POST "/api/network/validate" "$PASSWORD" "$payload")"
  177. expect_failure "invalid interface validate" "400" "3001" "$result"
  178. log "invalid ip should fail validate"
  179. payload="$(build_payload "$INTERFACE" "999.999.1.1" "24" "192.168.10.1" "8.8.8.8")"
  180. result="$(request POST "/api/network/validate" "$PASSWORD" "$payload")"
  181. expect_failure "invalid ip validate" "400" "3001" "$result"
  182. log "gateway outside subnet should fail validate"
  183. payload="$(build_payload "$INTERFACE" "192.168.10.20" "24" "10.0.0.1" "8.8.8.8")"
  184. result="$(request POST "/api/network/validate" "$PASSWORD" "$payload")"
  185. expect_failure "gateway subnet validate" "400" "3001" "$result"
  186. log "invalid dns should fail validate"
  187. payload="$(build_payload "$INTERFACE" "192.168.10.20" "24" "192.168.10.1" "8.8.8.8,not-an-ip")"
  188. result="$(request POST "/api/network/validate" "$PASSWORD" "$payload")"
  189. expect_failure "invalid dns validate" "400" "3001" "$result"
  190. log "negative checks completed"