Skip to content

Support TLS configuration as raw bytes #647

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading