Skip to content

Commit 6167f56

Browse files
committed
feat(server): unify scan target param on target, keep url as REST alias
The scan target was named inconsistently across surfaces: the MCP `scan_with_dalfox` tool and every response payload (`ResultPayload.target`, the submit `{scan_id, target}`) used `target`, but the REST request body and query string used `url`. Anyone wiring up both surfaces hit the mismatch. Standardize on `target` and keep `url` working for existing REST clients: - `ScanRequest.url` → `target` with `#[serde(alias = "url")]`, so POST `/scan` and POST `/preflight` accept `{"target": ...}` (canonical) or `{"url": ...}`. - GET `/scan` reads the `target` query param, falling back to `url`. - MCP already used `target` — unchanged. Backwards compatible: every existing client sending `url` (JSON body or query string) keeps working; responses are unchanged. Verified end-to-end against a live server: POST/GET with `target` and with the `url` alias all return a scan_id, `/preflight` with `target` works, and a body with neither field is a 400. Docs (integrations/server.md) updated to show `target` with the alias noted; added serde-alias + GET-`target` unit tests.
1 parent 016c22e commit 6167f56

4 files changed

Lines changed: 59 additions & 17 deletions

File tree

docs/content/integrations/server.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,15 @@ For browser clients that can't set custom headers:
5151

5252
```bash
5353
dalfox server --jsonp --callback-param-name callback
54-
# then GET /scan?url=...&callback=myFunction
54+
# then GET /scan?target=...&callback=myFunction
5555
```
5656

5757
## Endpoints
5858

5959
| Method | Path | What it does |
6060
|--------|------|--------------|
6161
| `POST` | `/scan` | Submit a new scan (JSON body) |
62-
| `GET` | `/scan?url=...` | Submit a new scan (query string) |
62+
| `GET` | `/scan?target=...` | Submit a new scan (query string) |
6363
| `GET` | `/scan/:id` | Get scan status and results |
6464
| `DELETE` | `/scan/:id` | Cancel a queued or running scan |
6565
| `GET` | `/scans` | List all scans (optional `?status=`) |
@@ -74,7 +74,7 @@ curl -X POST http://127.0.0.1:6664/scan \
7474
-H "X-API-KEY: change-me" \
7575
-H "Content-Type: application/json" \
7676
-d '{
77-
"url": "https://target.app?q=test",
77+
"target": "https://target.app?q=test",
7878
"options": {
7979
"worker": 50,
8080
"timeout": 10,
@@ -84,6 +84,8 @@ curl -X POST http://127.0.0.1:6664/scan \
8484
}'
8585
```
8686

87+
The scan target field is `target` (matching the MCP `scan_with_dalfox` tool and the response payload). The legacy field name `url` is still accepted as an alias — for both the JSON body and the `?target=` / `?url=` query string — so existing clients keep working.
88+
8789
Response:
8890

8991
```json

