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
105 changes: 105 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
version: "2"
linters:
default: none
enable:
- asciicheck
- bodyclose
- copyloopvar
- depguard
- dogsled
- errcheck
- goconst
- gocritic
- gocyclo
- godot
- goprintffuncname
- gosec
- govet
- importas
- ineffassign
- misspell
- nakedret
- nilerr
- noctx
- nolintlint
- prealloc
- predeclared
- revive
- rowserrcheck
- staticcheck
- thelper
- unconvert
- unparam
- unused
- whitespace
settings:
depguard:
rules:
main:
allow:
- $gostd
- go.trollit.tech/binance-proxy
- github.com/adshao/go-binance
- github.com/jessevdk/go-flags
- golang.org/x/time/rate
- github.com/samber/slog-multi
- google.golang.org/grpc
- go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
- github.com/go-slog/otelslog
- go.opentelemetry.io/otel
- github.com/stash86/binance-proxy
godot:
scope: toplevel
exclude:
- ^ \+.*
- ^ ANCHOR.*
gosec:
excludes:
- G307
- G108
importas:
no-unaliased: true
nolintlint:
require-specific: true
revive:
rules:
- name: exported
arguments:
- disableStutteringCheck
- name: unused-parameter
disabled: true
tagliatelle:
case:
rules:
json: goCamel
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gci
- gofmt
- goimports
settings:
gci:
sections:
- standard
- default
- prefix(go.trollit.tech/binance-proxy)
goimports:
local-prefixes:
- go.trollit.tech/binance-proxy
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
17 changes: 5 additions & 12 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
# build stage
FROM golang:1.24 AS builder
WORKDIR /app
COPY . .
RUN go mod download
RUN go mod vendor
RUN go mod tidy
RUN CGO_ENABLED=0 go build -o binance-proxy ./cmd/binance-proxy/main.go

# target stage
FROM alpine
COPY --from=builder /app/binance-proxy /go/bin/binance-proxy

COPY bin/binance-proxy /binance-proxy

EXPOSE 8090
EXPOSE 8091
ENTRYPOINT ["/go/bin/binance-proxy"]

ENTRYPOINT ["/binance-proxy"]
24 changes: 23 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ LD_FLAGS := -X main.Version='$(GOLDFLAGS_VERSION)' -X main.Buildtime='$(GOLD
SOURCE_FILES ?= ./internal/... ./pkg/... ./cmd/...
UNAME := $(uname -s)

BIN_DIR := bin
TOOLS_BIN_DIR := $(shell pwd)/$(BIN_DIR)
$(TOOLS_BIN_DIR):
mkdir -p $(TOOLS_BIN_DIR)

GOLANGCI_VER := 2.5.0
GOLANGCI_BIN := golangci
GOLANGCI := $(TOOLS_BIN_DIR)/$(GOLANGCI_BIN)-$(GOLANGCI_VER)

$(info GOLDFLAGS_VERSION=$(GOLDFLAGS_VERSION))
$(info GOLDFLAGS_BUILD_TIME=$(GOLDFLAGS_BUILD_TIME))
$(info LD_FLAGS=$(LD_FLAGS))
Expand Down Expand Up @@ -63,7 +72,9 @@ vet: ### Vet

### Lint
.PHONY: lint
lint: fmt vet
lint: $(GOLANGCI) fmt vet
$(GOLANGCI) run -c .golangci.yml -v


### Clean test
.PHONY: test-clean
Expand All @@ -77,3 +88,14 @@ test: lint ### Run tests
.PHONY: cover
cover: test ### Run tests and generate coverage
@go tool cover -html=cover.out -o=cover.html


