fix: address protocol probe review findings
This commit is contained in:
@@ -158,8 +158,14 @@ func TestCompleteModelDefensivelyRedactsEveryRenderedPage(t *testing.T) {
|
||||
if strings.Contains(frame, secret) {
|
||||
t.Fatalf("frame %d exposed a credential:\n%s", i, frame)
|
||||
}
|
||||
if !strings.Contains(frame, redacted) {
|
||||
t.Fatalf("frame %d did not apply the presentation redaction path:\n%s", i, frame)
|
||||
if i < 3 {
|
||||
if !strings.Contains(frame, redacted) {
|
||||
t.Fatalf("frame %d did not apply the presentation redaction path:\n%s", i, frame)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if strings.Contains(frame, "alice") || !strings.Contains(frame, "REDACTED@proxy.example:8443") {
|
||||
t.Fatalf("form frame %d did not mask the entire userinfo:\n%s", i, frame)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,8 +149,8 @@ func (p terminalPresentation) contextualHelp(model presentationState) string {
|
||||
default:
|
||||
body.WriteString("The Single proxy URL field visibly offers Protocol Probe; focus that field and press Ctrl+D to run it explicitly.\n")
|
||||
}
|
||||
} else if model.form.probe.feedback != "" {
|
||||
body.WriteString("The Protocol Probe result remains attached to the generated Advanced Profile URL values.\n")
|
||||
} else if model.form.hasProbeSourceAttachment() {
|
||||
body.WriteString("The Protocol Probe result remains attached to its read-only initiating Single proxy URL context. The HTTP, HTTPS, and ALL/SOCKS values were generated from that probe.\n")
|
||||
}
|
||||
default:
|
||||
body.WriteString(p.theme.section.Render("Dashboard"))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -8,6 +9,7 @@ import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
"proxyctl/internal/app"
|
||||
"proxyctl/internal/probe"
|
||||
"proxyctl/internal/profile"
|
||||
)
|
||||
|
||||
@@ -304,10 +306,6 @@ func TestCompleteModelFooterHintsMatchContextAndAcceptedKeys(t *testing.T) {
|
||||
if probeCommand == nil || !strings.Contains(model.View(), "Protocol Probe in progress") || strings.Contains(model.View(), "Ctrl+D: probe") {
|
||||
t.Fatalf("probe footer did not follow accepted in-field action and progress:\n%s", model.View())
|
||||
}
|
||||
model = updateKey(model, "?")
|
||||
assertUsableFrame(t, model.View(), 80, 24, "Protocol Probe is in progress", "Ctrl+D does not start a duplicate probe")
|
||||
model = updateKey(model, "esc")
|
||||
|
||||
model = updateKey(model, "ctrl+t")
|
||||
if strings.Contains(model.View(), "Ctrl+D: probe") || !strings.Contains(model.View(), "advanced mode") {
|
||||
t.Fatalf("Advanced form advertised a Single-field Protocol Probe:\n%s", model.View())
|
||||
@@ -335,6 +333,37 @@ func TestCompleteModelFooterHintsMatchContextAndAcceptedKeys(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompleteModelHelpDocumentsProbeShortcutExplicitOnlyAttachmentAndDuplicateSuppression(t *testing.T) {
|
||||
model := openProtocolProbeEditor(t, &recordingModule{}, func(context.Context, string) (probe.Capabilities, error) {
|
||||
return probe.Capabilities{HTTP: true, SOCKS5: true}, nil
|
||||
}, "proxy.example:7890")
|
||||
|
||||
model = updateKey(model, "?")
|
||||
assertUsableFrame(t, model.View(), 80, 24,
|
||||
"Ctrl+D explicitly runs Protocol Probe for the focused Single proxy URL field.",
|
||||
"never runs on form entry, focus changes, or save.")
|
||||
model = updateKey(model, "esc")
|
||||
|
||||
model, command := startProtocolProbe(t, model)
|
||||
updated, duplicate := model.Update(tea.KeyMsg{Type: tea.KeyCtrlD})
|
||||
model = updated.(Model)
|
||||
if duplicate != nil {
|
||||
t.Fatal("repeated Probe shortcut emitted duplicate work")
|
||||
}
|
||||
model = updateKey(model, "?")
|
||||
assertUsableFrame(t, model.View(), 80, 24,
|
||||
"Protocol Probe is in progress for the Single proxy URL field.",
|
||||
"Ctrl+D does not start a duplicate probe")
|
||||
model = updateKey(model, "esc")
|
||||
|
||||
model = finishProtocolProbe(t, model, command)
|
||||
model = updateKey(model, "?")
|
||||
assertUsableFrame(t, model.View(), 80, 24,
|
||||
"The Protocol Probe result remains attached to its read-only initiating Single",
|
||||
"proxy URL context. The HTTP, HTTPS, and ALL/SOCKS values were generated from",
|
||||
"that probe.")
|
||||
}
|
||||
|
||||
func TestCompleteModelHelpOverEditorPreservesFieldCursorPreviewAndResizeState(t *testing.T) {
|
||||
module := &profileManagementModule{}
|
||||
model := openNewProfileEditor(t, module)
|
||||
|
||||
+9
-86
@@ -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 {
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
"proxyctl/internal/app"
|
||||
"proxyctl/internal/probe"
|
||||
"proxyctl/internal/profile"
|
||||
)
|
||||
|
||||
@@ -138,349 +135,6 @@ func TestNoProfileStateStaysOnAccessibleMainPage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtocolProbeDetectsHTTPAndRewritesSingleURL(t *testing.T) {
|
||||
module := &recordingModule{}
|
||||
model := NewModel(module)
|
||||
var probed []string
|
||||
model.detect = func(_ context.Context, address string) (probe.Capabilities, error) {
|
||||
probed = append(probed, address)
|
||||
return probe.Capabilities{HTTP: true}, nil
|
||||
}
|
||||
model = updateKey(model, "m")
|
||||
model = updateKey(model, "n")
|
||||
model = updateRunes(model, "home")
|
||||
model = updateKey(model, "tab")
|
||||
model = updateRunes(model, "alice:secret@localhost:8888")
|
||||
|
||||
model, cmd := pressCtrlD(model)
|
||||
if cmd == nil {
|
||||
t.Fatal("ctrl+d did not start a Protocol Probe")
|
||||
}
|
||||
updated, _ := model.Update(cmd())
|
||||
model = updated.(Model)
|
||||
|
||||
if len(probed) != 1 || probed[0] != "localhost:8888" {
|
||||
t.Fatalf("Protocol Probe dialed %#v, want exactly localhost:8888 without userinfo", probed)
|
||||
}
|
||||
if got := model.form.profile.SingleURL; got != "http://alice:secret@localhost:8888" {
|
||||
t.Fatalf("Single URL after probe = %q, want detected scheme with preserved userinfo", got)
|
||||
}
|
||||
view := model.View()
|
||||
if !strings.Contains(view, "Protocol Probe: detected HTTP proxy at localhost:8888") {
|
||||
t.Fatalf("form view missing detection status:\n%s", view)
|
||||
}
|
||||
if strings.Contains(view, "secret") {
|
||||
t.Fatalf("form view exposed proxy password:\n%s", view)
|
||||
}
|
||||
}
|
||||
|
||||
func pressCtrlD(model Model) (Model, tea.Cmd) {
|
||||
updated, cmd := model.Update(tea.KeyMsg{Type: tea.KeyCtrlD})
|
||||
return updated.(Model), cmd
|
||||
}
|
||||
|
||||
func TestProtocolProbeFailureLeavesFieldUntouchedAndSavePossible(t *testing.T) {
|
||||
module := &recordingModule{}
|
||||
model := NewModel(module)
|
||||
model.detect = func(_ context.Context, address string) (probe.Capabilities, error) {
|
||||
return probe.Capabilities{}, errors.New("connection refused")
|
||||
}
|
||||
model = updateKey(model, "m")
|
||||
model = updateKey(model, "n")
|
||||
model = updateRunes(model, "home")
|
||||
model = updateKey(model, "tab")
|
||||
model = updateRunes(model, "localhost:8888")
|
||||
|
||||
model, cmd := pressCtrlD(model)
|
||||
if cmd == nil {
|
||||
t.Fatal("ctrl+d did not start a Protocol Probe")
|
||||
}
|
||||
updated, _ := model.Update(cmd())
|
||||
model = updated.(Model)
|
||||
|
||||
if got := model.form.profile.SingleURL; got != "localhost:8888" {
|
||||
t.Fatalf("failed probe rewrote Single URL to %q", got)
|
||||
}
|
||||
if view := model.View(); !strings.Contains(view, "Protocol Probe failed for localhost:8888") || strings.Contains(view, "connection refused") {
|
||||
t.Fatalf("form view missing credential-safe field failure status:\n%s", view)
|
||||
}
|
||||
model = updateKey(model, "ctrl+s")
|
||||
if len(module.intents) != 1 || module.intents[0].Kind != app.CreateProfile {
|
||||
t.Fatalf("save after failed probe dispatched %#v", module.intents)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtocolProbeUnrecognizedProtocolLeavesFieldUntouched(t *testing.T) {
|
||||
module := &recordingModule{}
|
||||
model := NewModel(module)
|
||||
model.detect = func(_ context.Context, address string) (probe.Capabilities, error) {
|
||||
return probe.Capabilities{}, nil
|
||||
}
|
||||
model = updateKey(model, "m")
|
||||
model = updateKey(model, "n")
|
||||
model = updateRunes(model, "home")
|
||||
model = updateKey(model, "tab")
|
||||
model = updateRunes(model, "localhost:8888")
|
||||
|
||||
model, cmd := pressCtrlD(model)
|
||||
if cmd == nil {
|
||||
t.Fatal("ctrl+d did not start a Protocol Probe")
|
||||
}
|
||||
updated, _ := model.Update(cmd())
|
||||
model = updated.(Model)
|
||||
|
||||
if got := model.form.profile.SingleURL; got != "localhost:8888" {
|
||||
t.Fatalf("unrecognized probe rewrote Single URL to %q", got)
|
||||
}
|
||||
if view := model.View(); !strings.Contains(view, "Protocol Probe: unrecognized protocol at localhost:8888") {
|
||||
t.Fatalf("form view missing unrecognized status:\n%s", view)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtocolProbeIgnoresRepeatedCtrlDWhileInFlight(t *testing.T) {
|
||||
module := &recordingModule{}
|
||||
model := NewModel(module)
|
||||
calls := 0
|
||||
model.detect = func(_ context.Context, address string) (probe.Capabilities, error) {
|
||||
calls++
|
||||
return probe.Capabilities{HTTP: true}, nil
|
||||
}
|
||||
model = updateKey(model, "m")
|
||||
model = updateKey(model, "n")
|
||||
model = updateRunes(model, "home")
|
||||
model = updateKey(model, "tab")
|
||||
model = updateRunes(model, "localhost:8888")
|
||||
|
||||
model, first := pressCtrlD(model)
|
||||
if first == nil {
|
||||
t.Fatal("ctrl+d did not start a Protocol Probe")
|
||||
}
|
||||
model, second := pressCtrlD(model)
|
||||
if second != nil {
|
||||
t.Fatal("ctrl+d during an in-flight probe started a second one")
|
||||
}
|
||||
updated, _ := model.Update(first())
|
||||
model = updated.(Model)
|
||||
|
||||
if calls != 1 {
|
||||
t.Fatalf("probe ran %d times, want 1", calls)
|
||||
}
|
||||
if got := model.form.profile.SingleURL; got != "http://localhost:8888" {
|
||||
t.Fatalf("Single URL after probe = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtocolProbeLateResultIgnoredAfterEscLeavesForm(t *testing.T) {
|
||||
module := &recordingModule{}
|
||||
model := NewModel(module)
|
||||
model.detect = func(_ context.Context, address string) (probe.Capabilities, error) {
|
||||
return probe.Capabilities{HTTP: true}, nil
|
||||
}
|
||||
model = updateKey(model, "m")
|
||||
model = updateKey(model, "n")
|
||||
model = updateRunes(model, "home")
|
||||
model = updateKey(model, "tab")
|
||||
model = updateRunes(model, "localhost:8888")
|
||||
|
||||
model, cmd := pressCtrlD(model)
|
||||
if cmd == nil {
|
||||
t.Fatal("ctrl+d did not start a Protocol Probe")
|
||||
}
|
||||
model = updateKey(model, "esc")
|
||||
updated, _ := model.Update(cmd())
|
||||
model = updated.(Model)
|
||||
|
||||
if model.page != profilesPage {
|
||||
t.Fatalf("page = %d, want profilesPage", model.page)
|
||||
}
|
||||
if strings.Contains(model.status, "detected HTTP proxy") {
|
||||
t.Fatalf("profiles page status mutated by late probe result: %s", model.status)
|
||||
}
|
||||
if model.form.profile.SingleURL != "" {
|
||||
t.Fatalf("abandoned form SingleURL mutated to %q", model.form.profile.SingleURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtocolProbeLateResultIgnoredAfterSaveLeavesForm(t *testing.T) {
|
||||
module := &recordingModule{}
|
||||
model := NewModel(module)
|
||||
model.detect = func(_ context.Context, address string) (probe.Capabilities, error) {
|
||||
return probe.Capabilities{HTTP: true}, nil
|
||||
}
|
||||
model = updateKey(model, "m")
|
||||
model = updateKey(model, "n")
|
||||
model = updateRunes(model, "home")
|
||||
model = updateKey(model, "tab")
|
||||
model = updateRunes(model, "localhost:8888")
|
||||
|
||||
model, cmd := pressCtrlD(model)
|
||||
if cmd == nil {
|
||||
t.Fatal("ctrl+d did not start a Protocol Probe")
|
||||
}
|
||||
model = updateKey(model, "ctrl+s")
|
||||
if len(module.intents) != 1 {
|
||||
t.Fatalf("save dispatched %d intents, want 1", len(module.intents))
|
||||
}
|
||||
|
||||
updated, _ := model.Update(cmd())
|
||||
model = updated.(Model)
|
||||
|
||||
if model.page != profilesPage {
|
||||
t.Fatalf("page = %d, want profilesPage", model.page)
|
||||
}
|
||||
if strings.Contains(model.status, "detected HTTP proxy") {
|
||||
t.Fatalf("profiles page status mutated by late probe result: %s", model.status)
|
||||
}
|
||||
if model.form.profile.SingleURL != "" {
|
||||
t.Fatalf("abandoned form SingleURL mutated to %q", model.form.profile.SingleURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveFormDoesNotTriggerProtocolProbe(t *testing.T) {
|
||||
module := &recordingModule{}
|
||||
model := NewModel(module)
|
||||
calls := 0
|
||||
model.detect = func(_ context.Context, address string) (probe.Capabilities, error) {
|
||||
calls++
|
||||
return probe.Capabilities{HTTP: true}, nil
|
||||
}
|
||||
model = updateKey(model, "m")
|
||||
model = updateKey(model, "n")
|
||||
model = updateRunes(model, "home")
|
||||
model = updateKey(model, "tab")
|
||||
model = updateRunes(model, "localhost:8888")
|
||||
|
||||
model = updateKey(model, "ctrl+s")
|
||||
if calls != 0 {
|
||||
t.Fatalf("ctrl+s triggered %d probe call(s), want 0", calls)
|
||||
}
|
||||
if len(module.intents) != 1 || module.intents[0].Kind != app.CreateProfile {
|
||||
t.Fatalf("ctrl+s dispatched %#v", module.intents)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtocolProbeOnlyTriggersOnSingleURLField(t *testing.T) {
|
||||
module := &recordingModule{}
|
||||
model := NewModel(module)
|
||||
calls := 0
|
||||
model.detect = func(_ context.Context, address string) (probe.Capabilities, error) {
|
||||
calls++
|
||||
return probe.Capabilities{HTTP: true}, nil
|
||||
}
|
||||
model = updateKey(model, "m")
|
||||
model = updateKey(model, "n")
|
||||
model = updateRunes(model, "home")
|
||||
|
||||
// Field 0 is Name.
|
||||
model, cmd := pressCtrlD(model)
|
||||
if cmd != nil {
|
||||
t.Fatal("ctrl+d on Name field started a Protocol Probe")
|
||||
}
|
||||
|
||||
// Move to Single URL and probe once so we can reach the Bypass field.
|
||||
model = updateKey(model, "tab")
|
||||
model = updateRunes(model, "localhost:8888")
|
||||
model, cmd = pressCtrlD(model)
|
||||
if cmd == nil {
|
||||
t.Fatal("ctrl+d on Single URL field did not start a Protocol Probe")
|
||||
}
|
||||
updated, _ := model.Update(cmd())
|
||||
model = updated.(Model)
|
||||
|
||||
// Move to Bypass (field 2) and press Ctrl+D again.
|
||||
model = updateKey(model, "tab")
|
||||
model, cmd = pressCtrlD(model)
|
||||
if cmd != nil {
|
||||
t.Fatal("ctrl+d on Bypass field started a Protocol Probe")
|
||||
}
|
||||
|
||||
if calls != 1 {
|
||||
t.Fatalf("Protocol Probe ran %d times, want 1", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtocolProbeDetectsDualProtocolEndpointBuildsCompatibleAdvancedProfile(t *testing.T) {
|
||||
module := &recordingModule{}
|
||||
model := NewModel(module)
|
||||
model.detect = func(_ context.Context, address string) (probe.Capabilities, error) {
|
||||
return probe.Capabilities{HTTP: true, SOCKS5: true}, nil
|
||||
}
|
||||
model = updateKey(model, "m")
|
||||
model = updateKey(model, "n")
|
||||
model = updateRunes(model, "home")
|
||||
model = updateKey(model, "tab")
|
||||
model = updateRunes(model, "localhost:7890")
|
||||
|
||||
model, cmd := pressCtrlD(model)
|
||||
if cmd == nil {
|
||||
t.Fatal("ctrl+d did not start a Protocol Probe")
|
||||
}
|
||||
updated, _ := model.Update(cmd())
|
||||
model = updated.(Model)
|
||||
|
||||
got := model.form.profile
|
||||
if got.Mode != profile.AdvancedMode || got.HTTPURL != "http://localhost:7890" || got.HTTPSURL != "http://localhost:7890" || got.AllURL != "socks5://localhost:7890" {
|
||||
t.Fatalf("dual-protocol Profile = %#v", got)
|
||||
}
|
||||
if view := model.View(); !strings.Contains(view, "Protocol Probe: detected HTTP and SOCKS5 proxy at localhost:7890") {
|
||||
t.Fatalf("form view missing dual-protocol detection status:\n%s", view)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtocolProbeDetectsSOCKS5AndRewritesSingleURL(t *testing.T) {
|
||||
module := &recordingModule{}
|
||||
model := NewModel(module)
|
||||
model.detect = func(_ context.Context, address string) (probe.Capabilities, error) {
|
||||
return probe.Capabilities{SOCKS5: true}, nil
|
||||
}
|
||||
model = updateKey(model, "m")
|
||||
model = updateKey(model, "n")
|
||||
model = updateRunes(model, "home")
|
||||
model = updateKey(model, "tab")
|
||||
model = updateRunes(model, "localhost:8888")
|
||||
|
||||
model, cmd := pressCtrlD(model)
|
||||
if cmd == nil {
|
||||
t.Fatal("ctrl+d did not start a Protocol Probe")
|
||||
}
|
||||
updated, _ := model.Update(cmd())
|
||||
model = updated.(Model)
|
||||
|
||||
if got := model.form.profile.SingleURL; got != "socks5h://localhost:8888" {
|
||||
t.Fatalf("Single URL after SOCKS5 probe = %q, want socks5h://localhost:8888", got)
|
||||
}
|
||||
if view := model.View(); !strings.Contains(view, "Protocol Probe: detected SOCKS5 proxy at localhost:8888") {
|
||||
t.Fatalf("form view missing SOCKS5 detection status:\n%s", view)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtocolProbeInAdvancedModeShowsHintWithoutProbing(t *testing.T) {
|
||||
module := &recordingModule{}
|
||||
model := NewModel(module)
|
||||
calls := 0
|
||||
model.detect = func(_ context.Context, address string) (probe.Capabilities, error) {
|
||||
calls++
|
||||
return probe.Capabilities{HTTP: true}, nil
|
||||
}
|
||||
model = updateKey(model, "m")
|
||||
model = updateKey(model, "n")
|
||||
model = updateRunes(model, "home")
|
||||
model = updateKey(model, "ctrl+t")
|
||||
model = updateKey(model, "tab")
|
||||
model = updateRunes(model, "localhost:8888")
|
||||
|
||||
model, cmd := pressCtrlD(model)
|
||||
if cmd != nil {
|
||||
t.Fatal("ctrl+d in Advanced mode started a Protocol Probe")
|
||||
}
|
||||
if calls != 0 {
|
||||
t.Fatalf("Advanced mode probe ran %d times, want 0", calls)
|
||||
}
|
||||
if view := model.View(); strings.Contains(view, "Protocol Probe") || strings.Contains(view, "Ctrl+D") {
|
||||
t.Fatalf("Advanced mode advertised an inapplicable Protocol Probe action:\n%s", view)
|
||||
}
|
||||
}
|
||||
|
||||
func updateKey(model Model, key string) Model {
|
||||
msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(key)}
|
||||
special := map[string]tea.KeyType{
|
||||
|
||||
@@ -238,6 +238,12 @@ func (p terminalPresentation) formView(model presentationState) terminalPage {
|
||||
fmt.Fprintf(&fields, "\n %s", probeText)
|
||||
currentLine++
|
||||
}
|
||||
if item.field == profile.NameField && model.form.hasProbeSourceAttachment() {
|
||||
fmt.Fprintf(&fields, "\n Single proxy URL probe [read-only]: %s", profile.ObscureURLForEditing(model.form.probe.sourceValue))
|
||||
fmt.Fprintf(&fields, "\n %s", model.form.probe.feedback)
|
||||
fields.WriteString("\n Generated Advanced URL fields from this Single proxy URL probe:")
|
||||
currentLine += 3
|
||||
}
|
||||
validation := model.form.validationText(i)
|
||||
if item.field == profile.NameField {
|
||||
if conflict := model.form.nameConflict(model.snapshot.Profiles); conflict != "" {
|
||||
|
||||
@@ -146,8 +146,13 @@ func (p terminalPresentation) Render(state presentationState) string {
|
||||
page = redactTerminalPage(page)
|
||||
// Dashboard composition retains structured Target metadata until pane sizes
|
||||
// are known. Redact the fully composed frame as the final presentation
|
||||
// boundary as well, so no render path can bypass credential scrubbing.
|
||||
return profile.RedactText(p.renderShell(page, width, height))
|
||||
// boundary as well, so no render path can bypass credential scrubbing. Form
|
||||
// frames use the stricter boundary that removes the entire userinfo.
|
||||
frame := p.renderShell(page, width, height)
|
||||
if state.page == formPage {
|
||||
return profile.RedactUserinfoText(frame)
|
||||
}
|
||||
return profile.RedactText(frame)
|
||||
}
|
||||
|
||||
func renderStatus(state presentationState) string {
|
||||
|
||||
@@ -55,26 +55,34 @@ func TestCompleteModelProfileEditorSupportsCursorEditingDeletionAndPaste(t *test
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteModelProfileEditorHorizontallyScrollsLongSecretURLWithoutDisclosure(t *testing.T) {
|
||||
func TestCompleteModelProfileEditorMasksEntireUserinfoWhilePreservingScrollAndSavedValue(t *testing.T) {
|
||||
module := &profileManagementModule{}
|
||||
model := openNewProfileEditor(t, module)
|
||||
model = updateRunes(model, "office")
|
||||
model = updateKey(model, "tab")
|
||||
const username = "operator-private"
|
||||
const secret = "credential-never-rendered"
|
||||
const raw = "https://operator:" + secret + "@this-is-a-very-long-proxy-hostname-for-horizontal-scroll.example:8443"
|
||||
const raw = "https://" + username + ":" + secret + "@this-is-a-very-long-proxy-hostname-for-horizontal-scroll.example:8443"
|
||||
model = updateRunes(model, raw)
|
||||
|
||||
end := model.View()
|
||||
assertUsableFrame(t, end, 80, 24, "[focused]", "scroll.example:8443")
|
||||
if strings.Contains(end, secret) || strings.Contains(end, "https://operator") {
|
||||
t.Fatalf("end-scrolled input disclosed credentials or failed to scroll:\n%s", end)
|
||||
for _, literal := range []string{username, secret} {
|
||||
if strings.Contains(end, literal) {
|
||||
t.Fatalf("end-scrolled input disclosed userinfo %q:\n%s", literal, end)
|
||||
}
|
||||
}
|
||||
|
||||
model = editorKey(model, tea.KeyHome)
|
||||
start := model.View()
|
||||
assertUsableFrame(t, start, 80, 24, "https://operator:")
|
||||
if strings.Contains(start, secret) || strings.Contains(start, "scroll.example:8443") {
|
||||
t.Fatalf("home did not scroll to the safe beginning of the input:\n%s", start)
|
||||
assertUsableFrame(t, start, 80, 24, "https://")
|
||||
if strings.Contains(start, username) || strings.Contains(start, secret) || strings.Contains(start, "scroll.example:8443") {
|
||||
t.Fatalf("home did not scroll to the masked beginning of the input:\n%s", start)
|
||||
}
|
||||
|
||||
model = updateKey(model, "ctrl+s")
|
||||
if len(module.intents) != 1 || module.intents[0].Profile.SingleURL != raw {
|
||||
t.Fatalf("editable masking changed saved Profile semantics: %#v", module.intents)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+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
|
||||
|
||||
@@ -105,6 +105,49 @@ func TestCompleteModelProtocolProbeNeverRunsWithoutExplicitInvocation(t *testing
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteModelProtocolProbeCompletionPreservesAnotherEditedFieldsFocusCursorAndViewport(t *testing.T) {
|
||||
module := &recordingModule{}
|
||||
model := openProtocolProbeEditor(t, module, func(context.Context, string) (probe.Capabilities, error) {
|
||||
return probe.Capabilities{HTTP: true}, nil
|
||||
}, "proxy.example:8080")
|
||||
model, command := startProtocolProbe(t, model)
|
||||
|
||||
model = updateKey(model, "shift+tab")
|
||||
model = editorKey(model, tea.KeyHome)
|
||||
model = updateRunes(model, "a-very-long-prefix-that-forces-the-name-field-to-scroll-horizontally-")
|
||||
model = editorKey(model, tea.KeyLeft)
|
||||
before := model.View()
|
||||
assertUsableFrame(t, before, 80, 24, "> Name [focused]", "Protocol Probe: checking proxy.example:8080")
|
||||
|
||||
model = finishProtocolProbe(t, model, command)
|
||||
after := model.View()
|
||||
assertUsableFrame(t, after, 80, 24, "> Name [focused]", "Protocol Probe: detected HTTP proxy at proxy.example:8080")
|
||||
if fieldLine(before, "> Name [focused]") != fieldLine(after, "> Name [focused]") {
|
||||
t.Fatalf("non-mode-changing probe completion reset the edited field viewport or cursor:\nbefore:\n%s\nafter:\n%s", before, after)
|
||||
}
|
||||
|
||||
model = updateRunes(model, "X")
|
||||
model = updateKey(model, "ctrl+s")
|
||||
if len(module.intents) != 1 {
|
||||
t.Fatalf("save after asynchronous completion dispatched %#v", module.intents)
|
||||
}
|
||||
if got, want := module.intents[0].Profile.Name, "a-very-long-prefix-that-forces-the-name-field-to-scroll-horizontallyX-office"; got != want {
|
||||
t.Fatalf("focus/cursor was not preserved: saved name = %q, want %q", got, want)
|
||||
}
|
||||
if got := module.intents[0].Profile.SingleURL; got != "http://proxy.example:8080" {
|
||||
t.Fatalf("probe did not update its affected URL input: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func fieldLine(frame, label string) string {
|
||||
for _, line := range strings.Split(frame, "\n") {
|
||||
if strings.Contains(line, label) {
|
||||
return line
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestCompleteModelProtocolProbePopulatesEstablishedCapabilityRepresentations(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -143,12 +186,12 @@ func TestCompleteModelProtocolProbePopulatesEstablishedCapabilityRepresentations
|
||||
}
|
||||
attachedField := "Single proxy URL"
|
||||
if test.caps.HTTP && test.caps.SOCKS5 {
|
||||
attachedField = "HTTP URL"
|
||||
attachedField = "Single proxy URL probe [read-only]"
|
||||
}
|
||||
field := strings.Index(view, attachedField)
|
||||
result := strings.Index(view, "Protocol Probe: detected")
|
||||
if field < 0 || result < field || strings.Count(view[field:result], "\n") > 1 {
|
||||
t.Fatalf("capability result was not attached to its populated URL field:\n%s", view)
|
||||
t.Fatalf("capability result was not attached to its initiating URL context:\n%s", view)
|
||||
}
|
||||
|
||||
model = updateKey(model, "ctrl+s")
|
||||
@@ -163,6 +206,27 @@ func TestCompleteModelProtocolProbePopulatesEstablishedCapabilityRepresentations
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteModelProtocolProbeDualResultRetainsSingleSourceAndLinksGeneratedFields(t *testing.T) {
|
||||
model := openProtocolProbeEditor(t, &recordingModule{}, func(context.Context, string) (probe.Capabilities, error) {
|
||||
return probe.Capabilities{HTTP: true, SOCKS5: true}, nil
|
||||
}, "proxy.example:7890")
|
||||
model, command := startProtocolProbe(t, model)
|
||||
model = finishProtocolProbe(t, model, command)
|
||||
view := model.View()
|
||||
assertUsableFrame(t, view, 80, 24,
|
||||
"Create Proxy Profile — advanced mode",
|
||||
"Single proxy URL probe [read-only]: proxy.example:7890",
|
||||
"Protocol Probe: detected HTTP and SOCKS5 proxy at proxy.example:7890",
|
||||
"Generated Advanced URL fields from this Single proxy URL probe:",
|
||||
"HTTP URL", "HTTPS URL", "ALL/SOCKS URL")
|
||||
if strings.Index(view, "Single proxy URL probe [read-only]") > strings.Index(view, "HTTP URL") {
|
||||
t.Fatalf("initiating Single URL context was not visibly linked before generated fields:\n%s", view)
|
||||
}
|
||||
if strings.Contains(fieldLine(view, "HTTP URL"), "Protocol Probe") {
|
||||
t.Fatalf("dual result was silently relabelled as an HTTP-field probe:\n%s", view)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteModelProtocolProbeFailureAndUnrecognizedResultsRemainEditableAndSavable(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -338,33 +402,43 @@ func TestCompleteModelProtocolProbeSurvivesResizeWithoutRestartingOrDetaching(t
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteModelProtocolProbeKeepsCredentialsOutOfDialStatusErrorsAndFrames(t *testing.T) {
|
||||
func TestCompleteModelProtocolProbeKeepsAllUserinfoOutOfDialStatusErrorsAndEveryFormFrame(t *testing.T) {
|
||||
const (
|
||||
inputUser = "alice-private"
|
||||
inputSecret = "input-secret"
|
||||
errorUser = "mallory-private"
|
||||
errorSecret = "error-secret"
|
||||
)
|
||||
var dialed []string
|
||||
model := openProtocolProbeEditor(t, &recordingModule{}, func(_ context.Context, address string) (probe.Capabilities, error) {
|
||||
module := &recordingModule{}
|
||||
model := openProtocolProbeEditor(t, module, func(_ context.Context, address string) (probe.Capabilities, error) {
|
||||
dialed = append(dialed, address)
|
||||
return probe.Capabilities{}, errors.New("failed https://mallory:" + errorSecret + "@evil.example:9999")
|
||||
}, "alice:"+inputSecret+"@proxy.example:8080")
|
||||
return probe.Capabilities{}, errors.New("failed https://" + errorUser + ":" + errorSecret + "@evil.example:9999")
|
||||
}, inputUser+":"+inputSecret+"@proxy.example:8080")
|
||||
frames := []string{model.View()}
|
||||
model, command := startProtocolProbe(t, model)
|
||||
progress := model.View()
|
||||
if strings.Contains(progress, inputSecret) {
|
||||
t.Fatalf("probe progress exposed credentials:\n%s", progress)
|
||||
}
|
||||
for _, line := range strings.Split(progress, "\n") {
|
||||
if strings.Contains(line, "Protocol Probe") && strings.Contains(line, "alice") {
|
||||
t.Fatalf("probe-generated progress exposed userinfo:\n%s", progress)
|
||||
}
|
||||
}
|
||||
frames = append(frames, model.View())
|
||||
model = finishProtocolProbe(t, model, command)
|
||||
view := model.View()
|
||||
frames = append(frames, model.View())
|
||||
model = resize(model, 60, 16)
|
||||
frames = append(frames, model.View())
|
||||
model = resize(model, 80, 24)
|
||||
frames = append(frames, model.View())
|
||||
|
||||
if len(dialed) != 1 || dialed[0] != "proxy.example:8080" {
|
||||
t.Fatalf("probe dial evidence = %#v, want credential-free endpoint", dialed)
|
||||
}
|
||||
if strings.Contains(view, inputSecret) || strings.Contains(view, errorSecret) || strings.Contains(view, "mallory") {
|
||||
t.Fatalf("probe frame exposed input or lower-layer credentials:\n%s", view)
|
||||
for i, frame := range frames {
|
||||
for _, literal := range []string{inputUser, inputSecret, errorUser, errorSecret} {
|
||||
if strings.Contains(frame, literal) {
|
||||
t.Fatalf("form frame %d exposed userinfo literal %q:\n%s", i, literal, frame)
|
||||
}
|
||||
}
|
||||
}
|
||||
assertUsableFrame(t, frames[len(frames)-1], 80, 24, "Protocol Probe failed for proxy.example:8080")
|
||||
|
||||
model = updateKey(model, "ctrl+s")
|
||||
if len(module.intents) != 1 || module.intents[0].Profile.SingleURL != "http://"+inputUser+":"+inputSecret+"@proxy.example:8080" {
|
||||
t.Fatalf("userinfo masking weakened saved Profile semantics: %#v", module.intents)
|
||||
}
|
||||
assertUsableFrame(t, view, 80, 24, "Protocol Probe failed for proxy.example:8080")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user