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
19 changes: 12 additions & 7 deletions services/asset/httpimpl/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,12 +273,14 @@ func New(logger ulogger.Logger, tSettings *settings.Settings, repo *repository.R
e.GET(h.settings.StatsPrefix+"*", AdaptStdHandler(gocore.HandleOther))
}

// Create auth handler for protecting admin endpoints (used regardless of dashboard state)
authHandler := dashboard.NewAuthHandler(h.logger, h.settings)

if h.settings.Dashboard.Enabled {
// Initialize dashboard with settings
dashboard.InitDashboard(h.settings)

// Apply authentication middleware for all POST endpoints
authHandler := dashboard.NewAuthHandler(h.logger, h.settings)
apiGroup.Use(authHandler.PostAuthMiddleware)

// Register dashboard-compatible API routes that need auth protection
Expand Down Expand Up @@ -327,12 +329,16 @@ func New(logger ulogger.Logger, tSettings *settings.Settings, repo *repository.R
pathFsmStates = "/fsm/states"
)

// Register FSM API endpoints
// Register FSM read-only endpoints (no auth required)
apiGroup.GET(pathFsmState, fsmHandler.GetFSMState)
apiGroup.POST(pathFsmState, fsmHandler.SendFSMEvent)
apiGroup.GET(pathFsmEvents, fsmHandler.GetFSMEvents)
apiGroup.GET(pathFsmStates, fsmHandler.GetFSMStates)

// Register FSM write endpoint with auth (requires authentication regardless of dashboard state)
apiAdminGroup := e.Group(apiPrefix)
apiAdminGroup.Use(authHandler.RequireAuthMiddleware)
apiAdminGroup.POST(pathFsmState, fsmHandler.SendFSMEvent)

// Add OPTIONS handlers for CORS preflight requests
apiGroup.OPTIONS(pathFsmState, func(c echo.Context) error {
return c.NoContent(http.StatusOK)
Expand All @@ -347,9 +353,9 @@ func New(logger ulogger.Logger, tSettings *settings.Settings, repo *repository.R
// Create and register block handler for block operations
blockHandler := NewBlockHandler(repo.BlockchainClient, repo.BlockvalidationClient, logger)

// Register block invalidation/revalidation endpoints
apiGroup.POST("/block/invalidate", blockHandler.InvalidateBlock)
apiGroup.POST("/block/revalidate", blockHandler.RevalidateBlock)
// Register block invalidation/revalidation endpoints (requires authentication)
apiAdminGroup.POST("/block/invalidate", blockHandler.InvalidateBlock)
apiAdminGroup.POST("/block/revalidate", blockHandler.RevalidateBlock)
apiGroup.GET("/blocks/invalid", blockHandler.GetLastNInvalidBlocks)

// Register catchup status endpoint
Expand All @@ -363,7 +369,6 @@ func New(logger ulogger.Logger, tSettings *settings.Settings, repo *repository.R

// Register settings handler for settings portal (always requires authentication)
settingsHandler := NewSettingsHandler(tSettings, logger)
authHandler := dashboard.NewAuthHandler(logger, tSettings)
apiSettingsGroup := e.Group(apiPrefix + "/settings")
apiSettingsGroup.Use(authHandler.RequireAuthMiddleware)
apiSettingsGroup.GET("", settingsHandler.GetSettings)
Expand Down
12 changes: 12 additions & 0 deletions services/rpc/Server.go
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,18 @@ handled:

// Execute the handler in a goroutine
go func() {
defer func() {
if r := recover(); r != nil {
s.logger.Errorf("Recovered from panic in RPC handler '%s': %v", cmd.method, r)
select {
case <-timeoutCtx.Done():
return
default:
resultCh <- nil
errCh <- errors.NewServiceError("internal error: RPC handler panicked")
}
}
}()
result, err := handler(timeoutCtx, s, cmd.cmd, closeChan)
select {
case <-timeoutCtx.Done():
Expand Down
35 changes: 35 additions & 0 deletions services/rpc/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1067,3 +1067,38 @@ func TestStart(t *testing.T) {
assert.Equal(t, int32(3), atomic.LoadInt32(&s.started))
})
}

func TestStandardCmdResult_PanicRecovery(t *testing.T) {
logger := mocklogger.NewTestLogger()

s := &RPCServer{
logger: logger,
settings: &settings.Settings{
RPC: settings.RPCSettings{
RPCTimeout: 5 * time.Second,
},
},
requestProcessShutdown: make(chan struct{}, 1),
}

err := s.Init(context.Background())
require.NoError(t, err)

// Inject a panicking handler
rpcHandlers["__test_panic"] = func(_ context.Context, _ *RPCServer, _ interface{}, _ <-chan struct{}) (interface{}, error) {
panic("test panic in handler")
}
defer delete(rpcHandlers, "__test_panic")

closeChan := make(chan struct{})
parsedCmd := &parsedRPCCmd{
method: "__test_panic",
cmd: nil,
}

result, err := s.standardCmdResult(context.Background(), parsedCmd, closeChan)

require.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "internal error: RPC handler panicked")
}
10 changes: 7 additions & 3 deletions services/subtreevalidation/check_block_subtrees.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,13 @@ func (u *Server) CheckBlockSubtrees(ctx context.Context, request *subtreevalidat
} else if block.TransactionCount > 0 && len(block.Subtrees) > 0 {
// Calculate exact txs per subtree using block metadata
txsPerSubtree := int(block.TransactionCount / uint64(len(block.Subtrees)))
subtreesBatchSize = txBatchSize / txsPerSubtree
if subtreesBatchSize == 0 {
subtreesBatchSize = 1 // Minimum 1 subtree per batch
if txsPerSubtree == 0 {
subtreesBatchSize = 1
} else {
subtreesBatchSize = txBatchSize / txsPerSubtree
if subtreesBatchSize == 0 {
subtreesBatchSize = 1 // Minimum 1 subtree per batch
}
}
} else {
// Fallback if metadata not available (shouldn't happen)
Expand Down
23 changes: 22 additions & 1 deletion settings/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ var (
metadataCacheOnce sync.Once
)

// sensitiveKeys contains setting keys whose values must be redacted in exported metadata.
var sensitiveKeys = map[string]bool{
"rpc_pass": true,
"rpc_limit_pass": true,
"p2p_private_key": true,
"coinbase_p2p_private_key": true,
"alert_p2p_private_key": true,
"coinbase_wallet_private_key": true,
"miner_wallet_private_keys": true,
"coinbaseDBUserPwd": true,
"slack_token": true,
"grpc_admin_api_key": true,
}

const redactedValue = "********"

// ExportMetadata exports all settings with their metadata for the settings portal.
// It uses reflection to extract struct tags on first call (cached), then combines
// with current runtime values on each subsequent call.
Expand All @@ -46,12 +62,17 @@ func (s *Settings) ExportMetadata() *SettingsRegistry {
// Get current value using cached field path
currentVal := getValueAtPath(val, entry.ValuePath)

currentValueStr := formatValue(currentVal)
if sensitiveKeys[entry.Key] && currentValueStr != "" {
currentValueStr = redactedValue
}

settings = append(settings, SettingMetadata{
Key: entry.Key,
Name: entry.Name,
Type: entry.Type,
DefaultValue: entry.DefaultValue,
CurrentValue: formatValue(currentVal),
CurrentValue: currentValueStr,
Description: entry.Description,
LongDescription: entry.LongDescription,
Category: entry.Category,
Expand Down
42 changes: 42 additions & 0 deletions settings/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,48 @@ func TestExportMetadata_Caching(t *testing.T) {
require.Equal(t, "def456", registry2.Commit)
}

func TestExportMetadata_SecretRedaction(t *testing.T) {
settings := &Settings{
Version: "1.0.0",
Commit: "abc123",
ChainCfgParams: &chaincfg.MainNetParams,
RPC: RPCSettings{
RPCPass: "super-secret-password",
RPCLimitPass: "limited-secret-password",
},
P2P: P2PSettings{
PrivateKey: "deadbeef1234567890",
},
}

registry := settings.ExportMetadata()
require.NotNil(t, registry)

settingsMap := make(map[string]SettingMetadata)
for _, s := range registry.Settings {
settingsMap[s.Key] = s
}

// Verify sensitive keys are redacted
for key := range sensitiveKeys {
if setting, ok := settingsMap[key]; ok {
if setting.CurrentValue != "" {
require.Equal(t, redactedValue, setting.CurrentValue,
"sensitive key %q should be redacted", key)
}
}
}

// Verify specific keys we set are redacted
require.Equal(t, redactedValue, settingsMap["rpc_pass"].CurrentValue)
require.Equal(t, redactedValue, settingsMap["rpc_limit_pass"].CurrentValue)
require.Equal(t, redactedValue, settingsMap["p2p_private_key"].CurrentValue)

// Verify non-sensitive keys are NOT redacted
logLevel := settingsMap["logLevel"]
require.NotEqual(t, redactedValue, logLevel.CurrentValue)
}

func mustParseURL(rawURL string) *url.URL {
u, err := url.Parse(rawURL)
if err != nil {
Expand Down
12 changes: 3 additions & 9 deletions ui/dashboard/authHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,8 @@ func (h *AuthHandler) CheckAuth(r *http.Request) bool {
authsha := sha256.Sum256([]byte(authHeader))
isValid := authsha == h.authsha

// Add debug logging
if !isValid {
expectedAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", rpcUser, rpcPass)))
expectedHash := sha256.Sum256([]byte(expectedAuth))

h.logger.Debugf("Auth validation failed. Got: %x, Expected: %x", authsha, expectedHash)
h.logger.Debugf("Username comparison: '%s' vs '%s'", username, rpcUser)
h.logger.Debugf("Password comparison: '%s' vs '%s'", password, rpcPass)
h.logger.Debugf("Auth validation failed for user")
}

return isValid
Expand Down Expand Up @@ -212,9 +206,9 @@ func (h *AuthHandler) LogoutHandler(c echo.Context) error {
func (h *AuthHandler) CheckAuthHandler(c echo.Context) error {
h.logger.Debugf("Auth check request received")

// Log all cookies for debugging
// Log cookie names for debugging (values omitted for security)
for _, cookie := range c.Cookies() {
h.logger.Debugf("Cookie found: %s = %s", cookie.Name, cookie.Value)
h.logger.Debugf("Cookie found: %s", cookie.Name)
}

// Log auth header if present
Expand Down
22 changes: 9 additions & 13 deletions util/grpc_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,9 +409,8 @@ func loadTLSCredentials(connectionData *ConnectionOptions, isServer bool) (crede

return credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
//nolint:gosec // G402: TLS InsecureSkipVerify set true. (gosec)
InsecureSkipVerify: true,
ClientAuth: tls.RequireAnyClientCert,
ClientAuth: tls.RequireAnyClientCert,
MinVersion: tls.VersionTLS12,
}), nil
} else {
// Load the server's CA certificate from disk
Expand All @@ -430,9 +429,8 @@ func loadTLSCredentials(connectionData *ConnectionOptions, isServer bool) (crede

return credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
//nolint:gosec // G402: TLS InsecureSkipVerify set true. (gosec)
InsecureSkipVerify: true,
RootCAs: caCertPool,
RootCAs: caCertPool,
MinVersion: tls.VersionTLS12,
}), nil
}
case 3:
Expand All @@ -454,10 +452,9 @@ func loadTLSCredentials(connectionData *ConnectionOptions, isServer bool) (crede

return credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
//nolint:gosec // G402: TLS InsecureSkipVerify set true. (gosec)
InsecureSkipVerify: true,
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: caCertPool,
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: caCertPool,
MinVersion: tls.VersionTLS12,
}), nil
} else {
// Load the server's CA certificate from disk
Expand All @@ -476,9 +473,8 @@ func loadTLSCredentials(connectionData *ConnectionOptions, isServer bool) (crede

return credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
//nolint:gosec // G402: TLS InsecureSkipVerify set true. (gosec)
InsecureSkipVerify: true,
RootCAs: caCertPool,
RootCAs: caCertPool,
MinVersion: tls.VersionTLS12,
}), nil
}
}
Expand Down
Loading
Loading