$(GOLANGCI): $(TOOLS_BIN_DIR)
ifeq (,$(wildcard $(GOLANGCI)))
mkdir -p /tmp/golangci && \
cd /tmp/golangci && \
curl -L https://github.com/golangci/golangci-lint/releases/download/v$(GOLANGCI_VER)/golangci-lint-$(GOLANGCI_VER)-linux-amd64.tar.gz -o golangci-lint-$(GOLANGCI_VER)-linux-amd64.tar.gz && \
tar xvf golangci-lint-$(GOLANGCI_VER)-linux-amd64.tar.gz && \
mv golangci-lint-$(GOLANGCI_VER)-linux-amd64/golangci-lint $(GOLANGCI) && \
ln -sf $(GOLANGCI) $(TOOLS_BIN_DIR)/$(GOLANGCI_BIN)
endif
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ The status endpoint provides comprehensive information about the proxy service:
```json
{
"proxy_status": {
"service": "binance-proxy",
"service": "github.com/stash86/binance-proxy",
"healthy": true,
"start_time": "2025-06-15T10:30:00Z",
"uptime": "2h15m30s",
Expand Down Expand Up @@ -535,7 +535,6 @@ By submitting a pull request to this project, you agree to license your contribu
- [go-binance](https://github.com/adshao/go-binance/blob/master/LICENSE)

- [go-flags](https://github.com/jessevdk/go-flags/blob/master/LICENSE)
- [logrus](https://github.com/sirupsen/logrus/blob/master/LICENSE)
- [go-time](https://cs.opensource.google/go/x/time/+/master:LICENSE)
- [go-simplejson](https://github.com/bitly/go-simplejson/blob/master/LICENSE)
- [websocket](https://github.com/gorilla/websocket/blob/master/LICENSE)
Expand Down
105 changes: 62 additions & 43 deletions cmd/binance-proxy/main.go
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
package main

import (
"binance-proxy/internal/handler"
"binance-proxy/internal/logcache"
"binance-proxy/internal/service"
"context"
"errors"
"fmt"
stdlog "log"
"log/slog"
"net/http"
_ "net/http/pprof"
"os"
"os/signal"
"syscall"
"time"

_ "net/http/pprof"

"github.com/jessevdk/go-flags"
log "github.com/sirupsen/logrus"
"github.com/stash86/binance-proxy/internal/handler"
"github.com/stash86/binance-proxy/internal/logcache"
"github.com/stash86/binance-proxy/internal/service"
)

func startProxy(ctx context.Context, port int, class service.Class, disablefakekline bool, alwaysshowforwards bool) {
func startProxy(ctx context.Context, logger *slog.Logger, bd *service.BanDetector, port int, class service.Class, disablefakekline bool, alwaysshowforwards bool, errChan chan<- error) {
mux := http.NewServeMux()
address := fmt.Sprintf(":%d", port)
mux.HandleFunc("/", handler.NewHandler(ctx, class, !disablefakekline, alwaysshowforwards))
mux.HandleFunc("/", handler.NewHandler(ctx, logger, bd, class, !disablefakekline, alwaysshowforwards))

// Create an HTTP server with a custom ErrorLog that suppresses repeated lines
srv := &http.Server{
Expand All @@ -38,9 +38,10 @@ func startProxy(ctx context.Context, port int, class service.Class, disablefakek
),
}

log.Infof("%s websocket proxy starting on port %d.", class, port)
if err := srv.ListenAndServe(); err != nil {
log.Fatalf("%s websocket proxy start failed (error: %s).", class, err)
logger.Info("websocket proxy starting", "class", class, "port", port)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Error("websocket proxy start failed", "class", class, "error", err)
errChan <- fmt.Errorf("%s proxy failed: %w", class, err)
}
}

Expand Down Expand Up @@ -74,75 +75,93 @@ var (
)

func main() {
log.SetFormatter(&log.TextFormatter{
DisableColors: true,
FullTimestamp: true,
})
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
slog.SetDefault(logger)

// Route logcache output through logrus for consistent formatting/levels
logcache.SetLoggerHook(func(level, msg string) {
switch level {
case "warn":
log.Warn(msg)
logger.Warn(msg)
case "error":
log.Error(msg)
logger.Error(msg)
case "info":
log.Info(msg)
logger.Info(msg)
default:
log.Print(msg)
logger.Info(msg)
}
})
logcache.SetWriterHook(func(msg string) {
// net/http ErrorLog messages typically include trailing newlines
if len(msg) > 0 && msg[len(msg)-1] == '\n' {
msg = msg[:len(msg)-1]
}
log.Warnf("http: %s", msg)

logger.Warn("http request", "msg", msg)
})

log.Infof("Binance proxy version %s, build time %s", Version, Buildtime)
logger.Info("Binance proxy version", "version", Version, "build", Buildtime)

if _, err := parser.Parse(); err != nil {
if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp {
os.Exit(0)
} else {
log.Fatalf("%s - %s", err, flagsErr.Type)
logger.Error("failed parsing flags", "error", err, "type", flagsErr.Type)
}
}

if len(config.Verbose) >= 2 {
log.SetLevel(log.TraceLevel)
} else if len(config.Verbose) == 1 {
log.SetLevel(log.DebugLevel)
} else {
log.SetLevel(log.InfoLevel)
}

if log.GetLevel() > log.InfoLevel {
log.Infof("Set level to %s", log.GetLevel())
}

if config.DisableSpot && config.DisableFutures {
log.Fatal("can't start if both SPOT and FUTURES are disabled!")
logger.Error("can't start if both SPOT and FUTURES are disabled!")
os.Exit(1)
}

if !config.DisableFakeKline {
log.Infof("Fake candles are enabled for faster processing, the feature can be disabled with --disable-fake-candles or -c")
logger.Info("Fake candles are enabled for faster processing, the feature can be disabled with --disable-fake-candles or -c")
}

if config.AlwaysShowForwards {
log.Infof("Always show forwards is enabled, all API requests, that can't be served from websockets cached will be logged.")
logger.Info("Always show forwards is enabled, all API requests, that can't be served from websockets cached will be logged.")
}

go handleSignal()

// Channel to collect errors from proxy goroutines
errChan := make(chan error, 2) // Buffer for up to 2 proxies
var proxyCount int

banDetector := service.NewBanDetector(logger)

if !config.DisableSpot {
go startProxy(ctx, config.SpotAddress, service.SPOT, config.DisableFakeKline, config.AlwaysShowForwards)
proxyCount++
go startProxy(ctx, logger, banDetector, config.SpotAddress, service.SPOT, config.DisableFakeKline, config.AlwaysShowForwards, errChan)
}

if !config.DisableFutures {
go startProxy(ctx, config.FuturesAddress, service.FUTURES, config.DisableFakeKline, config.AlwaysShowForwards)
proxyCount++
go startProxy(ctx, logger, banDetector, config.FuturesAddress, service.FUTURES, config.DisableFakeKline, config.AlwaysShowForwards, errChan)
}
<-ctx.Done()

log.Info("SIGINT received, aborting ...")
// Wait for either context cancellation or errors from proxies
var collectedErrors []error
done := false

for !done {
select {
case <-ctx.Done():
logger.Info("SIGINT received, aborting ...")
done = true
case err := <-errChan:
if err != nil {
collectedErrors = append(collectedErrors, err)
// If all proxies have failed, exit
if len(collectedErrors) >= proxyCount {
done = true
}
}
}
}

// Log any collected errors
if len(collectedErrors) > 0 {
combinedErr := errors.Join(collectedErrors...)
logger.Error("Proxy errors occurred", "error", combinedErr)
}
}
7 changes: 2 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
module binance-proxy
module github.com/stash86/binance-proxy

go 1.23.0

toolchain go1.24.4
go 1.25.3

require (
github.com/adshao/go-binance/v2 v2.8.2
github.com/jessevdk/go-flags v1.6.1
github.com/sirupsen/logrus v1.9.3
golang.org/x/time v0.12.0
)

Expand Down
Loading