Skip to content

Commit

Permalink
Merge branch 'master' into sbruens/caddy
Browse files Browse the repository at this point in the history
  • Loading branch information
sbruens committed Sep 4, 2024
2 parents 76e320e + 9992735 commit 469a18b
Show file tree
Hide file tree
Showing 2 changed files with 258 additions and 0 deletions.
91 changes: 91 additions & 0 deletions cmd/outline-ss-server/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright 2024 Jigsaw Operations LLC
//
// 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
//
// https://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 main

import (
"fmt"
"net"

"gopkg.in/yaml.v3"
)

type ServiceConfig struct {
Listeners []ListenerConfig
Keys []KeyConfig
}

type ListenerType string

const listenerTypeTCP ListenerType = "tcp"
const listenerTypeUDP ListenerType = "udp"

type ListenerConfig struct {
Type ListenerType
Address string
}

type KeyConfig struct {
ID string
Cipher string
Secret string
}

type LegacyKeyServiceConfig struct {
KeyConfig `yaml:",inline"`
Port int
}

type Config struct {
Services []ServiceConfig

// Deprecated: `keys` exists for backward compatibility. Prefer to configure
// using the newer `services` format.
Keys []LegacyKeyServiceConfig
}

// Validate checks that the config is valid.
func (c *Config) Validate() error {
existingListeners := make(map[string]bool)
for _, serviceConfig := range c.Services {
for _, lnConfig := range serviceConfig.Listeners {
// TODO: Support more listener types.
if lnConfig.Type != listenerTypeTCP && lnConfig.Type != listenerTypeUDP {
return fmt.Errorf("unsupported listener type: %s", lnConfig.Type)
}
host, _, err := net.SplitHostPort(lnConfig.Address)
if err != nil {
return fmt.Errorf("invalid listener address `%s`: %v", lnConfig.Address, err)
}
if ip := net.ParseIP(host); ip == nil {
return fmt.Errorf("address must be IP, found: %s", host)
}
key := string(lnConfig.Type) + "/" + lnConfig.Address
if _, exists := existingListeners[key]; exists {
return fmt.Errorf("listener of type %s with address %s already exists.", lnConfig.Type, lnConfig.Address)
}
existingListeners[key] = true
}
}
return nil
}

// readConfig attempts to read a config from a filename and parses it as a [Config].
func readConfig(configData []byte) (*Config, error) {
config := Config{}
if err := yaml.Unmarshal(configData, &config); err != nil {
return nil, fmt.Errorf("failed to parse config: %w", err)
}
return &config, nil
}
167 changes: 167 additions & 0 deletions cmd/outline-ss-server/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Copyright 2024 Jigsaw Operations LLC
//
// 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
//
// https://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 main

import (
"os"
"testing"

"github.com/stretchr/testify/require"
)

func TestValidateConfigFails(t *testing.T) {
tests := []struct {
name string
cfg *Config
}{
{
name: "WithUnknownListenerType",
cfg: &Config{
Services: []ServiceConfig{
ServiceConfig{
Listeners: []ListenerConfig{
ListenerConfig{Type: "foo", Address: "[::]:9000"},
},
},
},
},
},
{
name: "WithInvalidListenerAddress",
cfg: &Config{
Services: []ServiceConfig{
ServiceConfig{
Listeners: []ListenerConfig{
ListenerConfig{Type: listenerTypeTCP, Address: "tcp/[::]:9000"},
},
},
},
},
},
{
name: "WithHostnameAddress",
cfg: &Config{
Services: []ServiceConfig{
ServiceConfig{
Listeners: []ListenerConfig{
ListenerConfig{Type: listenerTypeTCP, Address: "example.com:9000"},
},
},
},
},
},
{
name: "WithDuplicateListeners",
cfg: &Config{
Services: []ServiceConfig{
ServiceConfig{
Listeners: []ListenerConfig{
ListenerConfig{Type: listenerTypeTCP, Address: "[::]:9000"},
},
},
ServiceConfig{
Listeners: []ListenerConfig{
ListenerConfig{Type: listenerTypeTCP, Address: "[::]:9000"},
},
},
},
},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := tc.cfg.Validate()
require.Error(t, err)
})
}
}

func TestReadConfig(t *testing.T) {
config, err := readConfigFile("./config_example.yml")

require.NoError(t, err)
expected := Config{
Services: []ServiceConfig{
ServiceConfig{
Listeners: []ListenerConfig{
ListenerConfig{Type: listenerTypeTCP, Address: "[::]:9000"},
ListenerConfig{Type: listenerTypeUDP, Address: "[::]:9000"},
},
Keys: []KeyConfig{
KeyConfig{"user-0", "chacha20-ietf-poly1305", "Secret0"},
KeyConfig{"user-1", "chacha20-ietf-poly1305", "Secret1"},
},
},
ServiceConfig{
Listeners: []ListenerConfig{
ListenerConfig{Type: listenerTypeTCP, Address: "[::]:9001"},
ListenerConfig{Type: listenerTypeUDP, Address: "[::]:9001"},
},
Keys: []KeyConfig{
KeyConfig{"user-2", "chacha20-ietf-poly1305", "Secret2"},
},
},
},
}
require.Equal(t, expected, *config)
}

func TestReadConfigParsesDeprecatedFormat(t *testing.T) {
config, err := readConfigFile("./config_example.deprecated.yml")

require.NoError(t, err)
expected := Config{
Keys: []LegacyKeyServiceConfig{
LegacyKeyServiceConfig{
KeyConfig: KeyConfig{ID: "user-0", Cipher: "chacha20-ietf-poly1305", Secret: "Secret0"},
Port: 9000,
},
LegacyKeyServiceConfig{
KeyConfig: KeyConfig{ID: "user-1", Cipher: "chacha20-ietf-poly1305", Secret: "Secret1"},
Port: 9000,
},
LegacyKeyServiceConfig{
KeyConfig: KeyConfig{ID: "user-2", Cipher: "chacha20-ietf-poly1305", Secret: "Secret2"},
Port: 9001,
},
},
}
require.Equal(t, expected, *config)
}

func TestReadConfigFromEmptyFile(t *testing.T) {
file, _ := os.CreateTemp("", "empty.yaml")

config, err := readConfigFile(file.Name())

require.NoError(t, err)
require.ElementsMatch(t, Config{}, config)
}

func TestReadConfigFromIncorrectFormatFails(t *testing.T) {
file, _ := os.CreateTemp("", "empty.yaml")
file.WriteString("foo")

config, err := readConfigFile(file.Name())

require.Error(t, err)
require.ElementsMatch(t, Config{}, config)
}

func readConfigFile(filename string) (*Config, error) {
configData, _ := os.ReadFile(filename)
return readConfig(configData)
}

0 comments on commit 469a18b

Please sign in to comment.