fix: address protocol probe review findings
This commit is contained in:
+127
-13
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user