add tokenSource support for auth configuration (#4865)

This commit is contained in:
fatedier
2025-07-03 13:17:21 +08:00
committed by GitHub
parent 61330d4d79
commit f9065a6a78
20 changed files with 832 additions and 23 deletions

View File

@ -612,6 +612,21 @@ When specifying `auth.method = "token"` in `frpc.toml` and `frps.toml` - token b
Make sure to specify the same `auth.token` in `frps.toml` and `frpc.toml` for frpc to pass frps validation
##### Token Source
frp supports reading authentication tokens from external sources using the `tokenSource` configuration. Currently, file-based token source is supported.
**File-based token source:**
```toml
# frpc.toml
auth.method = "token"
auth.tokenSource.type = "file"
auth.tokenSource.file.path = "/path/to/token/file"
```
The token will be read from the specified file at startup. This is useful for scenarios where tokens are managed by external systems or need to be kept separate from configuration files for security reasons.
#### OIDC Authentication
When specifying `auth.method = "oidc"` in `frpc.toml` and `frps.toml` - OIDC based authentication will be used.

View File

@ -1,4 +1,3 @@
## Features
* Support for YAML merge functionality (anchors and references with dot-prefixed fields) in strict configuration mode without requiring `--strict-config=false` parameter.
* Support for proxy protocol in UDP proxies to preserve real client IP addresses.
* Support tokenSource for loading authentication tokens from files

View File

@ -88,13 +88,16 @@ type ServiceOptions struct {
}
// setServiceOptionsDefault sets the default values for ServiceOptions.
func setServiceOptionsDefault(options *ServiceOptions) {
func setServiceOptionsDefault(options *ServiceOptions) error {
if options.Common != nil {
options.Common.Complete()
if err := options.Common.Complete(); err != nil {
return err
}
}
if options.ConnectorCreator == nil {
options.ConnectorCreator = NewConnector
}
return nil
}
// Service is the client service that connects to frps and provides proxy services.
@ -134,7 +137,9 @@ type Service struct {
}
func NewService(options ServiceOptions) (*Service, error) {
setServiceOptionsDefault(&options)
if err := setServiceOptionsDefault(&options); err != nil {
return nil, err
}
var webServer *httppkg.Server
if options.Common.WebServer.Port > 0 {

View File

@ -51,7 +51,10 @@ var natholeDiscoveryCmd = &cobra.Command{
cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
if err != nil {
cfg = &v1.ClientCommonConfig{}
cfg.Complete()
if err := cfg.Complete(); err != nil {
fmt.Printf("failed to complete config: %v\n", err)
os.Exit(1)
}
}
if natHoleSTUNServer != "" {
cfg.NatHoleSTUNServer = natHoleSTUNServer

View File

@ -73,7 +73,10 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm
Use: name,
Short: fmt.Sprintf("Run frpc with a single %s proxy", name),
Run: func(cmd *cobra.Command, args []string) {
clientCfg.Complete()
if err := clientCfg.Complete(); err != nil {
fmt.Println(err)
os.Exit(1)
}
if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil {
fmt.Println(err)
os.Exit(1)
@ -99,7 +102,10 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client
Use: "visitor",
Short: fmt.Sprintf("Run frpc with a single %s visitor", name),
Run: func(cmd *cobra.Command, args []string) {
clientCfg.Complete()
if err := clientCfg.Complete(); err != nil {
fmt.Println(err)
os.Exit(1)
}
if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil {
fmt.Println(err)
os.Exit(1)

View File

@ -70,7 +70,10 @@ var rootCmd = &cobra.Command{
"please use yaml/json/toml format instead!\n")
}
} else {
serverCfg.Complete()
if err := serverCfg.Complete(); err != nil {
fmt.Printf("failed to complete server config: %v\n", err)
os.Exit(1)
}
svrCfg = &serverCfg
}

View File

@ -32,6 +32,11 @@ auth.method = "token"
# auth token
auth.token = "12345678"
# alternatively, you can use tokenSource to load the token from a file
# this is mutually exclusive with auth.token
# auth.tokenSource.type = "file"
# auth.tokenSource.file.path = "/etc/frp/token"
# oidc.clientID specifies the client ID to use to get a token in OIDC authentication.
# auth.oidc.clientID = ""
# oidc.clientSecret specifies the client secret to use to get a token in OIDC authentication.

View File

@ -105,6 +105,11 @@ auth.method = "token"
# auth token
auth.token = "12345678"
# alternatively, you can use tokenSource to load the token from a file
# this is mutually exclusive with auth.token
# auth.tokenSource.type = "file"
# auth.tokenSource.file.path = "/etc/frp/token"
# oidc issuer specifies the issuer to verify OIDC tokens with.
auth.oidc.issuer = ""
# oidc audience specifies the audience OIDC tokens should contain when validated.

View File

@ -212,7 +212,9 @@ func LoadServerConfig(path string, strict bool) (*v1.ServerConfig, bool, error)
}
}
if svrCfg != nil {
svrCfg.Complete()
if err := svrCfg.Complete(); err != nil {
return nil, isLegacyFormat, err
}
}
return svrCfg, isLegacyFormat, nil
}
@ -280,7 +282,9 @@ func LoadClientConfig(path string, strict bool) (
}
if cliCfg != nil {
cliCfg.Complete()
if err := cliCfg.Complete(); err != nil {
return nil, nil, nil, isLegacyFormat, err
}
}
for _, c := range proxyCfgs {
c.Complete(cliCfg.User)

View File

@ -15,6 +15,8 @@
package v1
import (
"context"
"fmt"
"os"
"github.com/samber/lo"
@ -77,18 +79,21 @@ type ClientCommonConfig struct {
IncludeConfigFiles []string `json:"includes,omitempty"`
}
func (c *ClientCommonConfig) Complete() {
func (c *ClientCommonConfig) Complete() error {
c.ServerAddr = util.EmptyOr(c.ServerAddr, "0.0.0.0")
c.ServerPort = util.EmptyOr(c.ServerPort, 7000)
c.LoginFailExit = util.EmptyOr(c.LoginFailExit, lo.ToPtr(true))
c.NatHoleSTUNServer = util.EmptyOr(c.NatHoleSTUNServer, "stun.easyvoip.com:3478")
c.Auth.Complete()
if err := c.Auth.Complete(); err != nil {
return err
}
c.Log.Complete()
c.Transport.Complete()
c.WebServer.Complete()
c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500)
return nil
}
type ClientTransportConfig struct {
@ -184,12 +189,27 @@ type AuthClientConfig struct {
// Token specifies the authorization token used to create keys to be sent
// to the server. The server must have a matching token for authorization
// to succeed. By default, this value is "".
Token string `json:"token,omitempty"`
OIDC AuthOIDCClientConfig `json:"oidc,omitempty"`
Token string `json:"token,omitempty"`
// TokenSource specifies a dynamic source for the authorization token.
// This is mutually exclusive with Token field.
TokenSource *ValueSource `json:"tokenSource,omitempty"`
OIDC AuthOIDCClientConfig `json:"oidc,omitempty"`
}
func (c *AuthClientConfig) Complete() {
func (c *AuthClientConfig) Complete() error {
c.Method = util.EmptyOr(c.Method, "token")
// Resolve tokenSource during configuration loading
if c.Method == AuthMethodToken && c.TokenSource != nil {
token, err := c.TokenSource.Resolve(context.Background())
if err != nil {
return fmt.Errorf("failed to resolve auth.tokenSource: %w", err)
}
// Move the resolved token to the Token field and clear TokenSource
c.Token = token
c.TokenSource = nil
}
return nil
}
type AuthOIDCClientConfig struct {

View File

@ -15,6 +15,8 @@
package v1
import (
"os"
"path/filepath"
"testing"
"github.com/samber/lo"
@ -24,7 +26,8 @@ import (
func TestClientConfigComplete(t *testing.T) {
require := require.New(t)
c := &ClientConfig{}
c.Complete()
err := c.Complete()
require.NoError(err)
require.EqualValues("token", c.Auth.Method)
require.Equal(true, lo.FromPtr(c.Transport.TCPMux))
@ -33,3 +36,70 @@ func TestClientConfigComplete(t *testing.T) {
require.Equal(true, lo.FromPtr(c.Transport.TLS.DisableCustomTLSFirstByte))
require.NotEmpty(c.NatHoleSTUNServer)
}
func TestAuthClientConfig_Complete(t *testing.T) {
// Create a temporary file for testing
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test_token")
testContent := "client-token-value"
err := os.WriteFile(testFile, []byte(testContent), 0o600)
require.NoError(t, err)
tests := []struct {
name string
config AuthClientConfig
expectToken string
expectPanic bool
}{
{
name: "tokenSource resolved to token",
config: AuthClientConfig{
Method: AuthMethodToken,
TokenSource: &ValueSource{
Type: "file",
File: &FileSource{
Path: testFile,
},
},
},
expectToken: testContent,
expectPanic: false,
},
{
name: "direct token unchanged",
config: AuthClientConfig{
Method: AuthMethodToken,
Token: "direct-token",
},
expectToken: "direct-token",
expectPanic: false,
},
{
name: "invalid tokenSource should panic",
config: AuthClientConfig{
Method: AuthMethodToken,
TokenSource: &ValueSource{
Type: "file",
File: &FileSource{
Path: "/non/existent/file",
},
},
},
expectPanic: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.expectPanic {
err := tt.config.Complete()
require.Error(t, err)
} else {
err := tt.config.Complete()
require.NoError(t, err)
require.Equal(t, tt.expectToken, tt.config.Token)
require.Nil(t, tt.config.TokenSource, "TokenSource should be cleared after resolution")
}
})
}
}

View File

@ -15,6 +15,9 @@
package v1
import (
"context"
"fmt"
"github.com/samber/lo"
"github.com/fatedier/frp/pkg/config/types"
@ -98,8 +101,10 @@ type ServerConfig struct {
HTTPPlugins []HTTPPluginOptions `json:"httpPlugins,omitempty"`
}
func (c *ServerConfig) Complete() {
c.Auth.Complete()
func (c *ServerConfig) Complete() error {
if err := c.Auth.Complete(); err != nil {
return err
}
c.Log.Complete()
c.Transport.Complete()
c.WebServer.Complete()
@ -120,17 +125,31 @@ func (c *ServerConfig) Complete() {
c.UserConnTimeout = util.EmptyOr(c.UserConnTimeout, 10)
c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500)
c.NatHoleAnalysisDataReserveHours = util.EmptyOr(c.NatHoleAnalysisDataReserveHours, 7*24)
return nil
}
type AuthServerConfig struct {
Method AuthMethod `json:"method,omitempty"`
AdditionalScopes []AuthScope `json:"additionalScopes,omitempty"`
Token string `json:"token,omitempty"`
TokenSource *ValueSource `json:"tokenSource,omitempty"`
OIDC AuthOIDCServerConfig `json:"oidc,omitempty"`
}
func (c *AuthServerConfig) Complete() {
func (c *AuthServerConfig) Complete() error {
c.Method = util.EmptyOr(c.Method, "token")
// Resolve tokenSource during configuration loading
if c.Method == AuthMethodToken && c.TokenSource != nil {
token, err := c.TokenSource.Resolve(context.Background())
if err != nil {
return fmt.Errorf("failed to resolve auth.tokenSource: %w", err)
}
// Move the resolved token to the Token field and clear TokenSource
c.Token = token
c.TokenSource = nil
}
return nil
}
type AuthOIDCServerConfig struct {

View File

@ -15,6 +15,8 @@
package v1
import (
"os"
"path/filepath"
"testing"
"github.com/samber/lo"
@ -24,9 +26,77 @@ import (
func TestServerConfigComplete(t *testing.T) {
require := require.New(t)
c := &ServerConfig{}
c.Complete()
err := c.Complete()
require.NoError(err)
require.EqualValues("token", c.Auth.Method)
require.Equal(true, lo.FromPtr(c.Transport.TCPMux))
require.Equal(true, lo.FromPtr(c.DetailedErrorsToClient))
}
func TestAuthServerConfig_Complete(t *testing.T) {
// Create a temporary file for testing
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test_token")
testContent := "file-token-value"
err := os.WriteFile(testFile, []byte(testContent), 0o600)
require.NoError(t, err)
tests := []struct {
name string
config AuthServerConfig
expectToken string
expectPanic bool
}{
{
name: "tokenSource resolved to token",
config: AuthServerConfig{
Method: AuthMethodToken,
TokenSource: &ValueSource{
Type: "file",
File: &FileSource{
Path: testFile,
},
},
},
expectToken: testContent,
expectPanic: false,
},
{
name: "direct token unchanged",
config: AuthServerConfig{
Method: AuthMethodToken,
Token: "direct-token",
},
expectToken: "direct-token",
expectPanic: false,
},
{
name: "invalid tokenSource should panic",
config: AuthServerConfig{
Method: AuthMethodToken,
TokenSource: &ValueSource{
Type: "file",
File: &FileSource{
Path: "/non/existent/file",
},
},
},
expectPanic: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.expectPanic {
err := tt.config.Complete()
require.Error(t, err)
} else {
err := tt.config.Complete()
require.NoError(t, err)
require.Equal(t, tt.expectToken, tt.config.Token)
require.Nil(t, tt.config.TokenSource, "TokenSource should be cleared after resolution")
}
})
}
}

View File

@ -45,6 +45,18 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes))
}
// Validate token/tokenSource mutual exclusivity
if c.Auth.Token != "" && c.Auth.TokenSource != nil {
errs = AppendError(errs, fmt.Errorf("cannot specify both auth.token and auth.tokenSource"))
}
// Validate tokenSource if specified
if c.Auth.TokenSource != nil {
if err := c.Auth.TokenSource.Validate(); err != nil {
errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err))
}
}
if err := validateLogConfig(&c.Log); err != nil {
errs = AppendError(errs, err)
}

View File

@ -35,6 +35,18 @@ func ValidateServerConfig(c *v1.ServerConfig) (Warning, error) {
errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes))
}
// Validate token/tokenSource mutual exclusivity
if c.Auth.Token != "" && c.Auth.TokenSource != nil {
errs = AppendError(errs, fmt.Errorf("cannot specify both auth.token and auth.tokenSource"))
}
// Validate tokenSource if specified
if c.Auth.TokenSource != nil {
if err := c.Auth.TokenSource.Validate(); err != nil {
errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err))
}
}
if err := validateLogConfig(&c.Log); err != nil {
errs = AppendError(errs, err)
}

View File

@ -0,0 +1,93 @@
// Copyright 2025 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package v1
import (
"context"
"errors"
"fmt"
"os"
"strings"
)
// ValueSource provides a way to dynamically resolve configuration values
// from various sources like files, environment variables, or external services.
type ValueSource struct {
Type string `json:"type"`
File *FileSource `json:"file,omitempty"`
}
// FileSource specifies how to load a value from a file.
type FileSource struct {
Path string `json:"path"`
}
// Validate validates the ValueSource configuration.
func (v *ValueSource) Validate() error {
if v == nil {
return errors.New("valueSource cannot be nil")
}
switch v.Type {
case "file":
if v.File == nil {
return errors.New("file configuration is required when type is 'file'")
}
return v.File.Validate()
default:
return fmt.Errorf("unsupported value source type: %s (only 'file' is supported)", v.Type)
}
}
// Resolve resolves the value from the configured source.
func (v *ValueSource) Resolve(ctx context.Context) (string, error) {
if err := v.Validate(); err != nil {
return "", err
}
switch v.Type {
case "file":
return v.File.Resolve(ctx)
default:
return "", fmt.Errorf("unsupported value source type: %s", v.Type)
}
}
// Validate validates the FileSource configuration.
func (f *FileSource) Validate() error {
if f == nil {
return errors.New("fileSource cannot be nil")
}
if f.Path == "" {
return errors.New("file path cannot be empty")
}
return nil
}
// Resolve reads and returns the content from the specified file.
func (f *FileSource) Resolve(_ context.Context) (string, error) {
if err := f.Validate(); err != nil {
return "", err
}
content, err := os.ReadFile(f.Path)
if err != nil {
return "", fmt.Errorf("failed to read file %s: %v", f.Path, err)
}
// Trim whitespace, which is important for file-based tokens
return strings.TrimSpace(string(content)), nil
}

View File

@ -0,0 +1,246 @@
// Copyright 2025 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package v1
import (
"context"
"os"
"path/filepath"
"testing"
)
func TestValueSource_Validate(t *testing.T) {
tests := []struct {
name string
vs *ValueSource
wantErr bool
}{
{
name: "nil valueSource",
vs: nil,
wantErr: true,
},
{
name: "unsupported type",
vs: &ValueSource{
Type: "unsupported",
},
wantErr: true,
},
{
name: "file type without file config",
vs: &ValueSource{
Type: "file",
File: nil,
},
wantErr: true,
},
{
name: "valid file type with absolute path",
vs: &ValueSource{
Type: "file",
File: &FileSource{
Path: "/tmp/test",
},
},
wantErr: false,
},
{
name: "valid file type with relative path",
vs: &ValueSource{
Type: "file",
File: &FileSource{
Path: "configs/token",
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.vs.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("ValueSource.Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestFileSource_Validate(t *testing.T) {
tests := []struct {
name string
fs *FileSource
wantErr bool
}{
{
name: "nil fileSource",
fs: nil,
wantErr: true,
},
{
name: "empty path",
fs: &FileSource{
Path: "",
},
wantErr: true,
},
{
name: "relative path (allowed)",
fs: &FileSource{
Path: "relative/path",
},
wantErr: false,
},
{
name: "absolute path",
fs: &FileSource{
Path: "/absolute/path",
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.fs.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("FileSource.Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestFileSource_Resolve(t *testing.T) {
// Create a temporary file for testing
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test_token")
testContent := "test-token-value\n\t "
expectedContent := "test-token-value"
err := os.WriteFile(testFile, []byte(testContent), 0o600)
if err != nil {
t.Fatalf("failed to create test file: %v", err)
}
tests := []struct {
name string
fs *FileSource
want string
wantErr bool
}{
{
name: "valid file path",
fs: &FileSource{
Path: testFile,
},
want: expectedContent,
wantErr: false,
},
{
name: "non-existent file",
fs: &FileSource{
Path: "/non/existent/file",
},
want: "",
wantErr: true,
},
{
name: "path traversal attempt (should fail validation)",
fs: &FileSource{
Path: "../../../etc/passwd",
},
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.fs.Resolve(context.Background())
if (err != nil) != tt.wantErr {
t.Errorf("FileSource.Resolve() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("FileSource.Resolve() = %v, want %v", got, tt.want)
}
})
}
}
func TestValueSource_Resolve(t *testing.T) {
// Create a temporary file for testing
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test_token")
testContent := "test-token-value"
err := os.WriteFile(testFile, []byte(testContent), 0o600)
if err != nil {
t.Fatalf("failed to create test file: %v", err)
}
tests := []struct {
name string
vs *ValueSource
want string
wantErr bool
}{
{
name: "valid file type",
vs: &ValueSource{
Type: "file",
File: &FileSource{
Path: testFile,
},
},
want: testContent,
wantErr: false,
},
{
name: "unsupported type",
vs: &ValueSource{
Type: "unsupported",
},
want: "",
wantErr: true,
},
{
name: "file type with path traversal",
vs: &ValueSource{
Type: "file",
File: &FileSource{
Path: "../../../etc/passwd",
},
},
want: "",
wantErr: true,
},
}
ctx := context.Background()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.vs.Resolve(ctx)
if (err != nil) != tt.wantErr {
t.Errorf("ValueSource.Resolve() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("ValueSource.Resolve() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -105,7 +105,10 @@ func (s *TunnelServer) Run() error {
s.writeToClient(err.Error())
return fmt.Errorf("parse flags from ssh client error: %v", err)
}
clientCfg.Complete()
if err := clientCfg.Complete(); err != nil {
s.writeToClient(fmt.Sprintf("failed to complete client config: %v", err))
return fmt.Errorf("complete client config error: %v", err)
}
if sshConn.Permissions != nil {
clientCfg.User = util.EmptyOr(sshConn.Permissions.Extensions["user"], clientCfg.User)
}

View File

@ -37,7 +37,9 @@ type Client struct {
func NewClient(options ClientOptions) (*Client, error) {
if options.Common != nil {
options.Common.Complete()
if err := options.Common.Complete(); err != nil {
return nil, err
}
}
ln := netpkg.NewInternalListener()

View File

@ -0,0 +1,217 @@
// Copyright 2025 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package basic
import (
"fmt"
"os"
"path/filepath"
"github.com/onsi/ginkgo/v2"
"github.com/fatedier/frp/test/e2e/framework"
"github.com/fatedier/frp/test/e2e/framework/consts"
"github.com/fatedier/frp/test/e2e/pkg/port"
)
var _ = ginkgo.Describe("[Feature: TokenSource]", func() {
f := framework.NewDefaultFramework()
ginkgo.Describe("File-based token loading", func() {
ginkgo.It("should work with file tokenSource", func() {
// Create a temporary token file
tmpDir := f.TempDirectory
tokenFile := filepath.Join(tmpDir, "test_token")
tokenContent := "test-token-123"
err := os.WriteFile(tokenFile, []byte(tokenContent), 0o600)
framework.ExpectNoError(err)
serverConf := consts.DefaultServerConfig
clientConf := consts.DefaultClientConfig
portName := port.GenName("TCP")
// Server config with tokenSource
serverConf += fmt.Sprintf(`
auth.tokenSource.type = "file"
auth.tokenSource.file.path = "%s"
`, tokenFile)
// Client config with matching token
clientConf += fmt.Sprintf(`
auth.token = "%s"
[[proxies]]
name = "tcp"
type = "tcp"
localPort = {{ .%s }}
remotePort = {{ .%s }}
`, tokenContent, framework.TCPEchoServerPort, portName)
f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f).PortName(portName).Ensure()
})
ginkgo.It("should work with client tokenSource", func() {
// Create a temporary token file
tmpDir := f.TempDirectory
tokenFile := filepath.Join(tmpDir, "client_token")
tokenContent := "client-token-456"
err := os.WriteFile(tokenFile, []byte(tokenContent), 0o600)
framework.ExpectNoError(err)
serverConf := consts.DefaultServerConfig
clientConf := consts.DefaultClientConfig
portName := port.GenName("TCP")
// Server config with matching token
serverConf += fmt.Sprintf(`
auth.token = "%s"
`, tokenContent)
// Client config with tokenSource
clientConf += fmt.Sprintf(`
auth.tokenSource.type = "file"
auth.tokenSource.file.path = "%s"
[[proxies]]
name = "tcp"
type = "tcp"
localPort = {{ .%s }}
remotePort = {{ .%s }}
`, tokenFile, framework.TCPEchoServerPort, portName)
f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f).PortName(portName).Ensure()
})
ginkgo.It("should work with both server and client tokenSource", func() {
// Create temporary token files
tmpDir := f.TempDirectory
serverTokenFile := filepath.Join(tmpDir, "server_token")
clientTokenFile := filepath.Join(tmpDir, "client_token")
tokenContent := "shared-token-789"
err := os.WriteFile(serverTokenFile, []byte(tokenContent), 0o600)
framework.ExpectNoError(err)
err = os.WriteFile(clientTokenFile, []byte(tokenContent), 0o600)
framework.ExpectNoError(err)
serverConf := consts.DefaultServerConfig
clientConf := consts.DefaultClientConfig
portName := port.GenName("TCP")
// Server config with tokenSource
serverConf += fmt.Sprintf(`
auth.tokenSource.type = "file"
auth.tokenSource.file.path = "%s"
`, serverTokenFile)
// Client config with tokenSource
clientConf += fmt.Sprintf(`
auth.tokenSource.type = "file"
auth.tokenSource.file.path = "%s"
[[proxies]]
name = "tcp"
type = "tcp"
localPort = {{ .%s }}
remotePort = {{ .%s }}
`, clientTokenFile, framework.TCPEchoServerPort, portName)
f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f).PortName(portName).Ensure()
})
ginkgo.It("should fail with mismatched tokens", func() {
// Create temporary token files with different content
tmpDir := f.TempDirectory
serverTokenFile := filepath.Join(tmpDir, "server_token")
clientTokenFile := filepath.Join(tmpDir, "client_token")
err := os.WriteFile(serverTokenFile, []byte("server-token"), 0o600)
framework.ExpectNoError(err)
err = os.WriteFile(clientTokenFile, []byte("client-token"), 0o600)
framework.ExpectNoError(err)
serverConf := consts.DefaultServerConfig
clientConf := consts.DefaultClientConfig
portName := port.GenName("TCP")
// Server config with tokenSource
serverConf += fmt.Sprintf(`
auth.tokenSource.type = "file"
auth.tokenSource.file.path = "%s"
`, serverTokenFile)
// Client config with different tokenSource
clientConf += fmt.Sprintf(`
auth.tokenSource.type = "file"
auth.tokenSource.file.path = "%s"
[[proxies]]
name = "tcp"
type = "tcp"
localPort = {{ .%s }}
remotePort = {{ .%s }}
`, clientTokenFile, framework.TCPEchoServerPort, portName)
f.RunProcesses([]string{serverConf}, []string{clientConf})
// This should fail due to token mismatch - the client should not be able to connect
// We expect the request to fail because the proxy tunnel is not established
framework.NewRequestExpect(f).PortName(portName).ExpectError(true).Ensure()
})
ginkgo.It("should fail with non-existent token file", func() {
// This test verifies that server fails to start when tokenSource points to non-existent file
// We'll verify this by checking that the configuration loading itself fails
// Create a config that references a non-existent file
tmpDir := f.TempDirectory
nonExistentFile := filepath.Join(tmpDir, "non_existent_token")
serverConf := consts.DefaultServerConfig
// Server config with non-existent tokenSource file
serverConf += fmt.Sprintf(`
auth.tokenSource.type = "file"
auth.tokenSource.file.path = "%s"
`, nonExistentFile)
// The test expectation is that this will fail during the RunProcesses call
// because the server cannot load the configuration due to missing token file
defer func() {
if r := recover(); r != nil {
// Expected: server should fail to start due to missing file
ginkgo.By(fmt.Sprintf("Server correctly failed to start: %v", r))
}
}()
// This should cause a panic or error during server startup
f.RunProcesses([]string{serverConf}, []string{})
})
})
})