fix: 按 Grok 复审修复 Repeater 发送竞态、二进制正文预填、弹窗布局与代理测试(Codex gpt-6.1-sol)(#42)

This commit is contained in:
RuoJi6 committed 2026-10-24 23:17:45 +08:00
1 parent ddfdc36787
commit 5e908e9f39
4 files changed
+91 -14

No files matched your search

+42 -3
View File
@@ -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)
}
}
})
}
}
+42 -6
View File
@@ -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="请求正文(可选)"
+3 -2
View File
@@ -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">