fixed fee direction
This commit is contained in:
604
api/gateway/aurora/internal/server/internal/serverimp.go
Normal file
604
api/gateway/aurora/internal/server/internal/serverimp.go
Normal file
@@ -0,0 +1,604 @@
|
||||
package serverimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/tech/sendico/gateway/aurora/internal/appversion"
|
||||
auroraservice "github.com/tech/sendico/gateway/aurora/internal/service/gateway"
|
||||
"github.com/tech/sendico/gateway/aurora/internal/service/provider"
|
||||
"github.com/tech/sendico/gateway/aurora/storage"
|
||||
gatewaymongo "github.com/tech/sendico/gateway/aurora/storage/mongo"
|
||||
"github.com/tech/sendico/pkg/api/routers"
|
||||
"github.com/tech/sendico/pkg/db"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
msg "github.com/tech/sendico/pkg/messaging"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
|
||||
"github.com/tech/sendico/pkg/server/grpcapp"
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Imp struct {
|
||||
logger mlogger.Logger
|
||||
file string
|
||||
debug bool
|
||||
|
||||
config *config
|
||||
app *grpcapp.App[storage.Repository]
|
||||
http *http.Server
|
||||
service *auroraservice.Service
|
||||
}
|
||||
|
||||
type config struct {
|
||||
*grpcapp.Config `yaml:",inline"`
|
||||
Provider gatewayProviderConfig `yaml:"aurora"`
|
||||
LegacyProvider gatewayProviderConfig `yaml:"mcards"`
|
||||
Gateway gatewayConfig `yaml:"gateway"`
|
||||
HTTP httpConfig `yaml:"http"`
|
||||
}
|
||||
|
||||
type gatewayProviderConfig struct {
|
||||
BaseURL string `yaml:"base_url"`
|
||||
BaseURLEnv string `yaml:"base_url_env"`
|
||||
ProjectID int64 `yaml:"project_id"`
|
||||
ProjectIDEnv string `yaml:"project_id_env"`
|
||||
SecretKey string `yaml:"secret_key"`
|
||||
SecretKeyEnv string `yaml:"secret_key_env"`
|
||||
AllowedCurrencies []string `yaml:"allowed_currencies"`
|
||||
RequireCustomerAddress bool `yaml:"require_customer_address"`
|
||||
RequestTimeoutSeconds int `yaml:"request_timeout_seconds"`
|
||||
StatusSuccess string `yaml:"status_success"`
|
||||
StatusProcessing string `yaml:"status_processing"`
|
||||
StrictOperationMode bool `yaml:"strict_operation_mode"`
|
||||
}
|
||||
|
||||
type gatewayConfig struct {
|
||||
ID string `yaml:"id"`
|
||||
Network string `yaml:"network"`
|
||||
Currencies []string `yaml:"currencies"`
|
||||
IsEnabled *bool `yaml:"is_enabled"`
|
||||
Limits limitsConfig `yaml:"limits"`
|
||||
}
|
||||
|
||||
type limitsConfig struct {
|
||||
MinAmount string `yaml:"min_amount"`
|
||||
MaxAmount string `yaml:"max_amount"`
|
||||
PerTxMaxFee string `yaml:"per_tx_max_fee"`
|
||||
PerTxMinAmount string `yaml:"per_tx_min_amount"`
|
||||
PerTxMaxAmount string `yaml:"per_tx_max_amount"`
|
||||
VolumeLimit map[string]string `yaml:"volume_limit"`
|
||||
VelocityLimit map[string]int `yaml:"velocity_limit"`
|
||||
CurrencyLimits map[string]limitsOverrideCfg `yaml:"currency_limits"`
|
||||
}
|
||||
|
||||
type limitsOverrideCfg struct {
|
||||
MaxVolume string `yaml:"max_volume"`
|
||||
MinAmount string `yaml:"min_amount"`
|
||||
MaxAmount string `yaml:"max_amount"`
|
||||
MaxFee string `yaml:"max_fee"`
|
||||
MaxOps int `yaml:"max_ops"`
|
||||
}
|
||||
|
||||
type httpConfig struct {
|
||||
Callback callbackConfig `yaml:"callback"`
|
||||
}
|
||||
|
||||
type callbackConfig struct {
|
||||
Address string `yaml:"address"`
|
||||
Path string `yaml:"path"`
|
||||
AllowedCIDRs []string `yaml:"allowed_cidrs"`
|
||||
MaxBodyBytes int64 `yaml:"max_body_bytes"`
|
||||
}
|
||||
|
||||
// Create initialises the Aurora gateway server implementation.
|
||||
func Create(logger mlogger.Logger, file string, debug bool) (*Imp, error) {
|
||||
return &Imp{
|
||||
logger: logger.Named("server"),
|
||||
file: file,
|
||||
debug: debug,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (i *Imp) Shutdown() {
|
||||
if i.app == nil {
|
||||
return
|
||||
}
|
||||
|
||||
timeout := 15 * time.Second
|
||||
if i.config != nil && i.config.Runtime != nil {
|
||||
timeout = i.config.Runtime.ShutdownTimeout()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
if i.service != nil {
|
||||
i.service.Shutdown()
|
||||
}
|
||||
if i.http != nil {
|
||||
_ = i.http.Shutdown(ctx)
|
||||
i.http = nil
|
||||
}
|
||||
|
||||
i.app.Shutdown(ctx)
|
||||
}
|
||||
|
||||
func (i *Imp) Start() error {
|
||||
i.logger.Info("Starting Aurora gateway", zap.String("config_file", i.file), zap.Bool("debug", i.debug))
|
||||
|
||||
cfg, err := i.loadConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i.config = cfg
|
||||
|
||||
i.logger.Info("Configuration loaded",
|
||||
zap.String("grpc_address", cfg.GRPC.Address),
|
||||
zap.String("metrics_address", cfg.Metrics.Address),
|
||||
)
|
||||
|
||||
providerSection := effectiveProviderConfig(cfg.Provider, cfg.LegacyProvider)
|
||||
providerCfg, err := i.resolveProviderConfig(providerSection)
|
||||
if err != nil {
|
||||
i.logger.Error("Failed to resolve provider configuration", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
callbackCfg, err := i.resolveCallbackConfig(cfg.HTTP.Callback)
|
||||
if err != nil {
|
||||
i.logger.Error("Failed to resolve callback configuration", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
i.logger.Info("Provider configuration resolved",
|
||||
zap.Bool("base_url_set", strings.TrimSpace(providerCfg.BaseURL) != ""),
|
||||
zap.Int64("project_id", providerCfg.ProjectID),
|
||||
zap.Bool("secret_key_set", strings.TrimSpace(providerCfg.SecretKey) != ""),
|
||||
zap.Int("allowed_currencies", len(providerCfg.AllowedCurrencies)),
|
||||
zap.Bool("require_customer_address", providerCfg.RequireCustomerAddress),
|
||||
zap.Duration("request_timeout", providerCfg.RequestTimeout),
|
||||
zap.String("status_success", providerCfg.SuccessStatus()),
|
||||
zap.String("status_processing", providerCfg.ProcessingStatus()),
|
||||
zap.Bool("strict_operation_mode", providerSection.StrictOperationMode),
|
||||
)
|
||||
|
||||
gatewayDescriptor := resolveGatewayDescriptor(cfg.Gateway, providerCfg)
|
||||
if gatewayDescriptor != nil {
|
||||
i.logger.Info("Gateway descriptor resolved",
|
||||
zap.String("id", gatewayDescriptor.GetId()),
|
||||
zap.String("rail", gatewayDescriptor.GetRail().String()),
|
||||
zap.String("network", gatewayDescriptor.GetNetwork()),
|
||||
zap.Int("currencies", len(gatewayDescriptor.GetCurrencies())),
|
||||
zap.Bool("enabled", gatewayDescriptor.GetIsEnabled()),
|
||||
)
|
||||
}
|
||||
|
||||
i.logger.Info("Callback configuration resolved",
|
||||
zap.String("address", callbackCfg.Address),
|
||||
zap.String("path", callbackCfg.Path),
|
||||
zap.Int("allowed_cidrs", len(callbackCfg.AllowedCIDRs)),
|
||||
zap.Int64("max_body_bytes", callbackCfg.MaxBodyBytes),
|
||||
)
|
||||
|
||||
serviceFactory := func(logger mlogger.Logger, repo storage.Repository, producer msg.Producer) (grpcapp.Service, error) {
|
||||
invokeURI := ""
|
||||
if cfg.GRPC != nil {
|
||||
invokeURI = cfg.GRPC.DiscoveryInvokeURI()
|
||||
}
|
||||
opts := []auroraservice.Option{
|
||||
auroraservice.WithDiscoveryInvokeURI(invokeURI),
|
||||
auroraservice.WithProducer(producer),
|
||||
auroraservice.WithProviderConfig(providerCfg),
|
||||
auroraservice.WithStrictOperationIsolation(providerSection.StrictOperationMode),
|
||||
auroraservice.WithGatewayDescriptor(gatewayDescriptor),
|
||||
auroraservice.WithHTTPClient(&http.Client{Timeout: providerCfg.Timeout()}),
|
||||
auroraservice.WithStorage(repo),
|
||||
}
|
||||
if cfg.Messaging != nil {
|
||||
opts = append(opts, auroraservice.WithMessagingSettings(cfg.Messaging.Settings))
|
||||
}
|
||||
svc := auroraservice.NewService(logger, opts...)
|
||||
i.service = svc
|
||||
|
||||
if err := i.startHTTPCallbackServer(svc, callbackCfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
repoFactory := func(logger mlogger.Logger, conn *db.MongoConnection) (storage.Repository, error) {
|
||||
return gatewaymongo.New(logger, conn)
|
||||
}
|
||||
|
||||
app, err := grpcapp.NewApp(i.logger, paymenttypes.DefaultCardsGatewayID, cfg.Config, i.debug, repoFactory, serviceFactory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i.app = app
|
||||
|
||||
return i.app.Start()
|
||||
}
|
||||
|
||||
func (i *Imp) loadConfig() (*config, error) {
|
||||
data, err := os.ReadFile(i.file)
|
||||
if err != nil {
|
||||
i.logger.Error("Could not read configuration file", zap.String("config_file", i.file), zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := &config{
|
||||
Config: &grpcapp.Config{},
|
||||
}
|
||||
if err := yaml.Unmarshal(data, cfg); err != nil {
|
||||
i.logger.Error("Failed to parse configuration", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cfg.Runtime == nil {
|
||||
cfg.Runtime = &grpcapp.RuntimeConfig{ShutdownTimeoutSeconds: 15}
|
||||
}
|
||||
|
||||
if cfg.GRPC == nil {
|
||||
cfg.GRPC = &routers.GRPCConfig{
|
||||
Network: "tcp",
|
||||
Address: ":50075",
|
||||
EnableReflection: true,
|
||||
EnableHealth: true,
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Metrics == nil {
|
||||
cfg.Metrics = &grpcapp.MetricsConfig{Address: ":9405"}
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func effectiveProviderConfig(primary, legacy gatewayProviderConfig) gatewayProviderConfig {
|
||||
if hasProviderConfig(primary) {
|
||||
return primary
|
||||
}
|
||||
return legacy
|
||||
}
|
||||
|
||||
func hasProviderConfig(cfg gatewayProviderConfig) bool {
|
||||
return strings.TrimSpace(cfg.BaseURL) != "" ||
|
||||
strings.TrimSpace(cfg.BaseURLEnv) != "" ||
|
||||
cfg.ProjectID != 0 ||
|
||||
strings.TrimSpace(cfg.ProjectIDEnv) != "" ||
|
||||
strings.TrimSpace(cfg.SecretKey) != "" ||
|
||||
strings.TrimSpace(cfg.SecretKeyEnv) != "" ||
|
||||
len(cfg.AllowedCurrencies) > 0 ||
|
||||
cfg.RequireCustomerAddress ||
|
||||
cfg.RequestTimeoutSeconds != 0 ||
|
||||
strings.TrimSpace(cfg.StatusSuccess) != "" ||
|
||||
strings.TrimSpace(cfg.StatusProcessing) != "" ||
|
||||
cfg.StrictOperationMode
|
||||
}
|
||||
|
||||
func (i *Imp) resolveProviderConfig(cfg gatewayProviderConfig) (provider.Config, error) {
|
||||
baseURL := strings.TrimSpace(cfg.BaseURL)
|
||||
if env := strings.TrimSpace(cfg.BaseURLEnv); env != "" {
|
||||
if val := strings.TrimSpace(os.Getenv(env)); val != "" {
|
||||
baseURL = val
|
||||
}
|
||||
}
|
||||
|
||||
projectID := cfg.ProjectID
|
||||
if projectID == 0 && strings.TrimSpace(cfg.ProjectIDEnv) != "" {
|
||||
raw := strings.TrimSpace(os.Getenv(cfg.ProjectIDEnv))
|
||||
if raw != "" {
|
||||
if id, err := strconv.ParseInt(raw, 10, 64); err == nil {
|
||||
projectID = id
|
||||
} else {
|
||||
return provider.Config{}, merrors.InvalidArgument("invalid project id in env "+cfg.ProjectIDEnv, "aurora.project_id")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
secret := strings.TrimSpace(cfg.SecretKey)
|
||||
if env := strings.TrimSpace(cfg.SecretKeyEnv); env != "" {
|
||||
if val := strings.TrimSpace(os.Getenv(env)); val != "" {
|
||||
secret = val
|
||||
}
|
||||
}
|
||||
|
||||
timeout := time.Duration(cfg.RequestTimeoutSeconds) * time.Second
|
||||
if timeout <= 0 {
|
||||
timeout = 15 * time.Second
|
||||
}
|
||||
|
||||
statusSuccess := strings.TrimSpace(cfg.StatusSuccess)
|
||||
statusProcessing := strings.TrimSpace(cfg.StatusProcessing)
|
||||
|
||||
return provider.Config{
|
||||
BaseURL: baseURL,
|
||||
ProjectID: projectID,
|
||||
SecretKey: secret,
|
||||
AllowedCurrencies: cfg.AllowedCurrencies,
|
||||
RequireCustomerAddress: cfg.RequireCustomerAddress,
|
||||
RequestTimeout: timeout,
|
||||
StatusSuccess: statusSuccess,
|
||||
StatusProcessing: statusProcessing,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func resolveGatewayDescriptor(cfg gatewayConfig, providerCfg provider.Config) *gatewayv1.GatewayInstanceDescriptor {
|
||||
id := strings.TrimSpace(cfg.ID)
|
||||
if id == "" {
|
||||
id = paymenttypes.DefaultCardsGatewayID
|
||||
}
|
||||
|
||||
network := strings.ToUpper(strings.TrimSpace(cfg.Network))
|
||||
currencies := normalizeCurrencies(cfg.Currencies)
|
||||
if len(currencies) == 0 {
|
||||
currencies = normalizeCurrencies(providerCfg.AllowedCurrencies)
|
||||
}
|
||||
|
||||
enabled := true
|
||||
if cfg.IsEnabled != nil {
|
||||
enabled = *cfg.IsEnabled
|
||||
}
|
||||
|
||||
limits := buildGatewayLimits(cfg.Limits)
|
||||
if limits == nil {
|
||||
limits = &gatewayv1.Limits{MinAmount: "0"}
|
||||
}
|
||||
|
||||
version := strings.TrimSpace(appversion.Version)
|
||||
|
||||
return &gatewayv1.GatewayInstanceDescriptor{
|
||||
Id: id,
|
||||
Rail: gatewayv1.Rail_RAIL_CARD,
|
||||
Network: network,
|
||||
Currencies: currencies,
|
||||
Capabilities: &gatewayv1.RailCapabilities{
|
||||
CanPayOut: true,
|
||||
CanPayIn: false,
|
||||
CanReadBalance: false,
|
||||
CanSendFee: false,
|
||||
RequiresObserveConfirm: true,
|
||||
},
|
||||
Limits: limits,
|
||||
Version: version,
|
||||
IsEnabled: enabled,
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeCurrencies(values []string) []string {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
result := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
clean := strings.ToUpper(strings.TrimSpace(value))
|
||||
if clean == "" || seen[clean] {
|
||||
continue
|
||||
}
|
||||
seen[clean] = true
|
||||
result = append(result, clean)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func buildGatewayLimits(cfg limitsConfig) *gatewayv1.Limits {
|
||||
hasValue := strings.TrimSpace(cfg.MinAmount) != "" ||
|
||||
strings.TrimSpace(cfg.MaxAmount) != "" ||
|
||||
strings.TrimSpace(cfg.PerTxMaxFee) != "" ||
|
||||
strings.TrimSpace(cfg.PerTxMinAmount) != "" ||
|
||||
strings.TrimSpace(cfg.PerTxMaxAmount) != "" ||
|
||||
len(cfg.VolumeLimit) > 0 ||
|
||||
len(cfg.VelocityLimit) > 0 ||
|
||||
len(cfg.CurrencyLimits) > 0
|
||||
if !hasValue {
|
||||
return nil
|
||||
}
|
||||
|
||||
limits := &gatewayv1.Limits{
|
||||
MinAmount: strings.TrimSpace(cfg.MinAmount),
|
||||
MaxAmount: strings.TrimSpace(cfg.MaxAmount),
|
||||
PerTxMaxFee: strings.TrimSpace(cfg.PerTxMaxFee),
|
||||
PerTxMinAmount: strings.TrimSpace(cfg.PerTxMinAmount),
|
||||
PerTxMaxAmount: strings.TrimSpace(cfg.PerTxMaxAmount),
|
||||
}
|
||||
|
||||
if len(cfg.VolumeLimit) > 0 {
|
||||
limits.VolumeLimit = map[string]string{}
|
||||
for key, value := range cfg.VolumeLimit {
|
||||
bucket := strings.TrimSpace(key)
|
||||
amount := strings.TrimSpace(value)
|
||||
if bucket == "" || amount == "" {
|
||||
continue
|
||||
}
|
||||
limits.VolumeLimit[bucket] = amount
|
||||
}
|
||||
}
|
||||
|
||||
if len(cfg.VelocityLimit) > 0 {
|
||||
limits.VelocityLimit = map[string]int32{}
|
||||
for key, value := range cfg.VelocityLimit {
|
||||
bucket := strings.TrimSpace(key)
|
||||
if bucket == "" {
|
||||
continue
|
||||
}
|
||||
limits.VelocityLimit[bucket] = int32(value)
|
||||
}
|
||||
}
|
||||
|
||||
if len(cfg.CurrencyLimits) > 0 {
|
||||
limits.CurrencyLimits = map[string]*gatewayv1.LimitsOverride{}
|
||||
for key, override := range cfg.CurrencyLimits {
|
||||
currency := strings.ToUpper(strings.TrimSpace(key))
|
||||
if currency == "" {
|
||||
continue
|
||||
}
|
||||
limits.CurrencyLimits[currency] = &gatewayv1.LimitsOverride{
|
||||
MaxVolume: strings.TrimSpace(override.MaxVolume),
|
||||
MinAmount: strings.TrimSpace(override.MinAmount),
|
||||
MaxAmount: strings.TrimSpace(override.MaxAmount),
|
||||
MaxFee: strings.TrimSpace(override.MaxFee),
|
||||
MaxOps: int32(override.MaxOps),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return limits
|
||||
}
|
||||
|
||||
type callbackRuntimeConfig struct {
|
||||
Address string
|
||||
Path string
|
||||
AllowedCIDRs []*net.IPNet
|
||||
MaxBodyBytes int64
|
||||
}
|
||||
|
||||
func (i *Imp) resolveCallbackConfig(cfg callbackConfig) (callbackRuntimeConfig, error) {
|
||||
addr := strings.TrimSpace(cfg.Address)
|
||||
if addr == "" {
|
||||
addr = ":8084"
|
||||
}
|
||||
path := strings.TrimSpace(cfg.Path)
|
||||
if path == "" {
|
||||
path = "/" + paymenttypes.DefaultCardsGatewayID + "/callback"
|
||||
}
|
||||
maxBody := cfg.MaxBodyBytes
|
||||
if maxBody <= 0 {
|
||||
maxBody = 1 << 20 // 1MB
|
||||
}
|
||||
|
||||
var cidrs []*net.IPNet
|
||||
for _, raw := range cfg.AllowedCIDRs {
|
||||
clean := strings.TrimSpace(raw)
|
||||
if clean == "" {
|
||||
continue
|
||||
}
|
||||
_, block, err := net.ParseCIDR(clean)
|
||||
if err != nil {
|
||||
i.logger.Warn("Invalid callback allowlist CIDR skipped", zap.String("cidr", clean), zap.Error(err))
|
||||
continue
|
||||
}
|
||||
cidrs = append(cidrs, block)
|
||||
}
|
||||
|
||||
return callbackRuntimeConfig{
|
||||
Address: addr,
|
||||
Path: path,
|
||||
AllowedCIDRs: cidrs,
|
||||
MaxBodyBytes: maxBody,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (i *Imp) startHTTPCallbackServer(svc *auroraservice.Service, cfg callbackRuntimeConfig) error {
|
||||
if svc == nil {
|
||||
return merrors.InvalidArgument("nil service provided for callback server")
|
||||
}
|
||||
if strings.TrimSpace(cfg.Address) == "" {
|
||||
i.logger.Info("Aurora callback server disabled: address is empty")
|
||||
return nil
|
||||
}
|
||||
|
||||
router := chi.NewRouter()
|
||||
router.Post(cfg.Path, func(w http.ResponseWriter, r *http.Request) {
|
||||
log := i.logger.Named("callback_http")
|
||||
log.Debug("Callback request received",
|
||||
zap.String("remote_addr", strings.TrimSpace(r.RemoteAddr)),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("method", r.Method),
|
||||
)
|
||||
|
||||
if len(cfg.AllowedCIDRs) > 0 && !clientAllowed(r, cfg.AllowedCIDRs) {
|
||||
ip := clientIPFromRequest(r)
|
||||
remoteIP := ""
|
||||
if ip != nil {
|
||||
remoteIP = ip.String()
|
||||
}
|
||||
log.Warn("Callback rejected by CIDR allowlist", zap.String("remote_ip", remoteIP))
|
||||
http.Error(w, "forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(r.Body, cfg.MaxBodyBytes))
|
||||
if err != nil {
|
||||
log.Warn("Callback body read failed", zap.Error(err))
|
||||
http.Error(w, "failed to read body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
status, err := svc.ProcessProviderCallback(r.Context(), body)
|
||||
if err != nil {
|
||||
log.Warn("Callback processing failed", zap.Error(err), zap.Int("status", status))
|
||||
http.Error(w, err.Error(), status)
|
||||
return
|
||||
}
|
||||
log.Debug("Callback processed", zap.Int("status", status))
|
||||
w.WriteHeader(status)
|
||||
})
|
||||
|
||||
server := &http.Server{
|
||||
Addr: cfg.Address,
|
||||
Handler: router,
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", cfg.Address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i.http = server
|
||||
|
||||
go func() {
|
||||
if err := server.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
i.logger.Warn("Aurora callback server stopped with error", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
i.logger.Info("Aurora callback server listening", zap.String("address", cfg.Address), zap.String("path", cfg.Path))
|
||||
return nil
|
||||
}
|
||||
|
||||
func clientAllowed(r *http.Request, cidrs []*net.IPNet) bool {
|
||||
if len(cidrs) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
host := clientIPFromRequest(r)
|
||||
if host == nil {
|
||||
return false
|
||||
}
|
||||
for _, block := range cidrs {
|
||||
if block.Contains(host) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func clientIPFromRequest(r *http.Request) net.IP {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
if xfwd := strings.TrimSpace(r.Header.Get("X-Forwarded-For")); xfwd != "" {
|
||||
parts := strings.Split(xfwd, ",")
|
||||
if len(parts) > 0 {
|
||||
if ip := net.ParseIP(strings.TrimSpace(parts[0])); ip != nil {
|
||||
return ip
|
||||
}
|
||||
}
|
||||
}
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return net.ParseIP(host)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package serverimp
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEffectiveProviderConfig(t *testing.T) {
|
||||
primary := gatewayProviderConfig{
|
||||
BaseURL: "https://aurora.local",
|
||||
StrictOperationMode: true,
|
||||
}
|
||||
legacy := gatewayProviderConfig{
|
||||
BaseURL: "https://legacy.local",
|
||||
StrictOperationMode: false,
|
||||
}
|
||||
|
||||
got := effectiveProviderConfig(primary, legacy)
|
||||
if got.BaseURL != primary.BaseURL {
|
||||
t.Fatalf("expected primary provider config to be selected, got %q", got.BaseURL)
|
||||
}
|
||||
if !got.StrictOperationMode {
|
||||
t.Fatalf("expected strict operation mode from primary config")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveProviderConfig_FallsBackToLegacy(t *testing.T) {
|
||||
primary := gatewayProviderConfig{}
|
||||
legacy := gatewayProviderConfig{
|
||||
BaseURL: "https://legacy.local",
|
||||
StrictOperationMode: true,
|
||||
}
|
||||
|
||||
got := effectiveProviderConfig(primary, legacy)
|
||||
if got.BaseURL != legacy.BaseURL {
|
||||
t.Fatalf("expected legacy provider config to be selected, got %q", got.BaseURL)
|
||||
}
|
||||
if !got.StrictOperationMode {
|
||||
t.Fatalf("expected strict operation mode from legacy config")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientIPFromRequest(t *testing.T) {
|
||||
req := &http.Request{
|
||||
Header: http.Header{"X-Forwarded-For": []string{"1.2.3.4, 5.6.7.8"}},
|
||||
RemoteAddr: "9.8.7.6:1234",
|
||||
}
|
||||
ip := clientIPFromRequest(req)
|
||||
if ip == nil || ip.String() != "1.2.3.4" {
|
||||
t.Fatalf("expected forwarded ip, got %v", ip)
|
||||
}
|
||||
|
||||
req = &http.Request{RemoteAddr: "9.8.7.6:1234"}
|
||||
ip = clientIPFromRequest(req)
|
||||
if ip == nil || ip.String() != "9.8.7.6" {
|
||||
t.Fatalf("expected remote addr ip, got %v", ip)
|
||||
}
|
||||
|
||||
req = &http.Request{RemoteAddr: "invalid"}
|
||||
ip = clientIPFromRequest(req)
|
||||
if ip != nil {
|
||||
t.Fatalf("expected nil ip, got %v", ip)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientAllowed(t *testing.T) {
|
||||
_, cidr, err := net.ParseCIDR("10.0.0.0/8")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse cidr: %v", err)
|
||||
}
|
||||
|
||||
allowedReq := &http.Request{RemoteAddr: "10.1.2.3:1234"}
|
||||
if !clientAllowed(allowedReq, []*net.IPNet{cidr}) {
|
||||
t.Fatalf("expected allowed request")
|
||||
}
|
||||
|
||||
deniedReq := &http.Request{RemoteAddr: "8.8.8.8:1234"}
|
||||
if clientAllowed(deniedReq, []*net.IPNet{cidr}) {
|
||||
t.Fatalf("expected denied request")
|
||||
}
|
||||
|
||||
openReq := &http.Request{RemoteAddr: "8.8.8.8:1234"}
|
||||
if !clientAllowed(openReq, nil) {
|
||||
t.Fatalf("expected allow when no cidrs are configured")
|
||||
}
|
||||
}
|
||||
12
api/gateway/aurora/internal/server/server.go
Normal file
12
api/gateway/aurora/internal/server/server.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
serverimp "github.com/tech/sendico/gateway/aurora/internal/server/internal"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/server"
|
||||
)
|
||||
|
||||
// Create constructs the Aurora gateway server implementation.
|
||||
func Create(logger mlogger.Logger, file string, debug bool) (server.Application, error) {
|
||||
return serverimp.Create(logger, file, debug)
|
||||
}
|
||||
Reference in New Issue
Block a user