Skip to content

Commit

Permalink
Support TLS configuration as raw bytes
Browse files Browse the repository at this point in the history
This change is intended to allow users to configure TLS settings in the
form of raw byte slices. This is particularly useful for situations
where the FIX application is running in a cloud environment (e.g. k8s)
and session configuration is loaded from a database (rather than a
static file). In this scenario it is much more convenient to be able to
load TLS key pairs from the DB in the form of byte slices, than to have
to ensure files with the correct names exist on some persistent volume
such that the FIX app can load them from disk correctly.

To support this we changed the basic type stored within SessionSettings
to be a byte slice which means we are able to still support the existing
settings model, but also handle raw byte slices we can use for TLS
setup. The existing API exposed by SessionSettings remains unchanged
meaning users should not have to update their codee, but we have added a
new `SetRaw` and `RawSetting` accessors to directly read/write byte
slice values.
  • Loading branch information
smulube committed Jul 2, 2024
1 parent 358abef commit 5d51349
Show file tree
Hide file tree
Showing 7 changed files with 298 additions and 59 deletions.
3 changes: 3 additions & 0 deletions config/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ const (
SocketPrivateKeyFile string = "SocketPrivateKeyFile"
SocketCertificateFile string = "SocketCertificateFile"
SocketCAFile string = "SocketCAFile"
SocketPrivateKeyBytes string = "SocketPrivateKeyBytes"
SocketCertificateBytes string = "SocketCertificateBytes"
SocketCABytes string = "SocketCABytes"
SocketInsecureSkipVerify string = "SocketInsecureSkipVerify"
SocketServerName string = "SocketServerName"
SocketMinimumTLSVersion string = "SocketMinimumTLSVersion"
Expand Down
12 changes: 12 additions & 0 deletions config/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,18 @@ Certificate to use for secure TLS connections. Must be used with SocketPrivateKe
Optional root CA to use for secure TLS connections. For acceptors, client certificates will be verified against this CA. For initiators, clients will use the CA to verify the server certificate. If not configurated, initiators will verify the server certificate using the host's root CA set.
# SocketPrivateKeyBytes
Raw bytes of PEM encoded private to use for secure TLS connections. Must be used with SocketCertificateBytes
# SocketCertificateBytes
Raw bytes of PEM encoded certificate to use for secure TLS connections. Must be used with SocketPrivateKeyBytes
# SocketCABytes
Optional root CA to use for secure TLS connections as raw bytes. For acceptors, client certificates will be verified against this CA. For initiators, clients will use the CA to verify the server certificate. If not configurated, initiators will verify the server certificate using the host's root CA set.
# SocketServerName
The expected server name on a returned certificate, unless SocketInsecureSkipVerify is true. This is for the TLS Server Name Indication extension. Initiator only.
Expand Down
6 changes: 3 additions & 3 deletions session_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ func (f sessionFactory) newSession(
for _, dayStr := range dayStrs {
day, ok := dayLookup[dayStr]
if !ok {
err = IncorrectFormatForSetting{Setting: config.Weekdays, Value: weekdaysStr}
err = IncorrectFormatForSetting{Setting: config.Weekdays, Value: []byte(weekdaysStr)}
return
}
weekdays = append(weekdays, day)
Expand Down Expand Up @@ -315,7 +315,7 @@ func (f sessionFactory) newSession(
parseDay := func(setting, dayStr string) (day time.Weekday, err error) {
day, ok := dayLookup[dayStr]
if !ok {
return day, IncorrectFormatForSetting{Setting: setting, Value: dayStr}
return day, IncorrectFormatForSetting{Setting: setting, Value: []byte(dayStr)}
}
return
}
Expand Down Expand Up @@ -355,7 +355,7 @@ func (f sessionFactory) newSession(
s.timestampPrecision = Nanos

default:
err = IncorrectFormatForSetting{Setting: config.TimeStampPrecision, Value: precisionStr}
err = IncorrectFormatForSetting{Setting: config.TimeStampPrecision, Value: []byte(precisionStr)}
return
}
}
Expand Down
73 changes: 46 additions & 27 deletions session_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (

// SessionSettings maps session settings to values with typed accessors.
type SessionSettings struct {
settings map[string]string
settings map[string][]byte
}

// ConditionallyRequiredSetting indicates a missing setting.
Expand All @@ -37,8 +37,9 @@ func (e ConditionallyRequiredSetting) Error() string {

// IncorrectFormatForSetting indicates a setting that is incorrectly formatted.
type IncorrectFormatForSetting struct {
Setting, Value string
Err error
Setting string
Value []byte
Err error
}

func (e IncorrectFormatForSetting) Error() string {
Expand All @@ -47,7 +48,7 @@ func (e IncorrectFormatForSetting) Error() string {

// Init initializes or resets SessionSettings.
func (s *SessionSettings) Init() {
s.settings = make(map[string]string)
s.settings = make(map[string][]byte)
}

// NewSessionSettings returns a newly initialized SessionSettings instance.
Expand All @@ -58,8 +59,8 @@ func NewSessionSettings() *SessionSettings {
return s
}

// Set assigns a value to a setting on SessionSettings.
func (s *SessionSettings) Set(setting string, val string) {
// SetRaw assigns a value to a setting on SessionSettings.
func (s *SessionSettings) SetRaw(setting string, val []byte) {
// Lazy init.
if s.settings == nil {
s.Init()
Expand All @@ -68,69 +69,87 @@ func (s *SessionSettings) Set(setting string, val string) {
s.settings[setting] = val
}

// Set assigns a string value to a setting on SessionSettings.
func (s *SessionSettings) Set(setting string, val string) {
// Lazy init
if s.settings == nil {
s.Init()
}

s.settings[setting] = []byte(val)
}

// HasSetting returns true if a setting is set, false if not.
func (s *SessionSettings) HasSetting(setting string) bool {
_, ok := s.settings[setting]
return ok
}

// Setting is a settings string accessor. Returns an error if the setting is missing.
func (s *SessionSettings) Setting(setting string) (string, error) {
// RawSetting is a settings accessor that returns the raw byte slice value of
// the setting. Returns an error if the setting is missing.
func (s *SessionSettings) RawSetting(setting string) ([]byte, error) {
val, ok := s.settings[setting]
if !ok {
return val, ConditionallyRequiredSetting{setting}
return nil, ConditionallyRequiredSetting{Setting: setting}
}

return val, nil
}

// IntSetting returns the requested setting parsed as an int. Returns an errror if the setting is not set or cannot be parsed as an int.
func (s *SessionSettings) IntSetting(setting string) (val int, err error) {
stringVal, err := s.Setting(setting)
// Setting is a settings string accessor. Returns an error if the setting is missing.
func (s *SessionSettings) Setting(setting string) (string, error) {
val, err := s.RawSetting(setting)
if err != nil {
return "", err
}

return string(val), nil
}

// IntSetting returns the requested setting parsed as an int. Returns an errror if the setting is not set or cannot be parsed as an int.
func (s *SessionSettings) IntSetting(setting string) (int, error) {
rawVal, err := s.RawSetting(setting)
if err != nil {
return
return 0, err
}

if val, err = strconv.Atoi(stringVal); err != nil {
return val, IncorrectFormatForSetting{Setting: setting, Value: stringVal, Err: err}
if val, err := strconv.Atoi(string(rawVal)); err == nil {
return val, nil
}

return
return 0, IncorrectFormatForSetting{Setting: setting, Value: rawVal, Err: err}
}

// DurationSetting returns the requested setting parsed as a time.Duration.
// Returns an error if the setting is not set or cannot be parsed as a time.Duration.
func (s *SessionSettings) DurationSetting(setting string) (val time.Duration, err error) {
stringVal, err := s.Setting(setting)

func (s *SessionSettings) DurationSetting(setting string) (time.Duration, error) {
rawVal, err := s.RawSetting(setting)
if err != nil {
return
return 0, err
}

if val, err = time.ParseDuration(stringVal); err != nil {
return val, IncorrectFormatForSetting{Setting: setting, Value: stringVal, Err: err}
if val, err := time.ParseDuration(string(rawVal)); err == nil {
return val, nil
}

return
return 0, IncorrectFormatForSetting{Setting: setting, Value: rawVal, Err: err}
}

// BoolSetting returns the requested setting parsed as a boolean. Returns an error if the setting is not set or cannot be parsed as a bool.
func (s SessionSettings) BoolSetting(setting string) (bool, error) {
stringVal, err := s.Setting(setting)

rawVal, err := s.RawSetting(setting)
if err != nil {
return false, err
}

switch stringVal {
switch string(rawVal) {
case "Y", "y":
return true, nil
case "N", "n":
return false, nil
}

return false, IncorrectFormatForSetting{Setting: setting, Value: stringVal}
return false, IncorrectFormatForSetting{Setting: setting, Value: rawVal}
}

func (s *SessionSettings) overlay(overlay *SessionSettings) {
Expand Down
65 changes: 63 additions & 2 deletions session_settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
package quickfix

import (
"bytes"
"testing"
"time"

"github.com/quickfixgo/quickfix/config"
)
Expand Down Expand Up @@ -55,10 +57,15 @@ func TestSessionSettings_IntSettings(t *testing.T) {
}

s.Set(config.SocketAcceptPort, "notanint")
if _, err := s.IntSetting(config.SocketAcceptPort); err == nil {
_, err := s.IntSetting(config.SocketAcceptPort)
if err == nil {
t.Error("Expected error for unparsable value")
}

if err.Error() != `"notanint" is invalid for SocketAcceptPort` {
t.Errorf("Expected %s, got %s", `"notanint" is invalid for SocketAcceptPort`, err)
}

s.Set(config.SocketAcceptPort, "1005")
val, err := s.IntSetting(config.SocketAcceptPort)
if err != nil {
Expand All @@ -77,10 +84,15 @@ func TestSessionSettings_BoolSettings(t *testing.T) {
}

s.Set(config.ResetOnLogon, "notabool")
if _, err := s.BoolSetting(config.ResetOnLogon); err == nil {
_, err := s.BoolSetting(config.ResetOnLogon)
if err == nil {
t.Error("Expected error for unparsable value")
}

if err.Error() != `"notabool" is invalid for ResetOnLogon` {
t.Errorf("Expected %s, got %s", `"notabool" is invalid for ResetOnLogon`, err)
}

var boolTests = []struct {
input string
expected bool
Expand All @@ -105,6 +117,55 @@ func TestSessionSettings_BoolSettings(t *testing.T) {
}
}

func TestSessionSettings_DurationSettings(t *testing.T) {
s := NewSessionSettings()
if _, err := s.BoolSetting(config.ReconnectInterval); err == nil {
t.Error("Expected error for unknown setting")
}

s.Set(config.ReconnectInterval, "not duration")

_, err := s.DurationSetting(config.ReconnectInterval)
if err == nil {
t.Error("Expected error for unparsable value")
}

if err.Error() != `"not duration" is invalid for ReconnectInterval` {
t.Errorf("Expected %s, got %s", `"not duration" is invalid for ReconnectInterval`, err)
}

s.Set(config.ReconnectInterval, "10s")

got, err := s.DurationSetting(config.ReconnectInterval)
if err != nil {
t.Error("Unexpected err", err)
}

expected, _ := time.ParseDuration("10s")

if got != expected {
t.Errorf("Expected %v, got %v", expected, got)
}
}

func TestSessionSettings_ByteSettings(t *testing.T) {
s := NewSessionSettings()
if _, err := s.RawSetting(config.SocketPrivateKeyBytes); err == nil {
t.Error("Expected error for unknown setting")
}

s.SetRaw(config.SocketPrivateKeyBytes, []byte("pembytes"))

got, err := s.RawSetting(config.SocketPrivateKeyBytes)
if err != nil {
t.Error("Unexpected err", err)
}

if !bytes.Equal([]byte("pembytes"), got) {
t.Errorf("Expected %v, got %v", []byte("pembytes"), got)
}
}

func TestSessionSettings_Clone(t *testing.T) {
s := NewSessionSettings()

Expand Down
Loading

0 comments on commit 5d51349

Please sign in to comment.