Skip to content
Open
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
158 changes: 158 additions & 0 deletions fix/golang/auth.go
Original file line number Diff line number Diff line change
@@ -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))
}
172 changes: 172 additions & 0 deletions fix/golang/client.go
Original file line number Diff line number Diff line change
@@ -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()
}
21 changes: 21 additions & 0 deletions fix/golang/config/config.cfg
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions fix/golang/go.mod
Original file line number Diff line number Diff line change
@@ -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
)
Loading