274 lines
6.2 KiB
Go
274 lines
6.2 KiB
Go
// Package external defines all external services access
|
|
package external
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
"github.com/sasha-s/go-deadlock"
|
|
|
|
"quantex.com/qfixpt/src/app"
|
|
"quantex.com/qfixpt/src/app/version"
|
|
jwttoken "quantex.com/qfixpt/src/common/jwttoken"
|
|
"quantex.com/qfixpt/src/domain"
|
|
)
|
|
|
|
const (
|
|
sTimeout = 10
|
|
lTimeout = 15
|
|
)
|
|
|
|
type Config struct {
|
|
QApixPort string
|
|
QApixHost string
|
|
External map[string]app.ExtAuth
|
|
QApixToken string
|
|
EnableJWTAuth bool
|
|
}
|
|
|
|
type cacheItem struct {
|
|
Time time.Time
|
|
Resp []byte
|
|
}
|
|
|
|
type Manager struct {
|
|
config Config
|
|
notifier domain.Notifier
|
|
cache map[string]cacheItem
|
|
mutex deadlock.RWMutex
|
|
}
|
|
|
|
type RequestOptions struct {
|
|
Method string
|
|
Path string
|
|
URL string
|
|
Token string
|
|
Secured bool
|
|
Body interface{}
|
|
Retries int
|
|
Timeout time.Duration
|
|
CacheDuration time.Duration
|
|
Auth *app.ExtAuth
|
|
SessionToken string
|
|
}
|
|
|
|
// NewManager create new Manager struct
|
|
func NewManager(n domain.Notifier, cfg Config) *Manager {
|
|
return &Manager{
|
|
config: cfg,
|
|
notifier: n,
|
|
cache: make(map[string]cacheItem),
|
|
}
|
|
}
|
|
|
|
//revive:disable:argument-limit we need this arguments
|
|
func (s *Manager) sendRequestToExternal(opts RequestOptions) ([]byte, error) {
|
|
host := fmt.Sprintf("http://localhost:%v", opts.Auth.Port)
|
|
if opts.Auth.Host != "" {
|
|
host = opts.Auth.Host
|
|
}
|
|
|
|
token := s.config.QApixToken
|
|
|
|
secured := false
|
|
|
|
if s.config.EnableJWTAuth && opts.Auth != nil {
|
|
t, err := jwttoken.Encrypt(*opts.Auth)
|
|
if err != nil {
|
|
e := fmt.Errorf("error encrypting quantexService: %w", err)
|
|
log.Error().Msg(e.Error())
|
|
|
|
return nil, e
|
|
}
|
|
|
|
token = t
|
|
|
|
secured = true
|
|
}
|
|
|
|
opts.Secured = secured
|
|
opts.Token = token
|
|
opts.URL = urlFrom(host, opts.Path)
|
|
|
|
return s.sendRequestWithCache(opts)
|
|
}
|
|
|
|
func (s *Manager) sendRequestWithCache(opts RequestOptions) ([]byte, error) {
|
|
sha := optionSha(opts)
|
|
|
|
if opts.CacheDuration > 0 && len(sha) > 0 {
|
|
s.mutex.RLock()
|
|
t, ok := s.cache[sha]
|
|
s.mutex.RUnlock()
|
|
|
|
if ok && time.Since(t.Time) < opts.CacheDuration {
|
|
return t.Resp, nil
|
|
}
|
|
}
|
|
|
|
resp, err := s.sendRequestWithRetries(opts)
|
|
if err == nil && len(sha) > 0 {
|
|
s.mutex.Lock()
|
|
s.cache[sha] = cacheItem{
|
|
time.Now(),
|
|
resp,
|
|
}
|
|
s.mutex.Unlock()
|
|
}
|
|
|
|
return resp, err
|
|
}
|
|
|
|
//revive:disable:flag-parameter we need this flag
|
|
//nolint:funlen //it's long but easy to read
|
|
func (s *Manager) sendRequestWithRetries(opts RequestOptions) ([]byte, error) {
|
|
client := &http.Client{
|
|
Timeout: opts.Timeout,
|
|
}
|
|
|
|
bodyBytes, err := json.Marshal(&opts.Body)
|
|
if err != nil {
|
|
e := fmt.Errorf("error encoding the body: %v, %w", opts.Body, err)
|
|
slog.Error(e.Error())
|
|
|
|
return nil, e
|
|
}
|
|
|
|
slog.Debug("sending request to: " + opts.URL)
|
|
|
|
request, err := http.NewRequest(opts.Method, opts.URL, bytes.NewBuffer(bodyBytes)) //nolint:noctx //no ctx needed
|
|
if err != nil {
|
|
e := fmt.Errorf("error creating new %s request to: %s, %w", opts.Method, opts.URL, err)
|
|
slog.Error(e.Error())
|
|
|
|
return nil, e
|
|
}
|
|
|
|
authorization := "Bearer " + opts.Token
|
|
if opts.Secured {
|
|
authorization = opts.Token
|
|
}
|
|
|
|
request.Header.Set("Authorization", authorization)
|
|
|
|
request.Header.Set("Content-Type", "application/json")
|
|
|
|
if opts.SessionToken != "" {
|
|
// Create a cookie
|
|
cookie := &http.Cookie{
|
|
Name: "session_token",
|
|
Value: opts.SessionToken,
|
|
Path: "/",
|
|
}
|
|
|
|
// Add the cookie to the request
|
|
request.AddCookie(cookie)
|
|
}
|
|
|
|
var resp *http.Response
|
|
for i := 0; i <= opts.Retries; i++ { //nolint:wsl // It's ok in this case
|
|
interval := time.Duration(i) * time.Second
|
|
slog.Debug(fmt.Sprintf("request to '%s' try #%v in %v", request.URL, i, interval))
|
|
time.Sleep(interval)
|
|
|
|
resp, err = client.Do(request)
|
|
if err != nil {
|
|
e := fmt.Errorf("error making request to %s. error: %w", request.URL, err)
|
|
slog.Error(e.Error())
|
|
|
|
// send notification if notifier is available
|
|
s.sendNotification(domain.MessageChannelError, e.Error(), domain.MessageStatusWarning, nil)
|
|
|
|
continue
|
|
}
|
|
|
|
break
|
|
}
|
|
|
|
if resp == nil {
|
|
err = fmt.Errorf("error: '%s' response is nil", request.URL)
|
|
slog.Error(err.Error())
|
|
|
|
return nil, err
|
|
}
|
|
|
|
defer func() {
|
|
if err := resp.Body.Close(); err != nil {
|
|
slog.Error("error closing response body at sendRequestWithRetries: " + err.Error())
|
|
}
|
|
}()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
msg := fmt.Sprintf("response code from %s is not 200. StatusCode: %d.", opts.URL, resp.StatusCode)
|
|
// send notification if notifier is available
|
|
s.sendNotification(domain.MessageChannelError, msg, domain.MessageStatusWarning, nil)
|
|
|
|
return nil, fmt.Errorf("error %s", msg)
|
|
}
|
|
|
|
bodyBytes, resErr := io.ReadAll(resp.Body)
|
|
if resErr != nil {
|
|
msg := fmt.Sprintf("error reading response from %s. error: %s", opts.URL, resErr.Error())
|
|
// send notification if notifier is available
|
|
s.sendNotification(domain.MessageChannelError, msg, domain.MessageStatusWarning, nil)
|
|
|
|
return nil, fmt.Errorf("error %s", msg)
|
|
}
|
|
|
|
return bodyBytes, nil
|
|
}
|
|
|
|
//revive:enable
|
|
|
|
// sendNotification is a nil-safe wrapper around the notifier's SendMsg method.
|
|
// Some call sites run in contexts where Manager.notifier may be nil, so guard the call
|
|
// to avoid runtime panics.
|
|
func (s *Manager) sendNotification(channel domain.MessageChannel, message string, status domain.MessageStatus, wg *sync.WaitGroup) {
|
|
if s == nil || s.notifier == nil {
|
|
slog.Debug("notifier is nil, skipping SendMsg")
|
|
|
|
return
|
|
}
|
|
|
|
s.notifier.SendMsg(channel, message, status, wg)
|
|
}
|
|
|
|
func optionSha(opts RequestOptions) string {
|
|
key := fmt.Sprintf("%s-%s-%s", opts.Method, opts.URL, opts.Body)
|
|
|
|
return shaString(key)
|
|
}
|
|
|
|
func shaString(s string) (sha string) {
|
|
hash := sha256.New()
|
|
if _, err := hash.Write([]byte(s)); err != nil {
|
|
err = fmt.Errorf("error generating hash at generateShaString, error: %w", err)
|
|
slog.Error(err.Error())
|
|
|
|
return ""
|
|
}
|
|
|
|
return hex.EncodeToString(hash.Sum(nil))
|
|
}
|
|
|
|
func urlFrom(host, path string) string {
|
|
from := strings.ToLower(version.AppName)
|
|
|
|
url := fmt.Sprintf("%s%s?from=%s", host, path, from)
|
|
if strings.Contains(path, "?") {
|
|
url = fmt.Sprintf("%s%s&from=%s", host, path, from)
|
|
}
|
|
|
|
return url
|
|
}
|