security+reliability: remove public decrypt endpoint, add panic recovery for goroutines

Security:
- Remove /api/crypto/decrypt from public routes. The endpoint allowed
  anyone to decrypt ciphertext without authentication. Internal callers
  (exchange/model handlers) use the service directly and are behind auth.

Reliability:
- Add safe.Go / safe.GoNamed panic recovery wrapper (safe/go.go).
  Previously 31 goroutines had zero recover() calls — a single panic
  in any trader goroutine would crash the entire process.
- Apply safe.GoNamed to all trader launch paths:
  - StartAll, RestoreRunning, LoadSingleTrader auto-start
  - API handler start/restart endpoints
- Panics are now logged with full stack traces instead of crashing.
This commit is contained in:
shinchan-zhai
2026-03-23 10:15:02 +08:00
parent 25e470dfbb
commit 7c668cd7ef
4 changed files with 81 additions and 15 deletions

View File

@@ -7,6 +7,7 @@ import (
"time"
"nofx/logger"
"nofx/safe"
"nofx/store"
"nofx/trader"
"nofx/trader/aster"
@@ -418,12 +419,12 @@ func (s *Server) handleUpdateTrader(c *gin.Context) {
// If trader was running before, restart it with new config
if wasRunning {
if reloadedTrader, getErr := s.traderManager.GetTrader(traderID); getErr == nil {
go func() {
safe.GoNamed("trader-restart-"+traderID, func() {
logger.Infof("▶️ Restarting trader %s with new config...", traderID)
if runErr := reloadedTrader.Run(); runErr != nil {
logger.Infof("❌ Trader %s runtime error: %v", traderID, runErr)
}
}()
})
}
}
@@ -537,12 +538,12 @@ func (s *Server) handleStartTrader(c *gin.Context) {
}
// Start trader
go func() {
safe.GoNamed("trader-start-"+traderID, func() {
logger.Infof("▶️ Starting trader %s (%s)", traderID, trader.GetName())
if err := trader.Run(); err != nil {
logger.Infof("❌ Trader %s runtime error: %v", trader.GetName(), err)
}
}()
})
// Update running status in database
err = s.store.Trader().UpdateStatus(userID, traderID, true)

View File

@@ -126,10 +126,11 @@ func (s *Server) setupRoutes() {
api.POST("/wallet/validate", s.handleWalletValidate)
api.POST("/wallet/generate", s.handleWalletGenerate)
// Crypto related endpoints (no authentication required, not exposed to bot)
// Crypto public key & config (needed by frontend before login for transport encryption)
api.GET("/crypto/config", s.cryptoHandler.HandleGetCryptoConfig)
api.GET("/crypto/public-key", s.cryptoHandler.HandleGetPublicKey)
api.POST("/crypto/decrypt", s.cryptoHandler.HandleDecryptSensitiveData)
// NOTE: /crypto/decrypt moved behind auth — the public endpoint was a security risk
// (allowed anyone to decrypt captured ciphertext without authentication)
// Public competition data (no authentication required)
s.route(api, "GET", "/traders", "Public trader list", s.handlePublicTraderList)