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 Aug 6, 2024
1 parent e3a2994 commit d3b4f98
Show file tree
Hide file tree
Showing 6 changed files with 324 additions and 59 deletions.
41 changes: 41 additions & 0 deletions config/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,47 @@ const (
// - A filepath to a file with read access.
SocketCAFile string = "SocketCAFile"

// SocketPrivateKeyBytes is an optional value containing raw bytes of a PEM
// encoded private key to use for secure TLS communications.
// Must be used with SocketCertificateBytes.
// Must contain PEM encoded data.
//
// Required: No
//
// Default: N/A
//
// Valid Values:
// - Raw bytes containing a valid PEM encoded private key.
SocketPrivateKeyBytes string = "SocketPrivateKeyBytes"

// SocketCertificateBytes is an optional value containing raw bytes of a PEM
// encoded certificate to use for secure TLS communications.
// Must be used with SocketPrivateKeyBytes.
// Must contain PEM encoded data.
//
// Required: No
//
// Default: N/A
//
// Valid Values:
// - Raw bytes containing a valid PEM encoded certificate.
SocketCertificateBytes string = "SocketCertificateBytes"

// SocketCABytes is an optional value containing raw bytes of a PEM encoded
// root CA to use for secure TLS communications. For acceptors, client
// certificates will be verified against this CA. For initiators, clients
// will use the CA to verify the server certificate. If not configured,
// initiators will verify the server certificates using the host's root CA
// set.
//
// Required: No
//
// Default: N/A
//
// Valid Values:
// - Raw bytes containing a valid PEM encoded CA.
SocketCABytes string = "SocketCABytes"

// SocketInsecureSkipVerify controls whether a client verifies the server's certificate chain and host name.
// If SocketInsecureSkipVerify is set to Y, crypto/tls accepts any certificate presented by the server and any host name in that certificate.
// In this mode, TLS is susceptible to machine-in-the-middle attacks unless custom verification is used.
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 d3b4f98

Please sign in to comment.