src/server/handlers.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ pub(crate) async fn start_scan_handler(
4141

4242
// Trim once and use the trimmed value throughout (validation, scan_id,
4343
// stored target, dispatch) so whitespace variants stay consistent.
44-
let url = req.url.trim().to_string();
44+
let url = req.target.trim().to_string();
4545
if url.is_empty() {
4646
let resp = ApiResponse::<serde_json::Value> {
4747
code: 400,
@@ -257,8 +257,11 @@ pub(crate) async fn get_scan_handler(
257257
// Trim once and use the trimmed value throughout, so whitespace variants
258258
// of the same URL validate, hash to the same scan_id, and store the same
259259
// target consistently.
260+
// `target` is the canonical param (matches POST/MCP); `url` stays as a
261+
// backwards-compatible alias for existing query-string / JSONP callers.
260262
let url = params
261-
.get("url")
263+
.get("target")
264+
.or_else(|| params.get("url"))
262265
.cloned()
263266
.unwrap_or_default()
264267
.trim()
@@ -767,7 +770,7 @@ pub(crate) async fn preflight_handler(
767770
}
768771
};
769772

770-
let target_url = req.url.trim().to_string();
773+
let target_url = req.target.trim().to_string();
771774
if target_url.is_empty() || !has_http_scheme(&target_url) {
772775
let resp = ApiResponse::<serde_json::Value> {
773776
code: 400,

src/server/tests.rs

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -656,7 +656,7 @@ async fn test_start_scan_handler_unauthorized_and_bad_request_jsonp() {
656656
HeaderMap::new(),
657657
Query(params_auth),
658658
Ok(Json(ScanRequest {
659-
url: "http://example.com".to_string(),
659+
target: "http://example.com".to_string(),
660660
options: None,
661661
})),
662662
)
@@ -674,7 +674,7 @@ async fn test_start_scan_handler_unauthorized_and_bad_request_jsonp() {
674674
HeaderMap::new(),
675675
Query(params_bad_req),
676676
Ok(Json(ScanRequest {
677-
url: " ".to_string(),
677+
target: " ".to_string(),
678678
options: None,
679679
})),
680680
)
@@ -693,7 +693,7 @@ async fn test_start_scan_handler_success_creates_queued_job() {
693693
HeaderMap::new(),
694694
Query(Map::new()),
695695
Ok(Json(ScanRequest {
696-
url: "http://127.0.0.1:1/".to_string(),
696+
target: "http://127.0.0.1:1/".to_string(),
697697
options: Some(ScanOptions {
698698
include_request: Some(true),
699699
include_response: Some(true),
@@ -738,7 +738,7 @@ async fn test_start_scan_handler_success_jsonp_response() {
738738
HeaderMap::new(),
739739
Query(q),
740740
Ok(Json(ScanRequest {
741-
url: "http://127.0.0.1:1/".to_string(),
741+
target: "http://127.0.0.1:1/".to_string(),
742742
options: None,
743743
})),
744744
)
@@ -1631,7 +1631,7 @@ async fn test_preflight_handler_rejects_invalid_url() {
16311631
HeaderMap::new(),
16321632
Query(Map::new()),
16331633
Ok(Json(ScanRequest {
1634-
url: "not-http".to_string(),
1634+
target: "not-http".to_string(),
16351635
options: None,
16361636
})),
16371637
)
@@ -1648,7 +1648,7 @@ async fn test_preflight_handler_requires_auth() {
16481648
HeaderMap::new(),
16491649
Query(Map::new()),
16501650
Ok(Json(ScanRequest {
1651-
url: "http://example.com".to_string(),
1651+
target: "http://example.com".to_string(),
16521652
options: None,
16531653
})),
16541654
)
@@ -1665,7 +1665,7 @@ async fn test_preflight_handler_unreachable_target() {
16651665
HeaderMap::new(),
16661666
Query(Map::new()),
16671667
Ok(Json(ScanRequest {
1668-
url: "http://127.0.0.1:1/unreachable".to_string(),
1668+
target: "http://127.0.0.1:1/unreachable".to_string(),
16691669
options: Some(ScanOptions {
16701670
timeout: Some(1),
16711671
..ScanOptions::default()
@@ -1791,7 +1791,7 @@ async fn test_start_scan_handler_rejects_out_of_range_timeout() {
17911791
HeaderMap::new(),
17921792
Query(Map::new()),
17931793
Ok(Json(ScanRequest {
1794-
url: "http://example.com".to_string(),
1794+
target: "http://example.com".to_string(),
17951795
options: Some(ScanOptions {
17961796
timeout: Some(9999),
17971797
..ScanOptions::default()
@@ -1834,7 +1834,7 @@ async fn test_start_scan_handler_rejects_non_http_url() {
18341834
HeaderMap::new(),
18351835
Query(Map::new()),
18361836
Ok(Json(ScanRequest {
1837-
url: bad.to_string(),
1837+
target: bad.to_string(),
18381838
options: None,
18391839
})),
18401840
)
@@ -2570,7 +2570,7 @@ async fn test_start_scan_handler_503_when_at_capacity() {
25702570
HeaderMap::new(),
25712571
Query(Map::new()),
25722572
Ok(Json(ScanRequest {
2573-
url: "http://example.com".to_string(),
2573+
target: "http://example.com".to_string(),
25742574
options: None,
25752575
})),
25762576
)
@@ -2811,3 +2811,36 @@ async fn test_get_scan_handler_analyze_external_js_param_is_accepted() {
28112811
"job must be present in the queue after handler returns"
28122812
);
28132813
}
2814+
2815+
// Target/URL param unification: `target` is canonical (matches MCP + response),
2816+
// `url` stays as a backwards-compatible alias on the REST surface.
2817+
#[test]
2818+
fn test_scan_request_accepts_target_canonical_and_url_alias() {
2819+
let from_target: ScanRequest =
2820+
serde_json::from_str(r#"{"target":"http://a.test/?q=1"}"#).expect("target key parses");
2821+
assert_eq!(from_target.target, "http://a.test/?q=1");
2822+
2823+
let from_url: ScanRequest =
2824+
serde_json::from_str(r#"{"url":"http://b.test/?q=1"}"#).expect("url alias parses");
2825+
assert_eq!(from_url.target, "http://b.test/?q=1");
2826+
2827+
// Neither key present is a hard error (target is required).
2828+
assert!(serde_json::from_str::<ScanRequest>(r#"{"options":{}}"#).is_err());
2829+
}
2830+
2831+
#[tokio::test]
2832+
async fn test_get_scan_handler_accepts_target_query_param() {
2833+
let state = make_state(None, None, false, false, "cb");
2834+
let mut params = Map::new();
2835+
params.insert("target".to_string(), "http://127.0.0.1:1/?q=1".to_string());
2836+
let resp = get_scan_handler(State(state.clone()), HeaderMap::new(), Query(params))
2837+
.await
2838+
.into_response();
2839+
assert_eq!(resp.status(), StatusCode::OK);
2840+
let body = response_body_string(resp).await;
2841+
let parsed: serde_json::Value = serde_json::from_str(&body).expect("json body");
2842+
assert!(
2843+
parsed["data"]["scan_id"].as_str().is_some(),
2844+
"a scan submitted via the `target` query param must return a scan_id"
2845+
);
2846+
}

src/server/types.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,11 @@ pub(crate) struct ApiResponse<T> {
135135

136136
#[derive(Debug, Clone, Deserialize)]
137137
pub(crate) struct ScanRequest {
138-
pub(crate) url: String,
138+
/// Scan target. Named `target` to match the MCP `scan_with_dalfox` tool and
139+
/// the response payload's `target` field; `url` is accepted as a backwards-
140+
/// compatible alias so existing REST clients keep working.
141+
#[serde(alias = "url")]
142+
pub(crate) target: String,
139143
#[serde(default)]
140144
pub(crate) options: Option<ScanOptions>,
141145
}

0 commit comments

Comments
 (0)