From 9ed97c6d7758fc777f656628d031b71336f7335a Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 25 Nov 2022 14:57:38 +0300 Subject: [PATCH] add auth func --- fix/golang/auth.go | 158 ++++++++++++++++++++++++++++++++ fix/golang/client.go | 172 +++++++++++++++++++++++++++++++++++ fix/golang/config/config.cfg | 21 +++++ fix/golang/go.mod | 18 ++++ fix/golang/go.sum | 36 ++++++++ 5 files changed, 405 insertions(+) create mode 100644 fix/golang/auth.go create mode 100644 fix/golang/client.go create mode 100644 fix/golang/config/config.cfg create mode 100644 fix/golang/go.mod create mode 100644 fix/golang/go.sum diff --git a/fix/golang/auth.go b/fix/golang/auth.go new file mode 100644 index 0000000..d4acfe4 --- /dev/null +++ b/fix/golang/auth.go @@ -0,0 +1,158 @@ +package main + +import ( + "bytes" + "crypto/hmac" + "crypto/sha512" + "encoding/base64" + "encoding/binary" + "fmt" + "strconv" + "time" + + "github.com/quickfixgo/quickfix" + "github.com/quickfixgo/quickfix/enum" + "github.com/quickfixgo/quickfix/tag" +) + +const ( + delimiter = 0x01 +) + +type Presign struct { + SendingTime int64 + MsgSeqNum int + SenderCompID string + TargetCompID string + Password string +} + +func SignLogonMsg(msg *quickfix.Message, secret ApiSecret) { + if msg.IsMsgTypeOf(enum.MsgType_LOGON) { + //set passPhrase + msg.Body.SetString(tag.Password, strconv.FormatInt(time.Now().UTC().Unix(), 10)) + + //extract presign struct + presign, err := getMsgPresign(msg) + if err != nil { + fmt.Println(err) + return + } + + //make preSignByte from presign + preSignByte, err := makePresignByte(presign) + if err != nil { + fmt.Println(err) + return + } + + //sign is 64 based sha512(preSignByte) + sign := createSignFromBodyAndSecret(preSignByte, []byte(secret)) + + //sign the logonMessage + msg.Body.SetString(tag.RawData, sign) + } +} + +func getMsgPresign(msg *quickfix.Message) (Presign, error) { + var senderCompID quickfix.FIXString + err := msg.Header.GetField(tag.SenderCompID, &senderCompID) + if err != nil { + return Presign{}, err + } + var targetCompID quickfix.FIXString + err = msg.Header.GetField(tag.TargetCompID, &targetCompID) + if err != nil { + return Presign{}, err + } + var msgSeqNum quickfix.FIXInt + err = msg.Header.GetField(tag.MsgSeqNum, &msgSeqNum) + if err != nil { + return Presign{}, err + } + var sendingTime quickfix.FIXUTCTimestamp + err = msg.Header.GetField(tag.SendingTime, &sendingTime) + if err != nil { + return Presign{}, err + } + + var password quickfix.FIXString + err = msg.Body.GetField(tag.Password, &password) + if err != nil { + return Presign{}, err + } + + return Presign{ + SendingTime: sendingTime.UTC().Unix(), + MsgSeqNum: msgSeqNum.Int(), + SenderCompID: senderCompID.String(), + TargetCompID: targetCompID.String(), + Password: password.String(), + }, nil +} + +func makePresignByte(msg Presign) ([]byte, error) { + presignByte := new(bytes.Buffer) + + //sendingTime + binaryWriteErr := addToPresign(presignByte, msg.SendingTime, true) + if binaryWriteErr != nil { + return nil, binaryWriteErr + } + //msgSeqNum + binaryWriteErr = addToPresign(presignByte, int64(msg.MsgSeqNum), true) + if binaryWriteErr != nil { + return nil, binaryWriteErr + } + //senderCompID + binaryWriteErr = addToPresign(presignByte, msg.SenderCompID, true) + if binaryWriteErr != nil { + return nil, binaryWriteErr + } + //targetCompID + binaryWriteErr = addToPresign(presignByte, msg.TargetCompID, true) + if binaryWriteErr != nil { + return nil, binaryWriteErr + } + //password + binaryWriteErr = addToPresign(presignByte, msg.Password, false) + if binaryWriteErr != nil { + return nil, binaryWriteErr + } + + return presignByte.Bytes(), nil +} + +func addToPresign[Field int64 | string](presignByte *bytes.Buffer, field Field, withDelimeter bool) (binaryWriteErr error) { + switch f := any(field).(type) { + case string: + _, binaryWriteErr = presignByte.WriteString(f) + if binaryWriteErr != nil { + return binaryWriteErr + } + default: + binaryWriteErr = binary.Write(presignByte, binary.LittleEndian, f) + if binaryWriteErr != nil { + return binaryWriteErr + } + } + if withDelimeter { + return addDelimiter(presignByte) + } + return nil +} + +func addDelimiter(presignByte *bytes.Buffer) error { + binaryWriteErr := presignByte.WriteByte(delimiter) + if binaryWriteErr != nil { + return binaryWriteErr + } + return nil +} + +func createSignFromBodyAndSecret(body, secret []byte) string { + mac := hmac.New(sha512.New, secret) + _, _ = mac.Write(body) + + return base64.StdEncoding.EncodeToString(mac.Sum(nil)) +} diff --git a/fix/golang/client.go b/fix/golang/client.go new file mode 100644 index 0000000..9ce543f --- /dev/null +++ b/fix/golang/client.go @@ -0,0 +1,172 @@ +// Copyright (c) quickfixengine.org All rights reserved. +// +// This file may be distributed under the terms of the quickfixengine.org +// license as defined by quickfixengine.org and appearing in the file +// LICENSE included in the packaging of this file. +// +// This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING +// THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE. +// +// See http://www.quickfixengine.org/LICENSE for licensing information. +// +// Contact ask@quickfixengine.org if any conditions of this licensing +// are not clear to you. + +package main + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "log" + "os" + "os/signal" + "path" + "syscall" + + "github.com/fatih/color" + "github.com/pkg/errors" + "github.com/quickfixgo/quickfix" + "github.com/quickfixgo/quickfix/config" + "github.com/quickfixgo/quickfix/enum" +) + +type ( + ApiKey string + ApiSecret string +) + +// Client implements the quickfix.Application interface +type Client struct { + keys map[ApiKey]ApiSecret +} + +// OnCreate implemented as part of Application interface +func (e Client) OnCreate(sessionID quickfix.SessionID) {} + +// OnLogon implemented as part of Application interface +func (e Client) OnLogon(sessionID quickfix.SessionID) {} + +// OnLogout implemented as part of Application interface +func (e Client) OnLogout(sessionID quickfix.SessionID) {} + +// FromAdmin implemented as part of Application interface +func (e Client) FromAdmin(msg *quickfix.Message, sessionID quickfix.SessionID) (reject quickfix.MessageRejectError) { + if msg.IsMsgTypeOf(enum.MsgType_LOGON) { + log.Printf("api key [%s] is logged on", sessionID.SenderCompID) + } + return nil +} + +// ToAdmin implemented as part of Application interface +func (e Client) ToAdmin(msg *quickfix.Message, sessionID quickfix.SessionID) { + apiKey := ApiKey(sessionID.SenderCompID) + secret, exist := e.keys[apiKey] + if !exist { + log.Fatalf("unknown api-key [%s] in sessionID", apiKey) + } + SignLogonMsg(msg, secret) +} + +// ToApp implemented as part of Application interface +func (e Client) ToApp(msg *quickfix.Message, sessionID quickfix.SessionID) (err error) { + fmt.Printf("Sending %s\n", msg) + return +} + +// FromApp implemented as part of Application interface. This is the callback for all Application level messages from the counter party. +func (e Client) FromApp(msg *quickfix.Message, sessionID quickfix.SessionID) (reject quickfix.MessageRejectError) { + fmt.Printf("FromApp: %s\n", msg.String()) + return +} + +func main() { + err := startClient() + if err != nil { + log.Fatal(err) + } +} + +func startClient() error { + cfgFileName := path.Join("config", "config.cfg") + + cfg, err := os.Open(cfgFileName) + if err != nil { + return fmt.Errorf("Error opening %v, %v\n", cfgFileName, err) + } + defer cfg.Close() + + stringData, readErr := io.ReadAll(cfg) + if readErr != nil { + return fmt.Errorf("error reading cfg: %s", readErr) + } + + appSettings, err := quickfix.ParseSettings(bytes.NewReader(stringData)) + if err != nil { + return fmt.Errorf("error reading cfg: %s", err) + } + + sessions := appSettings.SessionSettings() + apiKeys := make(map[ApiKey]ApiSecret, len(sessions)) + for _, sessionSettings := range sessions { + sci, serr := sessionSettings.Setting(config.SenderCompID) + sec, perr := sessionSettings.Setting("Password") + if serr == nil && perr == nil { + apiKeys[ApiKey(sci)] = ApiSecret(sec) + } + } + app := Client{keys: apiKeys} + screenLogFactory := quickfix.NewScreenLogFactory() + + if err != nil { + return fmt.Errorf("error creating file log factory: %s", err) + } + + initiator, err := quickfix.NewInitiator(app, quickfix.NewMemoryStoreFactory(), appSettings, screenLogFactory) + if err != nil { + return fmt.Errorf("Unable to create Initiator: %s\n", err) + } + + err = initiator.Start() + if err != nil { + return fmt.Errorf("Unable to start Initiator: %s\n", err) + } + + printConfig(bytes.NewReader(stringData)) + + defer initiator.Stop() + awaitTermination() + return nil +} + +func printConfig(reader io.Reader) { + scanner := bufio.NewScanner(reader) + color.Set(color.Bold) + fmt.Println("Started FIX initiator with config:") + color.Unset() + + color.Set(color.FgHiMagenta) + for scanner.Scan() { + line := scanner.Text() + fmt.Println(line) + } + + color.Unset() +} + +func awaitTermination() { + // Listen to interrupt signal + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + defer func() { + stop() + if errors.Is(ctx.Err(), context.Canceled) { + log.Println("FIX initiator is stopped") + return + } + log.Println(ctx.Err()) + }() + <-ctx.Done() +} diff --git a/fix/golang/config/config.cfg b/fix/golang/config/config.cfg new file mode 100644 index 0000000..b9bc995 --- /dev/null +++ b/fix/golang/config/config.cfg @@ -0,0 +1,21 @@ +[DEFAULT] +SocketConnectHost=127.0.0.1 +SocketConnectPort=5001 +HeartBtInt=30 +TargetCompID=EXMO +ResetOnLogon=Y +FileLogPath=tmp + +[SESSION] +BeginString=FIX.4.4 +# this is your Api key obtained from https://exmo.com/profile/api +SenderCompID=K-1a07dcb30e6f6aea5421f5a24a321c5d5b78a952 +# this is your Api secret obtained from https://exmo.com/profile/api +Password=S-1b32ef93597b809a96a44e473a8413aacaaa9ac8 + +[SESSION] +BeginString=FIX.4.4 +# this is your Api key obtained from https://exmo.com/profile/api +SenderCompID=K-c3f69c0233b84eebd659ff58a6118bb2a704a628 +# this is your Api secret obtained from https://exmo.com/profile/api +Password=S-e3ff449f6b5772d41cff26da594679d1beca9631 \ No newline at end of file diff --git a/fix/golang/go.mod b/fix/golang/go.mod new file mode 100644 index 0000000..4d8667c --- /dev/null +++ b/fix/golang/go.mod @@ -0,0 +1,18 @@ +module gitlab.exmoney.com/golang/exmo_api_lib/fix/golang + +go 1.19 + +require ( + github.com/fatih/color v1.13.0 + github.com/pkg/errors v0.9.1 + github.com/quickfixgo/quickfix v0.6.0 +) + +require ( + github.com/mattn/go-colorable v0.1.9 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mattn/go-sqlite3 v1.14.16 // indirect + github.com/shopspring/decimal v1.3.1 // indirect + github.com/stretchr/testify v1.8.1 // indirect + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect +) diff --git a/fix/golang/go.sum b/fix/golang/go.sum new file mode 100644 index 0000000..5443e1d --- /dev/null +++ b/fix/golang/go.sum @@ -0,0 +1,36 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quickfixgo/quickfix v0.6.0 h1:sSUFaKiMVaaFLGgWaK1ZmwFNZeQ0/awu+IzEu3cJWJE= +github.com/quickfixgo/quickfix v0.6.0/go.mod h1:RuN5MIPnzolPNDYibgBXHhgMoTEjjPzcCN3rLFcODS4= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=