Skip to content

Commit d3b4f98

Browse files
committed
Support TLS configuration as raw bytes
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.
1 parent e3a2994 commit d3b4f98

File tree

6 files changed

+324
-59
lines changed

6 files changed

+324
-59
lines changed

config/configuration.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,47 @@ const (
739739
// - A filepath to a file with read access.
740740
SocketCAFile string = "SocketCAFile"
741741

742+
// SocketPrivateKeyBytes is an optional value containing raw bytes of a PEM
743+
// encoded private key to use for secure TLS communications.
744+
// Must be used with SocketCertificateBytes.
745+
// Must contain PEM encoded data.
746+
//
747+
// Required: No
748+
//
749+
// Default: N/A
750+
//
751+
// Valid Values:
752+
// - Raw bytes containing a valid PEM encoded private key.
753+
SocketPrivateKeyBytes string = "SocketPrivateKeyBytes"
754+
755+
// SocketCertificateBytes is an optional value containing raw bytes of a PEM
756+
// encoded certificate to use for secure TLS communications.
757+
// Must be used with SocketPrivateKeyBytes.
758+
// Must contain PEM encoded data.
759+
//
760+
// Required: No
761+
//
762+
// Default: N/A
763+
//
764+
// Valid Values:
765+
// - Raw bytes containing a valid PEM encoded certificate.
766+
SocketCertificateBytes string = "SocketCertificateBytes"
767+
768+
// SocketCABytes is an optional value containing raw bytes of a PEM encoded
769+
// root CA to use for secure TLS communications. For acceptors, client
770+
// certificates will be verified against this CA. For initiators, clients
771+
// will use the CA to verify the server certificate. If not configured,
772+
// initiators will verify the server certificates using the host's root CA
773+
// set.
774+
//
775+
// Required: No
776+
//
777+
// Default: N/A
778+
//
779+
// Valid Values:
780+
// - Raw bytes containing a valid PEM encoded CA.
781+
SocketCABytes string = "SocketCABytes"
782+
742783
// SocketInsecureSkipVerify controls whether a client verifies the server's certificate chain and host name.
743784
// If SocketInsecureSkipVerify is set to Y, crypto/tls accepts any certificate presented by the server and any host name in that certificate.
744785
// In this mode, TLS is susceptible to machine-in-the-middle attacks unless custom verification is used.

session_factory.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ func (f sessionFactory) newSession(
284284
for _, dayStr := range dayStrs {
285285
day, ok := dayLookup[dayStr]
286286
if !ok {
287-
err = IncorrectFormatForSetting{Setting: config.Weekdays, Value: weekdaysStr}
287+
err = IncorrectFormatForSetting{Setting: config.Weekdays, Value: []byte(weekdaysStr)}
288288
return
289289
}
290290
weekdays = append(weekdays, day)
@@ -315,7 +315,7 @@ func (f sessionFactory) newSession(
315315
parseDay := func(setting, dayStr string) (day time.Weekday, err error) {
316316
day, ok := dayLookup[dayStr]
317317
if !ok {
318-
return day, IncorrectFormatForSetting{Setting: setting, Value: dayStr}
318+
return day, IncorrectFormatForSetting{Setting: setting, Value: []byte(dayStr)}
319319
}
320320
return
321321
}
@@ -355,7 +355,7 @@ func (f sessionFactory) newSession(
355355
s.timestampPrecision = Nanos
356356

357357
default:
358-
err = IncorrectFormatForSetting{Setting: config.TimeStampPrecision, Value: precisionStr}
358+
err = IncorrectFormatForSetting{Setting: config.TimeStampPrecision, Value: []byte(precisionStr)}
359359
return
360360
}
361361
}

session_settings.go

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323

2424
// SessionSettings maps session settings to values with typed accessors.
2525
type SessionSettings struct {
26-
settings map[string]string
26+
settings map[string][]byte
2727
}
2828

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

3838
// IncorrectFormatForSetting indicates a setting that is incorrectly formatted.
3939
type IncorrectFormatForSetting struct {
40-
Setting, Value string
41-
Err error
40+
Setting string
41+
Value []byte
42+
Err error
4243
}
4344

4445
func (e IncorrectFormatForSetting) Error() string {
@@ -47,7 +48,7 @@ func (e IncorrectFormatForSetting) Error() string {
4748

4849
// Init initializes or resets SessionSettings.
4950
func (s *SessionSettings) Init() {
50-
s.settings = make(map[string]string)
51+
s.settings = make(map[string][]byte)
5152
}
5253

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

61-
// Set assigns a value to a setting on SessionSettings.
62-
func (s *SessionSettings) Set(setting string, val string) {
62+
// SetRaw assigns a value to a setting on SessionSettings.
63+
func (s *SessionSettings) SetRaw(setting string, val []byte) {
6364
// Lazy init.
6465
if s.settings == nil {
6566
s.Init()
@@ -68,69 +69,87 @@ func (s *SessionSettings) Set(setting string, val string) {
6869
s.settings[setting] = val
6970
}
7071

72+
// Set assigns a string value to a setting on SessionSettings.
73+
func (s *SessionSettings) Set(setting string, val string) {
74+
// Lazy init
75+
if s.settings == nil {
76+
s.Init()
77+
}
78+
79+
s.settings[setting] = []byte(val)
80+
}
81+
7182
// HasSetting returns true if a setting is set, false if not.
7283
func (s *SessionSettings) HasSetting(setting string) bool {
7384
_, ok := s.settings[setting]
7485
return ok
7586
}
7687

77-
// Setting is a settings string accessor. Returns an error if the setting is missing.
78-
func (s *SessionSettings) Setting(setting string) (string, error) {
88+
// RawSetting is a settings accessor that returns the raw byte slice value of
89+
// the setting. Returns an error if the setting is missing.
90+
func (s *SessionSettings) RawSetting(setting string) ([]byte, error) {
7991
val, ok := s.settings[setting]
8092
if !ok {
81-
return val, ConditionallyRequiredSetting{setting}
93+
return nil, ConditionallyRequiredSetting{Setting: setting}
8294
}
8395

8496
return val, nil
8597
}
8698

87-
// 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.
88-
func (s *SessionSettings) IntSetting(setting string) (val int, err error) {
89-
stringVal, err := s.Setting(setting)
99+
// Setting is a settings string accessor. Returns an error if the setting is missing.
100+
func (s *SessionSettings) Setting(setting string) (string, error) {
101+
val, err := s.RawSetting(setting)
102+
if err != nil {
103+
return "", err
104+
}
90105

106+
return string(val), nil
107+
}
108+
109+
// 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.
110+
func (s *SessionSettings) IntSetting(setting string) (int, error) {
111+
rawVal, err := s.RawSetting(setting)
91112
if err != nil {
92-
return
113+
return 0, err
93114
}
94115

95-
if val, err = strconv.Atoi(stringVal); err != nil {
96-
return val, IncorrectFormatForSetting{Setting: setting, Value: stringVal, Err: err}
116+
if val, err := strconv.Atoi(string(rawVal)); err == nil {
117+
return val, nil
97118
}
98119

99-
return
120+
return 0, IncorrectFormatForSetting{Setting: setting, Value: rawVal, Err: err}
100121
}
101122

102123
// DurationSetting returns the requested setting parsed as a time.Duration.
103124
// Returns an error if the setting is not set or cannot be parsed as a time.Duration.
104-
func (s *SessionSettings) DurationSetting(setting string) (val time.Duration, err error) {
105-
stringVal, err := s.Setting(setting)
106-
125+
func (s *SessionSettings) DurationSetting(setting string) (time.Duration, error) {
126+
rawVal, err := s.RawSetting(setting)
107127
if err != nil {
108-
return
128+
return 0, err
109129
}
110130

111-
if val, err = time.ParseDuration(stringVal); err != nil {
112-
return val, IncorrectFormatForSetting{Setting: setting, Value: stringVal, Err: err}
131+
if val, err := time.ParseDuration(string(rawVal)); err == nil {
132+
return val, nil
113133
}
114134

115-
return
135+
return 0, IncorrectFormatForSetting{Setting: setting, Value: rawVal, Err: err}
116136
}
117137

118138
// 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.
119139
func (s SessionSettings) BoolSetting(setting string) (bool, error) {
120-
stringVal, err := s.Setting(setting)
121-
140+
rawVal, err := s.RawSetting(setting)
122141
if err != nil {
123142
return false, err
124143
}
125144

126-
switch stringVal {
145+
switch string(rawVal) {
127146
case "Y", "y":
128147
return true, nil
129148
case "N", "n":
130149
return false, nil
131150
}
132151

133-
return false, IncorrectFormatForSetting{Setting: setting, Value: stringVal}
152+
return false, IncorrectFormatForSetting{Setting: setting, Value: rawVal}
134153
}
135154

136155
func (s *SessionSettings) overlay(overlay *SessionSettings) {

session_settings_test.go

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
package quickfix
1717

1818
import (
19+
"bytes"
1920
"testing"
21+
"time"
2022

2123
"github.com/quickfixgo/quickfix/config"
2224
)
@@ -55,10 +57,15 @@ func TestSessionSettings_IntSettings(t *testing.T) {
5557
}
5658

5759
s.Set(config.SocketAcceptPort, "notanint")
58-
if _, err := s.IntSetting(config.SocketAcceptPort); err == nil {
60+
_, err := s.IntSetting(config.SocketAcceptPort)
61+
if err == nil {
5962
t.Error("Expected error for unparsable value")
6063
}
6164

65+
if err.Error() != `"notanint" is invalid for SocketAcceptPort` {
66+
t.Errorf("Expected %s, got %s", `"notanint" is invalid for SocketAcceptPort`, err)
67+
}
68+
6269
s.Set(config.SocketAcceptPort, "1005")
6370
val, err := s.IntSetting(config.SocketAcceptPort)
6471
if err != nil {
@@ -77,10 +84,15 @@ func TestSessionSettings_BoolSettings(t *testing.T) {
7784
}
7885

7986
s.Set(config.ResetOnLogon, "notabool")
80-
if _, err := s.BoolSetting(config.ResetOnLogon); err == nil {
87+
_, err := s.BoolSetting(config.ResetOnLogon)
88+
if err == nil {
8189
t.Error("Expected error for unparsable value")
8290
}
8391

92+
if err.Error() != `"notabool" is invalid for ResetOnLogon` {
93+
t.Errorf("Expected %s, got %s", `"notabool" is invalid for ResetOnLogon`, err)
94+
}
95+
8496
var boolTests = []struct {
8597
input string
8698
expected bool
@@ -105,6 +117,55 @@ func TestSessionSettings_BoolSettings(t *testing.T) {
105117
}
106118
}
107119

120+
func TestSessionSettings_DurationSettings(t *testing.T) {
121+
s := NewSessionSettings()
122+
if _, err := s.BoolSetting(config.ReconnectInterval); err == nil {
123+
t.Error("Expected error for unknown setting")
124+
}
125+
126+
s.Set(config.ReconnectInterval, "not duration")
127+
128+
_, err := s.DurationSetting(config.ReconnectInterval)
129+
if err == nil {
130+
t.Error("Expected error for unparsable value")
131+
}
132+
133+
if err.Error() != `"not duration" is invalid for ReconnectInterval` {
134+
t.Errorf("Expected %s, got %s", `"not duration" is invalid for ReconnectInterval`, err)
135+
}
136+
137+
s.Set(config.ReconnectInterval, "10s")
138+
139+
got, err := s.DurationSetting(config.ReconnectInterval)
140+
if err != nil {
141+
t.Error("Unexpected err", err)
142+
}
143+
144+
expected, _ := time.ParseDuration("10s")
145+
146+
if got != expected {
147+
t.Errorf("Expected %v, got %v", expected, got)
148+
}
149+
}
150+
151+
func TestSessionSettings_ByteSettings(t *testing.T) {
152+
s := NewSessionSettings()
153+
if _, err := s.RawSetting(config.SocketPrivateKeyBytes); err == nil {
154+
t.Error("Expected error for unknown setting")
155+
}
156+
157+
s.SetRaw(config.SocketPrivateKeyBytes, []byte("pembytes"))
158+
159+
got, err := s.RawSetting(config.SocketPrivateKeyBytes)
160+
if err != nil {
161+
t.Error("Unexpected err", err)
162+
}
163+
164+
if !bytes.Equal([]byte("pembytes"), got) {
165+
t.Errorf("Expected %v, got %v", []byte("pembytes"), got)
166+
}
167+
}
168+
108169
func TestSessionSettings_Clone(t *testing.T) {
109170
s := NewSessionSettings()
110171

0 commit comments

Comments
 (0)