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
+9 -86
View File
@@ -2,9 +2,7 @@ package tui
import (
"context"
"errors"
"fmt"
"strings"
tea "github.com/charmbracelet/bubbletea"
@@ -47,14 +45,12 @@ type Model struct {
detect func(context.Context, string) (probe.Capabilities, error)
}
// probeDone carries one finished Protocol Probe back to the form. Endpoint and
// input text deliberately stay in formState: an asynchronous result cannot
// introduce userinfo into status, and formID plus generation reject stale work.
// probeDone carries only detector output and formState's opaque typed token.
// Source data and all validation/application rules remain owned by formState.
type probeDone struct {
caps probe.Capabilities
err error
formID uint64
generation uint64
caps probe.Capabilities
err error
token probeToken
}
type profileDeleteConfirmation struct {
@@ -177,57 +173,7 @@ func (m Model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
return m, waitForOperation(progress.updates)
}
if done, ok := message.(probeDone); ok {
if m.page != formPage || !m.form.probe.active || done.formID != m.form.id || done.generation != m.form.probe.generation {
return m, nil
}
m.form.probe.active = false
address := m.form.probe.address
switch {
case done.err != nil:
// Detector errors are intentionally not rendered. Production errors are
// based on the credential-free dial endpoint, but this boundary also
// protects frames if a lower layer returns hostile credential text.
m.form.probe.feedback = "Protocol Probe failed for " + address
case !done.caps.HTTP && !done.caps.SOCKS5:
m.form.probe.feedback = "Protocol Probe: unrecognized protocol at " + address
default:
raw := m.form.profile.SingleURL
if done.caps.HTTP && done.caps.SOCKS5 {
httpURL, err := probe.URL(raw, probe.Capabilities{HTTP: true})
if err != nil {
m.form.probe.feedback = "Protocol Probe could not apply the detected capabilities"
return m, nil
}
socksURL, err := probe.URL(raw, probe.Capabilities{SOCKS5: true})
if err != nil {
m.form.probe.feedback = "Protocol Probe could not apply the detected capabilities"
return m, nil
}
m.form.profile.Mode = profile.AdvancedMode
m.form.profile.HTTPURL = httpURL
m.form.profile.HTTPSURL = httpURL
m.form.profile.AllURL = strings.Replace(socksURL, "socks5h://", "socks5://", 1)
m.form.advancedInitialized = true
m.form.probe.field = profile.HTTPURLField
m.form.probe.feedback = "Protocol Probe: detected HTTP and SOCKS5 proxy at " + address
m.form.rebuildInputs(1)
return m, nil
}
rewritten, err := probe.URL(raw, done.caps)
if err != nil {
m.form.probe.feedback = "Protocol Probe could not apply the detected capabilities"
return m, nil
}
m.form.profile.SingleURL = rewritten
m.form.probe.field = profile.SingleURLField
kind := "SOCKS5"
if done.caps.HTTP {
kind = "HTTP"
}
m.form.probe.feedback = "Protocol Probe: detected " + kind + " proxy at " + address
m.form.rebuildInputs(1)
}
return m, nil
return m, m.form.applyProbeResult(done.token, done.caps, done.err)
}
key, ok := message.(tea.KeyMsg)
if !ok {
@@ -596,26 +542,14 @@ func (m Model) updateForm(key tea.KeyMsg) (tea.Model, tea.Cmd) {
m.form = formState{}
return m, nil
case tea.KeyCtrlD:
if m.form.profile.Mode != profile.SingleMode || m.form.currentField() != profile.SingleURLField || m.form.probe.active {
address, token, started := m.form.startProbe()
if !started {
return m, nil
}
m.form.syncFocusedValue()
m.form.invalidateProbe()
m.form.probe.field = profile.SingleURLField
address, err := probeAddress(m.form.profile.SingleURL)
if err != nil {
m.form.probe.feedback = "Protocol Probe: " + err.Error()
return m, nil
}
m.form.probe.active = true
m.form.probe.address = address
m.form.probe.feedback = "Protocol Probe: checking " + address + "…"
detect := m.detect
formID := m.form.id
generation := m.form.probe.generation
return m, func() tea.Msg {
caps, err := detect(context.Background(), address)
return probeDone{caps: caps, err: err, formID: formID, generation: generation}
return probeDone{caps: caps, err: err, token: token}
}
case tea.KeyCtrlT:
return m, m.form.toggleMode()
@@ -653,17 +587,6 @@ func (m *Model) applyProfileMutation(intent app.Intent, preferredName string) ap
return result
}
// probeAddress extracts a bare host:port from the typed endpoint for dialing
// and status text, dropping any userinfo so credentials are never displayed
// or sent anywhere.
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 (m Model) selectedProfile() (profile.Profile, bool) {
if m.selectedProfileName != "" {
for i := range m.snapshot.Profiles {