Skip to content

Commit

Permalink
Revamp config
Browse files Browse the repository at this point in the history
  • Loading branch information
fortuna committed Dec 24, 2024
1 parent f63d2b9 commit 8df547f
Show file tree
Hide file tree
Showing 13 changed files with 296 additions and 291 deletions.
43 changes: 14 additions & 29 deletions client/go/outline/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,27 @@ import (
"github.com/Jigsaw-Code/outline-sdk/transport"
)

// Client provides a transparent container for [transport.StreamDialer] and [transport.PacketListener]
// Transport provides a transparent container for [transport.StreamDialer] and [transport.PacketListener]
// that is exportable (as an opaque object) via gobind.
// It's used by the connectivity test and the tun2socks handlers.
type Client struct {
type Transport struct {
*config.Dialer[transport.StreamConn]
*config.PacketListener
}

// NewClientResult represents the result of [NewClientAndReturnError].
// NewTransportResult represents the result of [NewClientAndReturnError].
//
// We use a struct instead of a tuple to preserve a strongly typed error that gobind recognizes.
type NewClientResult struct {
Client *Client
Error *platerrors.PlatformError
type NewTransportResult struct {
Transport *Transport
Error *platerrors.PlatformError
}

// NewClient creates a new Outline client from a configuration string.
func NewClient(transportConfig string) *NewClientResult {
// NewTransport creates a new Outline client from a configuration string.
func NewTransport(transportConfig string) *NewTransportResult {
transportYAML, err := config.ParseConfigYAML(transportConfig)
if err != nil {
return &NewClientResult{
return &NewTransportResult{
Error: &platerrors.PlatformError{
Code: platerrors.IllegalConfig,
Message: "config is not valid YAML",
Expand All @@ -51,33 +51,18 @@ func NewClient(transportConfig string) *NewClientResult {
}
}

providers := config.RegisterDefaultProviders(config.NewProviderContainer())

streamDialer, err := providers.StreamDialers.NewInstance(context.Background(), transportYAML)
if err != nil {
return &NewClientResult{
Error: &platerrors.PlatformError{
Code: platerrors.IllegalConfig,
Message: "failed to create TCP handler",
Details: platerrors.ErrorDetails{"handler": "tcp"},
Cause: platerrors.ToPlatformError(err),
},
}
}

packetListener, err := providers.PacketListeners.NewInstance(context.Background(), transportYAML)
transportPair, err := config.NewDefaultTransportProvider().NewInstance(context.Background(), transportYAML)
if err != nil {
return &NewClientResult{
return &NewTransportResult{
Error: &platerrors.PlatformError{
Code: platerrors.IllegalConfig,
Message: "failed to create UDP handler",
Details: platerrors.ErrorDetails{"handler": "udp"},
Message: "failed to create transport",
Cause: platerrors.ToPlatformError(err),
},
}
}

return &NewClientResult{
Client: &Client{Dialer: streamDialer, PacketListener: packetListener},
return &NewTransportResult{
Transport: &Transport{Dialer: transportPair.StreamDialer, PacketListener: transportPair.PacketListener},
}
}
18 changes: 9 additions & 9 deletions client/go/outline/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ password: SECRET`,
firstHop: "example.com:4321",
}, {
name: "Explicit endpoint",
input: `$type: ss
input: `$type: shadowsocks
endpoint:
$type: dial
address: example.com:4321
Expand All @@ -68,7 +68,7 @@ secret: SECRET`,
firstHop: "example.com:4321",
}, {
name: "Multi-hop",
input: `$type: ss
input: `$type: shadowsocks
endpoint:
$type: dial
address: exit.example.com:4321
Expand All @@ -80,10 +80,10 @@ secret: SECRET`,
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := NewClient(tt.input)
require.Nil(t, result.Error)
require.Equal(t, tt.firstHop, result.Client.Dialer.FirstHop)
require.Equal(t, tt.firstHop, result.Client.PacketListener.FirstHop)
result := NewTransport(tt.input)
require.Nil(t, result.Error, "Got %v", result.Error)
require.Equal(t, tt.firstHop, result.Transport.Dialer.FirstHop)
require.Equal(t, tt.firstHop, result.Transport.PacketListener.FirstHop)
})
}
}
Expand Down Expand Up @@ -140,9 +140,9 @@ func Test_NewClientFromJSON_Errors(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NewClient(tt.input)
if got.Error == nil || got.Client != nil {
t.Errorf("NewClientFromJSON() expects an error, got = %v", got.Client)
got := NewTransport(tt.input)
if got.Error == nil || got.Transport != nil {
t.Errorf("NewClientFromJSON() expects an error, got = %v", got.Transport)
return
}
})
Expand Down
73 changes: 73 additions & 0 deletions client/go/outline/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,18 @@ package config

import (
"bytes"
"context"
"fmt"

"gopkg.in/yaml.v3"
)

const ConfigTypeKey = "$type"

type ConfigNode any
type ConfigFunction func(ctx context.Context, input any) (any, error)

type ParseFunc[ObjectType any] func(ctx context.Context, input any) (ObjectType, error)

func ParseConfigYAML(configText string) (ConfigNode, error) {
var node any
Expand All @@ -47,3 +54,69 @@ func mapToAny(in map[string]any, out any) error {
decoder.KnownFields(true)
return decoder.Decode(out)
}

type TypeProvider[T any] struct {
fallbackHandler ParseFunc[T]
parsers map[string]func(context.Context, map[string]any) (T, error)
}

var _ ParseFunc[any] = (*TypeProvider[any])(nil).NewInstance

func NewTypeProvider[T any](fallbackHandler func(context.Context, any) (T, error)) *TypeProvider[T] {
return &TypeProvider[T]{
fallbackHandler: fallbackHandler,
parsers: make(map[string]func(context.Context, map[string]any) (T, error)),
}
}

func (p *TypeProvider[T]) NewInstance(ctx context.Context, input any) (T, error) {
var zero T

// Iterate while the input is a function call.
for {
inMap, ok := input.(map[string]any)
if !ok {
break
}
parserNameAny, ok := inMap[ConfigTypeKey]
if !ok {
break
}
parserName, ok := parserNameAny.(string)
if !ok {
return zero, fmt.Errorf("parser name must be a string, found \"%T\"", parserNameAny)
}
parser, ok := p.parsers[parserName]
if !ok {
return zero, fmt.Errorf("provider \"%v\" for type %T is not registered", parserName, zero)
}

// $type is embedded in the value: {$type: ..., ...}.
// Need to copy value and remove the type directive.
inputCopy := make(map[string]any, len(inMap))
for k, v := range inMap {
if k == ConfigTypeKey {
continue
}
inputCopy[k] = v
}

var err error
input, err = parser(ctx, inputCopy)
if err != nil {
return zero, fmt.Errorf("parser \"%v\" failed: %w", parserName, err)
}
}

typed, ok := input.(T)
if ok {
return typed, nil
}

// Input is an intermediate type. We need a fallback handler.
return p.fallbackHandler(ctx, input)
}

func (p *TypeProvider[T]) RegisterParser(name string, function func(context.Context, map[string]any) (T, error)) {
p.parsers[name] = function
}
48 changes: 23 additions & 25 deletions client/go/outline/config/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,37 +24,35 @@ import (

type DialEndpointConfig struct {
Address string
Dialer ConfigNode
Dialer any
}

func registerDirectDialEndpoint[ConnType any](r TypeRegistry[*Endpoint[ConnType]], typeID string, newDialer BuildFunc[*Dialer[ConnType]]) {
r.RegisterType(typeID, func(ctx context.Context, config ConfigNode) (*Endpoint[ConnType], error) {
if config == nil {
return nil, errors.New("endpoint config cannot be nil")
}
func newDirectDialerEndpoint[ConnType any](ctx context.Context, config any, newDialer ParseFunc[*Dialer[ConnType]]) (*Endpoint[ConnType], error) {
if config == nil {
return nil, errors.New("endpoint config cannot be nil")
}

dialParams, err := parseEndpointConfig(config)
if err != nil {
return nil, err
}
dialParams, err := parseEndpointConfig(config)
if err != nil {
return nil, err
}

dialer, err := newDialer(ctx, dialParams.Dialer)
if err != nil {
return nil, fmt.Errorf("failed to create sub-dialer: %w", err)
}
dialer, err := newDialer(ctx, dialParams.Dialer)
if err != nil {
return nil, fmt.Errorf("failed to create sub-dialer: %w", err)
}

endpoint := &Endpoint[ConnType]{
Connect: func(ctx context.Context) (ConnType, error) {
return dialer.Dial(ctx, dialParams.Address)
endpoint := &Endpoint[ConnType]{
Connect: func(ctx context.Context) (ConnType, error) {
return dialer.Dial(ctx, dialParams.Address)

},
ConnectionProviderInfo: dialer.ConnectionProviderInfo,
}
if dialer.ConnType == ConnTypeDirect {
endpoint.ConnectionProviderInfo.FirstHop = dialParams.Address
}
return endpoint, nil
})
},
ConnectionProviderInfo: dialer.ConnectionProviderInfo,
}
if dialer.ConnType == ConnTypeDirect {
endpoint.ConnectionProviderInfo.FirstHop = dialParams.Address
}
return endpoint, nil
}

func parseEndpointConfig(node ConfigNode) (*DialEndpointConfig, error) {
Expand Down
Loading

0 comments on commit 8df547f

Please sign in to comment.