fix: address protocol probe review findings

This commit is contained in:
proxyctl coordinator
2026-07-18 00:44:10 +08:00
parent 02b49d5112
commit 954e7d977a
12 changed files with 315 additions and 489 deletions
+127 -13
View File
@@ -1,12 +1,14 @@
package tui
import (
"errors"
"fmt"
"strings"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"proxyctl/internal/probe"
"proxyctl/internal/profile"
)
@@ -22,12 +24,19 @@ type formDefinition struct {
value string
}
type protocolProbeFieldState struct {
active bool
type probeToken struct {
formID uint64
generation uint64
address string
feedback string
field profile.Field
source profile.Field
}
type protocolProbeFieldState struct {
active bool
token probeToken
address string
feedback string
sourceValue string
sourceAttached bool
}
type formState struct {
@@ -39,6 +48,7 @@ type formState struct {
bypassPreviewLine int
validation profile.Validation
advancedInitialized bool
probeGeneration uint64
probe protocolProbeFieldState
}
@@ -136,7 +146,7 @@ func (f *formState) updateInput(key tea.KeyMsg) tea.Cmd {
f.bypassPreviewLine = 0
changedField := f.inputs[f.field].field
f.syncFocusedValue()
if changedField == profile.SingleURLField {
if isProxyURLField(changedField) {
f.invalidateProbe()
}
f.refreshValidation()
@@ -195,12 +205,106 @@ func (f formState) currentField() profile.Field {
return f.inputs[f.field].field
}
func isProxyURLField(field profile.Field) bool {
switch field {
case profile.SingleURLField, profile.HTTPURLField, profile.HTTPSURLField, profile.AllURLField:
return true
default:
return false
}
}
func (f *formState) invalidateProbe() {
f.probe.generation++
f.probeGeneration++
f.probe = protocolProbeFieldState{}
}
// startProbe owns applicability, duplicate suppression, source capture, and
// token issuance so callers cannot accidentally start work outside formState's
// stale-result rules.
func (f *formState) startProbe() (string, probeToken, bool) {
if f.profile.Mode != profile.SingleMode || f.currentField() != profile.SingleURLField || f.probe.active {
return "", probeToken{}, false
}
f.syncFocusedValue()
f.invalidateProbe()
token := probeToken{formID: f.id, generation: f.probeGeneration, source: profile.SingleURLField}
f.probe.token = token
f.probe.sourceValue = f.profile.SingleURL
address, err := probeAddress(f.probe.sourceValue)
if err != nil {
f.probe.feedback = "Protocol Probe: " + err.Error()
return "", probeToken{}, false
}
f.probe.active = true
f.probe.address = address
f.probe.feedback = "Protocol Probe: checking " + address + "…"
return address, token, true
}
// applyProbeResult validates and applies a completion as one form-owned
// operation. Every stale-result invariant stays here: form identity,
// generation, source field, active work, mode, and unchanged source value.
func (f *formState) applyProbeResult(token probeToken, caps probe.Capabilities, resultErr error) tea.Cmd {
if !f.probe.active || token != f.probe.token || token.formID != f.id ||
token.source != profile.SingleURLField || f.profile.Mode != profile.SingleMode ||
f.profile.SingleURL != f.probe.sourceValue {
return nil
}
f.probe.active = false
f.probe.address = ""
f.probe.feedback = ""
f.probe.field = ""
address, raw := f.probe.address, f.probe.sourceValue
switch {
case resultErr != nil:
f.probe.feedback = "Protocol Probe failed for " + address
case !caps.HTTP && !caps.SOCKS5:
f.probe.feedback = "Protocol Probe: unrecognized protocol at " + address
case caps.HTTP && caps.SOCKS5:
httpURL, err := probe.URL(raw, probe.Capabilities{HTTP: true})
if err != nil {
f.probe.feedback = "Protocol Probe could not apply the detected capabilities"
return nil
}
socksURL, err := probe.URL(raw, probe.Capabilities{SOCKS5: true})
if err != nil {
f.probe.feedback = "Protocol Probe could not apply the detected capabilities"
return nil
}
f.profile.Mode = profile.AdvancedMode
f.profile.HTTPURL = httpURL
f.profile.HTTPSURL = httpURL
f.profile.AllURL = strings.Replace(socksURL, "socks5h://", "socks5://", 1)
f.advancedInitialized = true
f.probe.feedback = "Protocol Probe: detected HTTP and SOCKS5 proxy at " + address
f.probe.sourceAttached = true
return f.rebuildInputs(1)
default:
rewritten, err := probe.URL(raw, caps)
if err != nil {
f.probe.feedback = "Protocol Probe could not apply the detected capabilities"
return nil
}
f.profile.SingleURL = rewritten
f.setInputValue(profile.SingleURLField, rewritten)
kind := "SOCKS5"
if caps.HTTP {
kind = "HTTP"
}
f.probe.feedback = "Protocol Probe: detected " + kind + " proxy at " + address
}
f.refreshValidation()
return nil
}
func (f *formState) setInputValue(field profile.Field, value string) {
for i := range f.inputs {
if f.inputs[i].field != field {
continue
}
position := f.inputs[i].input.Position()
f.inputs[i].input.SetValue(value)
f.inputs[i].input.SetCursor(position)
return
}
}
func (f formState) probeText(field profile.Field) string {
@@ -210,12 +314,22 @@ func (f formState) probeText(field profile.Field) string {
}
return "Protocol Probe [Ctrl+D]: available"
}
if field == f.probe.field {
return f.probe.feedback
}
return ""
}
func (f formState) hasProbeSourceAttachment() bool {
return f.profile.Mode == profile.AdvancedMode && f.probe.sourceAttached &&
f.probe.token.source == profile.SingleURLField && f.probe.feedback != ""
}
func probeAddress(raw string) (string, error) {
u, err := profile.ParseProxyURL(raw)
if err != nil {
return "", errors.New("Single proxy URL must include host:port")
}
return u.Host, nil
}
func (f *formState) scrollBypassPreview(delta int) {
if f.currentField() != profile.BypassField {
return