mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-10-27 17:03:49 +08:00
fix: 按 Grok 复审修复 Repeater 发送竞态、二进制正文预填、弹窗布局与代理测试(Codex gpt-6.1-sol)(#42)
This commit is contained in:
1 parent
ddfdc36787
commit
5e908e9f39
4 files changed
+91
-14
No files matched your search
+42
-3
@@ -2,12 +2,16 @@ package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -195,12 +199,12 @@ func TestRepeaterRejectsCrossHostRedirect(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepeaterSocks5TransportConstruction(t *testing.T) {
|
||||
func TestRepeaterProxyTransportConstruction(t *testing.T) {
|
||||
s := &Server{m: &Manager{}}
|
||||
for _, scheme := range []string{"socks5", "socks5h"} {
|
||||
for _, scheme := range []string{"http", "https", "socks5", "socks5h"} {
|
||||
tr := s.httpProxyTransport(httpExec{Proxy: scheme + "://user:[email protected]:1080"})
|
||||
if tr == nil || tr.Proxy == nil {
|
||||
t.Fatalf("%s transport not configured as SOCKS: %#v", scheme, tr)
|
||||
t.Fatalf("%s proxy transport not configured: %#v", scheme, tr)
|
||||
}
|
||||
proxyURL, err := tr.Proxy(httptest.NewRequest(http.MethodGet, "https://target.example/", nil))
|
||||
if err != nil || proxyURL == nil || proxyURL.Scheme != scheme || proxyURL.Host != "127.0.0.1:1080" {
|
||||
@@ -245,4 +249,39 @@ func TestRepeaterSocks5TransportConstruction(t *testing.T) {
|
||||
if err != nil || proxyURL == nil || proxyURL.Scheme != "http" || proxyURL.Host != "127.0.0.1:18788" {
|
||||
t.Fatalf("recording proxy URL = %#v, err=%v", proxyURL, err)
|
||||
}
|
||||
caFile := s.m.ProxyCACert()
|
||||
if caFile == "" {
|
||||
t.Fatal("recording proxy CA path is empty")
|
||||
}
|
||||
caPEM, err := os.ReadFile(caFile)
|
||||
if err != nil {
|
||||
t.Fatalf("read recording proxy CA: %v", err)
|
||||
}
|
||||
wantRoots := x509.NewCertPool()
|
||||
if !wantRoots.AppendCertsFromPEM(caPEM) {
|
||||
t.Fatal("recording proxy CA is not valid PEM")
|
||||
}
|
||||
if tr.TLSClientConfig == nil || tr.TLSClientConfig.RootCAs == nil || !tr.TLSClientConfig.RootCAs.Equal(wantRoots) {
|
||||
t.Fatal("recording proxy CA was not loaded into transport RootCAs")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepeaterRedactProxyCredentials(t *testing.T) {
|
||||
for _, scheme := range []string{"http", "https", "socks5", "socks5h"} {
|
||||
t.Run(scheme, func(t *testing.T) {
|
||||
proxyErr := &url.Error{
|
||||
Op: "proxyconnect", URL: scheme + "://proxy-user:[email protected]:1080",
|
||||
Err: errors.New("connection refused"),
|
||||
}
|
||||
for _, err := range []error{proxyErr, fmt.Errorf("send request: %w", proxyErr)} {
|
||||
got := repeaterRedactError(err)
|
||||
if strings.Contains(got, "proxy-secret") || strings.Contains(got, "proxy-user") {
|
||||
t.Fatalf("proxy credentials leaked: %q", got)
|
||||
}
|
||||
if got != "proxyconnect 127.0.0.1:1080: connection refused" {
|
||||
t.Fatalf("unexpected redacted error: %q", got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -110,24 +110,40 @@ export function FindingRepeaterDialog({
|
||||
const [response, setResponse] = React.useState<RepeaterSendResponse | null>(null);
|
||||
const [history, setHistory] = React.useState<HistoryEntry[]>([]);
|
||||
const sequence = React.useRef(0);
|
||||
const generation = React.useRef(0);
|
||||
const sending = React.useRef(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
sending.current = false;
|
||||
setBusy(false);
|
||||
if (!open) return;
|
||||
const nextMethod = (initial?.method ?? "GET").toUpperCase();
|
||||
setMethod(METHODS.includes(nextMethod as (typeof METHODS)[number]) ? nextMethod : "GET");
|
||||
setURL(initial?.url ?? "");
|
||||
setHeadersText(initial?.headersText ?? "");
|
||||
setBody(initial?.body ?? "");
|
||||
setUseProxy(true);
|
||||
setInsecure(false);
|
||||
setResponse(null);
|
||||
return () => {
|
||||
// Closing, reseeding, or unmounting makes any pending result obsolete.
|
||||
generation.current += 1;
|
||||
sending.current = false;
|
||||
};
|
||||
}, [open, initial?.method, initial?.url, initial?.headersText, initial?.body]);
|
||||
|
||||
async function send() {
|
||||
// State alone cannot stop a second click before React disables the button.
|
||||
if (sending.current || !open) return;
|
||||
const target = url.trim();
|
||||
if (!target) {
|
||||
toast.error("请填写 URL");
|
||||
return;
|
||||
}
|
||||
const requestGeneration = ++generation.current;
|
||||
sending.current = true;
|
||||
setBusy(true);
|
||||
setResponse(null);
|
||||
try {
|
||||
const result = await api.repeaterSend({
|
||||
method,
|
||||
@@ -137,23 +153,38 @@ export function FindingRepeaterDialog({
|
||||
use_recording_proxy: useProxy,
|
||||
insecure_skip_verify: insecure,
|
||||
});
|
||||
if (requestGeneration !== generation.current) return;
|
||||
setResponse(result);
|
||||
sequence.current += 1;
|
||||
const id = ++sequence.current;
|
||||
setHistory((previous) =>
|
||||
[
|
||||
{ id: sequence.current, method, url: target, headersText, body, useProxy, insecure, response: result },
|
||||
{ id, method, url: target, headersText, body, useProxy, insecure, response: result },
|
||||
...previous,
|
||||
].slice(0, 20),
|
||||
);
|
||||
} catch (error) {
|
||||
toast.error((error as Error).message);
|
||||
if (requestGeneration === generation.current) toast.error((error as Error).message);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
if (requestGeneration === generation.current) {
|
||||
sending.current = false;
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(nextOpen) => {
|
||||
if (!nextOpen) {
|
||||
// Invalidate immediately, before the parent's close render/effect.
|
||||
generation.current += 1;
|
||||
sending.current = false;
|
||||
setBusy(false);
|
||||
}
|
||||
onOpenChange(nextOpen);
|
||||
}}
|
||||
>
|
||||
<DialogContent className="flex max-h-[92vh] w-[min(1150px,96vw)] max-w-none flex-col gap-3 overflow-hidden sm:max-w-none">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Repeater · 请求重放</DialogTitle>
|
||||
@@ -164,7 +195,7 @@ export function FindingRepeaterDialog({
|
||||
<div className="flex flex-wrap items-end gap-2">
|
||||
<div className="w-28">
|
||||
<Label className="mb-1 block text-xs text-muted-foreground">方法</Label>
|
||||
<Select value={method} onValueChange={setMethod}>
|
||||
<Select value={method} onValueChange={setMethod} disabled={busy}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
@@ -182,6 +213,7 @@ export function FindingRepeaterDialog({
|
||||
<div className="min-w-[240px] flex-1">
|
||||
<Label className="mb-1 block text-xs text-muted-foreground">URL</Label>
|
||||
<Input
|
||||
disabled={busy}
|
||||
value={url}
|
||||
onChange={(event) => setURL(event.target.value)}
|
||||
placeholder="https://target.example/path"
|
||||
@@ -197,6 +229,7 @@ export function FindingRepeaterDialog({
|
||||
<Label className="flex items-center gap-2" htmlFor="repeater-use-proxy">
|
||||
<Checkbox
|
||||
id="repeater-use-proxy"
|
||||
disabled={busy}
|
||||
checked={useProxy}
|
||||
onCheckedChange={(value) => setUseProxy(value === true)}
|
||||
/>
|
||||
@@ -205,6 +238,7 @@ export function FindingRepeaterDialog({
|
||||
<Label className="flex items-center gap-2" htmlFor="repeater-insecure">
|
||||
<Checkbox
|
||||
id="repeater-insecure"
|
||||
disabled={busy}
|
||||
checked={insecure}
|
||||
onCheckedChange={(value) => setInsecure(value === true)}
|
||||
/>
|
||||
@@ -258,6 +292,7 @@ export function FindingRepeaterDialog({
|
||||
<Label className="text-xs text-muted-foreground">请求头</Label>
|
||||
<Textarea
|
||||
className="min-h-[120px] flex-1 font-mono text-xs"
|
||||
disabled={busy}
|
||||
value={headersText}
|
||||
onChange={(event) => setHeadersText(event.target.value)}
|
||||
placeholder="Header: value"
|
||||
@@ -268,6 +303,7 @@ export function FindingRepeaterDialog({
|
||||
<Label className="text-xs text-muted-foreground">请求体</Label>
|
||||
<Textarea
|
||||
className="min-h-[120px] flex-1 font-mono text-xs"
|
||||
disabled={busy}
|
||||
value={body}
|
||||
onChange={(event) => setBody(event.target.value)}
|
||||
placeholder="请求正文(可选)"
|
||||
|
||||
@@ -102,12 +102,13 @@ export function FindingTrafficPanel({
|
||||
setRepeaterBusy(true);
|
||||
try {
|
||||
const detail = await api.findingTrafficDetail(findingId, bindingId, contextTask);
|
||||
if (detail.request.truncated) toast.info("请求体为预览片段,完整正文请先下载");
|
||||
if (detail.request.binary) toast.info("请求体为二进制,已留空;完整正文请先下载");
|
||||
else if (detail.request.truncated) toast.info("请求体为预览片段,完整正文请先下载");
|
||||
setRepeaterInitial({
|
||||
method: detail.binding.snapshot.method || "GET",
|
||||
url: detail.binding.snapshot.url || "",
|
||||
headersText: headersFromReqHead(detail.binding.snapshot.req_head),
|
||||
body: detail.request.content || "",
|
||||
body: detail.request.binary ? "" : detail.request.content || "",
|
||||
});
|
||||
setRepeaterOpen(true);
|
||||
} catch (e) {
|
||||
|
||||
@@ -67,12 +67,13 @@ export function TrafficEvidenceViewer({
|
||||
|
||||
function openRepeater() {
|
||||
if (!detail) return;
|
||||
if (detail.request.truncated) toast.info("请求体为预览片段,完整正文请先下载");
|
||||
if (detail.request.binary) toast.info("请求体为二进制,已留空;完整正文请先下载");
|
||||
else if (detail.request.truncated) toast.info("请求体为预览片段,完整正文请先下载");
|
||||
setRepeaterInitial({
|
||||
method: detail.binding.snapshot.method || "GET",
|
||||
url: detail.binding.snapshot.url || "",
|
||||
headersText: headersFromReqHead(detail.binding.snapshot.req_head),
|
||||
body: detail.request.content || "",
|
||||
body: detail.request.binary ? "" : detail.request.content || "",
|
||||
});
|
||||
setRepeaterOpen(true);
|
||||
}
|
||||
@@ -87,7 +88,7 @@ export function TrafficEvidenceViewer({
|
||||
>
|
||||
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-4xl">
|
||||
<DialogHeader>
|
||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||
<div className="flex flex-wrap items-start justify-between gap-2 pe-8">
|
||||
<div>
|
||||
<DialogTitle>流量证据 #{bindingId}</DialogTitle>
|
||||
<DialogDescription className="break-all">
|
||||
|
||||
Reference in new issue
Block a user