mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-24 15:49:06 +02:00
220 lines
7.0 KiB
Go
220 lines
7.0 KiB
Go
package proxycmd
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
rpproxy "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
|
|
"github.com/netbirdio/netbird/management/server/store"
|
|
)
|
|
|
|
func newTestStore(t *testing.T) store.Store {
|
|
t.Helper()
|
|
|
|
s, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
|
|
require.NoError(t, err)
|
|
t.Cleanup(cleanup)
|
|
|
|
return s
|
|
}
|
|
|
|
func seedProxies(t *testing.T, ctx context.Context, s store.Store) {
|
|
t.Helper()
|
|
|
|
accountID := "account-1"
|
|
alreadyDisconnectedAt := time.Now().Add(-time.Hour)
|
|
seed := []*rpproxy.Proxy{
|
|
{
|
|
ID: "proxy-1",
|
|
SessionID: "session-1",
|
|
ClusterAddress: "cluster-a.example.com",
|
|
IPAddress: "10.0.0.1",
|
|
Version: "0.60.0",
|
|
LastSeen: time.Now(),
|
|
Status: rpproxy.StatusConnected,
|
|
},
|
|
{
|
|
ID: "proxy-2",
|
|
SessionID: "session-2",
|
|
ClusterAddress: "cluster-b.example.com",
|
|
IPAddress: "10.0.0.2",
|
|
AccountID: &accountID,
|
|
LastSeen: time.Now(),
|
|
Status: rpproxy.StatusConnected,
|
|
},
|
|
{
|
|
ID: "proxy-3",
|
|
SessionID: "session-3",
|
|
ClusterAddress: "cluster-a.example.com",
|
|
IPAddress: "10.0.0.3",
|
|
LastSeen: time.Now().Add(-time.Hour),
|
|
Status: rpproxy.StatusDisconnected,
|
|
DisconnectedAt: &alreadyDisconnectedAt,
|
|
},
|
|
}
|
|
for _, p := range seed {
|
|
require.NoError(t, s.SaveProxy(ctx, p))
|
|
}
|
|
}
|
|
|
|
func proxiesByID(t *testing.T, ctx context.Context, s store.Store) map[string]*rpproxy.Proxy {
|
|
t.Helper()
|
|
|
|
proxies, err := s.GetAllProxies(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, proxies, 3)
|
|
|
|
byID := make(map[string]*rpproxy.Proxy, len(proxies))
|
|
for _, p := range proxies {
|
|
byID[p.ID] = p
|
|
}
|
|
return byID
|
|
}
|
|
|
|
func TestRunDisconnectAllWithConfirmation(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
seedProxies(t, ctx, s)
|
|
|
|
var out bytes.Buffer
|
|
require.NoError(t, runDisconnectAll(ctx, s, &out, strings.NewReader(disconnectAllConfirmation+"\n"), false, false))
|
|
|
|
output := out.String()
|
|
require.Contains(t, output, "proxy-1")
|
|
require.Contains(t, output, "proxy-2")
|
|
require.Contains(t, output, "proxy-3")
|
|
require.Contains(t, output, "cluster-a.example.com")
|
|
require.Contains(t, output, "0.60.0")
|
|
require.Contains(t, output, "account-1")
|
|
require.Contains(t, output, "Type \"disconnect all proxies\" to continue")
|
|
require.Contains(t, output, "Force-marked 2 of 3 reverse proxy instance(s) as disconnected.")
|
|
|
|
for _, p := range proxiesByID(t, ctx, s) {
|
|
require.Equal(t, rpproxy.StatusDisconnected, p.Status, "proxy %s should be disconnected", p.ID)
|
|
require.NotNil(t, p.DisconnectedAt, "proxy %s should have a disconnected timestamp", p.ID)
|
|
}
|
|
}
|
|
|
|
func TestRunDisconnectAllForceSkipsConfirmation(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
seedProxies(t, ctx, s)
|
|
|
|
var out bytes.Buffer
|
|
require.NoError(t, runDisconnectAll(ctx, s, &out, strings.NewReader(""), false, true))
|
|
|
|
output := out.String()
|
|
require.NotContains(t, output, "Type \"disconnect all proxies\" to continue")
|
|
require.Contains(t, output, "Force-marked 2 of 3 reverse proxy instance(s) as disconnected.")
|
|
}
|
|
|
|
func TestRunDisconnectAllAbortLeavesProxiesUnchanged(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
seedProxies(t, ctx, s)
|
|
|
|
var out bytes.Buffer
|
|
require.NoError(t, runDisconnectAll(ctx, s, &out, strings.NewReader("no\n"), false, false))
|
|
|
|
output := out.String()
|
|
require.Contains(t, output, "Type \"disconnect all proxies\" to continue")
|
|
require.Contains(t, output, "Aborted. No reverse proxy instances were changed.")
|
|
|
|
byID := proxiesByID(t, ctx, s)
|
|
require.Equal(t, rpproxy.StatusConnected, byID["proxy-1"].Status)
|
|
require.Equal(t, rpproxy.StatusConnected, byID["proxy-2"].Status)
|
|
require.Equal(t, rpproxy.StatusDisconnected, byID["proxy-3"].Status)
|
|
}
|
|
|
|
func TestRunDisconnectAllDryRunLeavesProxiesUnchanged(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
seedProxies(t, ctx, s)
|
|
|
|
var out bytes.Buffer
|
|
require.NoError(t, runDisconnectAll(ctx, s, &out, strings.NewReader(""), true, false))
|
|
|
|
output := out.String()
|
|
require.Contains(t, output, "Dry run: would force-mark 2 of 3 reverse proxy instance(s) as disconnected.")
|
|
require.NotContains(t, output, "Type \"disconnect all proxies\" to continue")
|
|
|
|
byID := proxiesByID(t, ctx, s)
|
|
require.Equal(t, rpproxy.StatusConnected, byID["proxy-1"].Status)
|
|
require.Equal(t, rpproxy.StatusConnected, byID["proxy-2"].Status)
|
|
require.Equal(t, rpproxy.StatusDisconnected, byID["proxy-3"].Status)
|
|
}
|
|
|
|
func TestNewCommandsDisconnectAllDryRun(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
seedProxies(t, ctx, s)
|
|
|
|
opened := false
|
|
cmd := NewCommands(func(cmd *cobra.Command, fn func(ctx context.Context, s store.Store) error) error {
|
|
opened = true
|
|
return fn(cmd.Context(), s)
|
|
})
|
|
|
|
var out bytes.Buffer
|
|
cmd.SetOut(&out)
|
|
cmd.SetErr(&out)
|
|
cmd.SetIn(strings.NewReader(""))
|
|
cmd.SetArgs([]string{"disconnect-all", "--dry-run"})
|
|
|
|
require.NoError(t, cmd.ExecuteContext(ctx))
|
|
require.True(t, opened)
|
|
require.Contains(t, out.String(), "Dry run: would force-mark 2 of 3 reverse proxy instance(s) as disconnected.")
|
|
}
|
|
|
|
func TestRunDisconnectAllEmpty(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
|
|
var out bytes.Buffer
|
|
require.NoError(t, runDisconnectAll(ctx, s, &out, strings.NewReader(""), false, false))
|
|
require.Contains(t, out.String(), "No reverse proxy instances found.")
|
|
}
|
|
|
|
func TestRunDisconnectAllEscapesProxyReportedFields(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
|
|
// A proxy reports its own id and version on connect, so both reach this
|
|
// listing unvalidated. Carriage returns, tabs and ANSI escapes would let
|
|
// a malicious proxy redraw the table or forge a row on the operator's
|
|
// terminal; U+202E would reverse the rendering of the rest of the line.
|
|
require.NoError(t, s.SaveProxy(ctx, &rpproxy.Proxy{
|
|
ID: "proxy-\r\x1b[2Kevil",
|
|
SessionID: "session-1",
|
|
ClusterAddress: "cluster-a.example.com",
|
|
IPAddress: "10.0.0.1",
|
|
Version: "0.60.0\tfake\rcolumn\u202e",
|
|
LastSeen: time.Now(),
|
|
Status: rpproxy.StatusConnected,
|
|
}))
|
|
|
|
var out bytes.Buffer
|
|
require.NoError(t, runDisconnectAll(ctx, s, &out, strings.NewReader(disconnectAllConfirmation+"\n"), true, false))
|
|
|
|
output := out.String()
|
|
for _, forbidden := range []string{"\r", "\x1b", "\u202e"} {
|
|
require.NotContains(t, output, forbidden, "listing must not carry proxy-reported control characters")
|
|
}
|
|
// The table has one data row; a smuggled tab would add a phantom column.
|
|
var dataRow string
|
|
for _, line := range strings.Split(output, "\n") {
|
|
if strings.Contains(line, "evil") {
|
|
dataRow = line
|
|
}
|
|
}
|
|
require.NotEmpty(t, dataRow, "listing should still show the proxy row")
|
|
require.NotContains(t, dataRow, "\t", "tabwriter output should not carry a smuggled column separator")
|
|
require.Contains(t, dataRow, "0.60.0", "the printable part of the version should survive")
|
|
}
|