mirror of
https://github.com/fatedier/frp.git
synced 2025-07-04 14:49:31 +00:00
add tokenSource support for auth configuration (#4865)
This commit is contained in:
parent
61330d4d79
commit
f9065a6a78
15
README.md
15
README.md
@ -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
|
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
|
#### OIDC Authentication
|
||||||
|
|
||||||
When specifying `auth.method = "oidc"` in `frpc.toml` and `frps.toml` - OIDC based authentication will be used.
|
When specifying `auth.method = "oidc"` in `frpc.toml` and `frps.toml` - OIDC based authentication will be used.
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
* Support for YAML merge functionality (anchors and references with dot-prefixed fields) in strict configuration mode without requiring `--strict-config=false` parameter.
|
* Support tokenSource for loading authentication tokens from files
|
||||||
* Support for proxy protocol in UDP proxies to preserve real client IP addresses.
|
|
@ -88,13 +88,16 @@ type ServiceOptions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// setServiceOptionsDefault sets the default values for ServiceOptions.
|
// setServiceOptionsDefault sets the default values for ServiceOptions.
|
||||||
func setServiceOptionsDefault(options *ServiceOptions) {
|
func setServiceOptionsDefault(options *ServiceOptions) error {
|
||||||
if options.Common != nil {
|
if options.Common != nil {
|
||||||
options.Common.Complete()
|
if err := options.Common.Complete(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if options.ConnectorCreator == nil {
|
if options.ConnectorCreator == nil {
|
||||||
options.ConnectorCreator = NewConnector
|
options.ConnectorCreator = NewConnector
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Service is the client service that connects to frps and provides proxy services.
|
// 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) {
|
func NewService(options ServiceOptions) (*Service, error) {
|
||||||
setServiceOptionsDefault(&options)
|
if err := setServiceOptionsDefault(&options); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
var webServer *httppkg.Server
|
var webServer *httppkg.Server
|
||||||
if options.Common.WebServer.Port > 0 {
|
if options.Common.WebServer.Port > 0 {
|
||||||
|
@ -51,7 +51,10 @@ var natholeDiscoveryCmd = &cobra.Command{
|
|||||||
cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
|
cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cfg = &v1.ClientCommonConfig{}
|
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 != "" {
|
if natHoleSTUNServer != "" {
|
||||||
cfg.NatHoleSTUNServer = natHoleSTUNServer
|
cfg.NatHoleSTUNServer = natHoleSTUNServer
|
||||||
|
@ -73,7 +73,10 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm
|
|||||||
Use: name,
|
Use: name,
|
||||||
Short: fmt.Sprintf("Run frpc with a single %s proxy", name),
|
Short: fmt.Sprintf("Run frpc with a single %s proxy", name),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
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 {
|
if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@ -99,7 +102,10 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client
|
|||||||
Use: "visitor",
|
Use: "visitor",
|
||||||
Short: fmt.Sprintf("Run frpc with a single %s visitor", name),
|
Short: fmt.Sprintf("Run frpc with a single %s visitor", name),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
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 {
|
if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
@ -70,7 +70,10 @@ var rootCmd = &cobra.Command{
|
|||||||
"please use yaml/json/toml format instead!\n")
|
"please use yaml/json/toml format instead!\n")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
serverCfg.Complete()
|
if err := serverCfg.Complete(); err != nil {
|
||||||
|
fmt.Printf("failed to complete server config: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
svrCfg = &serverCfg
|
svrCfg = &serverCfg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,6 +32,11 @@ auth.method = "token"
|
|||||||
# auth token
|
# auth token
|
||||||
auth.token = "12345678"
|
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.
|
# oidc.clientID specifies the client ID to use to get a token in OIDC authentication.
|
||||||
# auth.oidc.clientID = ""
|
# auth.oidc.clientID = ""
|
||||||
# oidc.clientSecret specifies the client secret to use to get a token in OIDC authentication.
|
# oidc.clientSecret specifies the client secret to use to get a token in OIDC authentication.
|
||||||
|
@ -105,6 +105,11 @@ auth.method = "token"
|
|||||||
# auth token
|
# auth token
|
||||||
auth.token = "12345678"
|
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.
|
# oidc issuer specifies the issuer to verify OIDC tokens with.
|
||||||
auth.oidc.issuer = ""
|
auth.oidc.issuer = ""
|
||||||
# oidc audience specifies the audience OIDC tokens should contain when validated.
|
# oidc audience specifies the audience OIDC tokens should contain when validated.
|
||||||
|
@ -212,7 +212,9 @@ func LoadServerConfig(path string, strict bool) (*v1.ServerConfig, bool, error)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if svrCfg != nil {
|
if svrCfg != nil {
|
||||||
svrCfg.Complete()
|
if err := svrCfg.Complete(); err != nil {
|
||||||
|
return nil, isLegacyFormat, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return svrCfg, isLegacyFormat, nil
|
return svrCfg, isLegacyFormat, nil
|
||||||
}
|
}
|
||||||
@ -280,7 +282,9 @@ func LoadClientConfig(path string, strict bool) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cliCfg != nil {
|
if cliCfg != nil {
|
||||||
cliCfg.Complete()
|
if err := cliCfg.Complete(); err != nil {
|
||||||
|
return nil, nil, nil, isLegacyFormat, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for _, c := range proxyCfgs {
|
for _, c := range proxyCfgs {
|
||||||
c.Complete(cliCfg.User)
|
c.Complete(cliCfg.User)
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
@ -77,18 +79,21 @@ type ClientCommonConfig struct {
|
|||||||
IncludeConfigFiles []string `json:"includes,omitempty"`
|
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.ServerAddr = util.EmptyOr(c.ServerAddr, "0.0.0.0")
|
||||||
c.ServerPort = util.EmptyOr(c.ServerPort, 7000)
|
c.ServerPort = util.EmptyOr(c.ServerPort, 7000)
|
||||||
c.LoginFailExit = util.EmptyOr(c.LoginFailExit, lo.ToPtr(true))
|
c.LoginFailExit = util.EmptyOr(c.LoginFailExit, lo.ToPtr(true))
|
||||||
c.NatHoleSTUNServer = util.EmptyOr(c.NatHoleSTUNServer, "stun.easyvoip.com:3478")
|
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.Log.Complete()
|
||||||
c.Transport.Complete()
|
c.Transport.Complete()
|
||||||
c.WebServer.Complete()
|
c.WebServer.Complete()
|
||||||
|
|
||||||
c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500)
|
c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientTransportConfig struct {
|
type ClientTransportConfig struct {
|
||||||
@ -185,11 +190,26 @@ type AuthClientConfig struct {
|
|||||||
// to the server. The server must have a matching token for authorization
|
// to the server. The server must have a matching token for authorization
|
||||||
// to succeed. By default, this value is "".
|
// to succeed. By default, this value is "".
|
||||||
Token string `json:"token,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"`
|
OIDC AuthOIDCClientConfig `json:"oidc,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *AuthClientConfig) Complete() {
|
func (c *AuthClientConfig) Complete() error {
|
||||||
c.Method = util.EmptyOr(c.Method, "token")
|
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 {
|
type AuthOIDCClientConfig struct {
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
@ -24,7 +26,8 @@ import (
|
|||||||
func TestClientConfigComplete(t *testing.T) {
|
func TestClientConfigComplete(t *testing.T) {
|
||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
c := &ClientConfig{}
|
c := &ClientConfig{}
|
||||||
c.Complete()
|
err := c.Complete()
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
require.EqualValues("token", c.Auth.Method)
|
require.EqualValues("token", c.Auth.Method)
|
||||||
require.Equal(true, lo.FromPtr(c.Transport.TCPMux))
|
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.Equal(true, lo.FromPtr(c.Transport.TLS.DisableCustomTLSFirstByte))
|
||||||
require.NotEmpty(c.NatHoleSTUNServer)
|
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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -15,6 +15,9 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/config/types"
|
"github.com/fatedier/frp/pkg/config/types"
|
||||||
@ -98,8 +101,10 @@ type ServerConfig struct {
|
|||||||
HTTPPlugins []HTTPPluginOptions `json:"httpPlugins,omitempty"`
|
HTTPPlugins []HTTPPluginOptions `json:"httpPlugins,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ServerConfig) Complete() {
|
func (c *ServerConfig) Complete() error {
|
||||||
c.Auth.Complete()
|
if err := c.Auth.Complete(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
c.Log.Complete()
|
c.Log.Complete()
|
||||||
c.Transport.Complete()
|
c.Transport.Complete()
|
||||||
c.WebServer.Complete()
|
c.WebServer.Complete()
|
||||||
@ -120,17 +125,31 @@ func (c *ServerConfig) Complete() {
|
|||||||
c.UserConnTimeout = util.EmptyOr(c.UserConnTimeout, 10)
|
c.UserConnTimeout = util.EmptyOr(c.UserConnTimeout, 10)
|
||||||
c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500)
|
c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500)
|
||||||
c.NatHoleAnalysisDataReserveHours = util.EmptyOr(c.NatHoleAnalysisDataReserveHours, 7*24)
|
c.NatHoleAnalysisDataReserveHours = util.EmptyOr(c.NatHoleAnalysisDataReserveHours, 7*24)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthServerConfig struct {
|
type AuthServerConfig struct {
|
||||||
Method AuthMethod `json:"method,omitempty"`
|
Method AuthMethod `json:"method,omitempty"`
|
||||||
AdditionalScopes []AuthScope `json:"additionalScopes,omitempty"`
|
AdditionalScopes []AuthScope `json:"additionalScopes,omitempty"`
|
||||||
Token string `json:"token,omitempty"`
|
Token string `json:"token,omitempty"`
|
||||||
|
TokenSource *ValueSource `json:"tokenSource,omitempty"`
|
||||||
OIDC AuthOIDCServerConfig `json:"oidc,omitempty"`
|
OIDC AuthOIDCServerConfig `json:"oidc,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *AuthServerConfig) Complete() {
|
func (c *AuthServerConfig) Complete() error {
|
||||||
c.Method = util.EmptyOr(c.Method, "token")
|
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 {
|
type AuthOIDCServerConfig struct {
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
@ -24,9 +26,77 @@ import (
|
|||||||
func TestServerConfigComplete(t *testing.T) {
|
func TestServerConfigComplete(t *testing.T) {
|
||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
c := &ServerConfig{}
|
c := &ServerConfig{}
|
||||||
c.Complete()
|
err := c.Complete()
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
require.EqualValues("token", c.Auth.Method)
|
require.EqualValues("token", c.Auth.Method)
|
||||||
require.Equal(true, lo.FromPtr(c.Transport.TCPMux))
|
require.Equal(true, lo.FromPtr(c.Transport.TCPMux))
|
||||||
require.Equal(true, lo.FromPtr(c.DetailedErrorsToClient))
|
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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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))
|
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 {
|
if err := validateLogConfig(&c.Log); err != nil {
|
||||||
errs = AppendError(errs, err)
|
errs = AppendError(errs, err)
|
||||||
}
|
}
|
||||||
|
@ -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))
|
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 {
|
if err := validateLogConfig(&c.Log); err != nil {
|
||||||
errs = AppendError(errs, err)
|
errs = AppendError(errs, err)
|
||||||
}
|
}
|
||||||
|
93
pkg/config/v1/value_source.go
Normal file
93
pkg/config/v1/value_source.go
Normal 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
|
||||||
|
}
|
246
pkg/config/v1/value_source_test.go
Normal file
246
pkg/config/v1/value_source_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -105,7 +105,10 @@ func (s *TunnelServer) Run() error {
|
|||||||
s.writeToClient(err.Error())
|
s.writeToClient(err.Error())
|
||||||
return fmt.Errorf("parse flags from ssh client error: %v", err)
|
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 {
|
if sshConn.Permissions != nil {
|
||||||
clientCfg.User = util.EmptyOr(sshConn.Permissions.Extensions["user"], clientCfg.User)
|
clientCfg.User = util.EmptyOr(sshConn.Permissions.Extensions["user"], clientCfg.User)
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,9 @@ type Client struct {
|
|||||||
|
|
||||||
func NewClient(options ClientOptions) (*Client, error) {
|
func NewClient(options ClientOptions) (*Client, error) {
|
||||||
if options.Common != nil {
|
if options.Common != nil {
|
||||||
options.Common.Complete()
|
if err := options.Common.Complete(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ln := netpkg.NewInternalListener()
|
ln := netpkg.NewInternalListener()
|
||||||
|
217
test/e2e/v1/basic/token_source.go
Normal file
217
test/e2e/v1/basic/token_source.go
Normal 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{})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Loading…
x
Reference in New Issue
Block a user