diff --git a/cmd/mining/cmd/remote.go b/cmd/mining/cmd/remote.go index b73f043..b462ec9 100644 --- a/cmd/mining/cmd/remote.go +++ b/cmd/mining/cmd/remote.go @@ -11,10 +11,10 @@ import ( ) var ( - remoteController *node.Controller - peerTransport *node.Transport - remoteControllerOnce sync.Once - remoteControllerErr error + cachedRemoteController *node.Controller + cachedPeerTransport *node.Transport + loadRemoteControllerOnce sync.Once + cachedRemoteControllerErr error ) // remote status peer-19f3, remote start peer-19f3 --type xmrig, and remote ping peer-19f3 --count 4 live under this command group. @@ -325,29 +325,29 @@ func init() { // getController() returns the cached controller after `node init` succeeds. func getController() (*node.Controller, error) { - remoteControllerOnce.Do(func() { + loadRemoteControllerOnce.Do(func() { nodeManager, err := getNodeManager() if err != nil { - remoteControllerErr = fmt.Errorf("failed to get node manager: %w", err) + cachedRemoteControllerErr = fmt.Errorf("failed to get node manager: %w", err) return } if !nodeManager.HasIdentity() { - remoteControllerErr = fmt.Errorf("no node identity found. Run `node init` first") + cachedRemoteControllerErr = fmt.Errorf("no node identity found. Run `node init` first") return } peerRegistry, err := getPeerRegistry() if err != nil { - remoteControllerErr = fmt.Errorf("failed to get peer registry: %w", err) + cachedRemoteControllerErr = fmt.Errorf("failed to get peer registry: %w", err) return } transportConfig := node.DefaultTransportConfig() - peerTransport = node.NewTransport(nodeManager, peerRegistry, transportConfig) - remoteController = node.NewController(nodeManager, peerRegistry, peerTransport) + cachedPeerTransport = node.NewTransport(nodeManager, peerRegistry, transportConfig) + cachedRemoteController = node.NewController(nodeManager, peerRegistry, cachedPeerTransport) }) - return remoteController, remoteControllerErr + return cachedRemoteController, cachedRemoteControllerErr } // findPeerByPartialID("peer-19f3") returns the peer whose ID starts with `peer-19f3`. @@ -364,13 +364,13 @@ func findPeerByPartialID(partialID string) *node.Peer { } // peerRegistry.ListPeers() falls back to partial IDs such as `peer-19`. - for _, p := range peerRegistry.ListPeers() { - if strings.HasPrefix(p.ID, partialID) { - return p + for _, peer := range peerRegistry.ListPeers() { + if strings.HasPrefix(peer.ID, partialID) { + return peer } // Also try matching by name - if strings.EqualFold(p.Name, partialID) { - return p + if strings.EqualFold(peer.Name, partialID) { + return peer } } diff --git a/cmd/mining/cmd/serve.go b/cmd/mining/cmd/serve.go index f03a719..744cb3e 100644 --- a/cmd/mining/cmd/serve.go +++ b/cmd/mining/cmd/serve.go @@ -17,9 +17,9 @@ import ( ) var ( - host string - port int - namespace string + serveHost string + servePort int + apiNamespace string ) // mining serve starts the HTTP API and interactive shell. @@ -31,7 +31,7 @@ var serveCmd = &cobra.Command{ ctx, cancel := context.WithCancel(context.Background()) defer cancel() - displayHostName := host + displayHostName := serveHost if displayHostName == "0.0.0.0" { var err error displayHostName, err = getLocalIP() @@ -39,13 +39,13 @@ var serveCmd = &cobra.Command{ displayHostName = "localhost" } } - displayAddress := fmt.Sprintf("%s:%d", displayHostName, port) - listenAddress := fmt.Sprintf("%s:%d", host, port) + displayAddress := fmt.Sprintf("%s:%d", displayHostName, servePort) + listenAddress := fmt.Sprintf("%s:%d", serveHost, servePort) // manager := getManager() shares the same miner lifecycle state across `mining start`, `mining stop`, and `mining serve`. manager := getManager() - service, err := mining.NewService(manager, listenAddress, displayAddress, namespace) + service, err := mining.NewService(manager, listenAddress, displayAddress, apiNamespace) if err != nil { return fmt.Errorf("failed to create new service: %w", err) } @@ -64,8 +64,8 @@ var serveCmd = &cobra.Command{ // go func() { fmt.Print(">> ") } // keeps the interactive shell responsive while the API serves requests. go func() { - fmt.Printf("Mining service started on http://%s:%d\n", displayHostName, port) - fmt.Printf("Swagger documentation is available at http://%s:%d%s/index.html\n", displayHostName, port, service.SwaggerUIPath) + fmt.Printf("Mining service started on http://%s:%d\n", displayHostName, servePort) + fmt.Printf("Swagger documentation is available at http://%s:%d%s/index.html\n", displayHostName, servePort, service.SwaggerUIPath) fmt.Println("Entering interactive shell. Type 'exit' or 'quit' to stop.") fmt.Print(">> ") @@ -215,9 +215,9 @@ var serveCmd = &cobra.Command{ } func init() { - serveCmd.Flags().StringVar(&host, "host", "127.0.0.1", "Host to listen on") - serveCmd.Flags().IntVarP(&port, "port", "p", 9090, "Port to listen on") - serveCmd.Flags().StringVarP(&namespace, "namespace", "n", "/api/v1/mining", "API namespace for the swagger UI") + serveCmd.Flags().StringVar(&serveHost, "host", "127.0.0.1", "Host to listen on") + serveCmd.Flags().IntVarP(&servePort, "port", "p", 9090, "Port to listen on") + serveCmd.Flags().StringVarP(&apiNamespace, "namespace", "n", "/api/v1/mining", "API namespace for the swagger UI") rootCmd.AddCommand(serveCmd) } diff --git a/cmd/mining/cmd/simulate.go b/cmd/mining/cmd/simulate.go index bc7fb3f..ccfc64f 100644 --- a/cmd/mining/cmd/simulate.go +++ b/cmd/mining/cmd/simulate.go @@ -49,7 +49,7 @@ Available presets: ctx, cancel := context.WithCancel(context.Background()) defer cancel() - displayHost := host + displayHost := serveHost if displayHost == "0.0.0.0" { var err error displayHost, err = getLocalIP() @@ -57,8 +57,8 @@ Available presets: displayHost = "localhost" } } - displayAddress := fmt.Sprintf("%s:%d", displayHost, port) - listenAddress := fmt.Sprintf("%s:%d", host, port) + displayAddress := fmt.Sprintf("%s:%d", displayHost, servePort) + listenAddress := fmt.Sprintf("%s:%d", serveHost, servePort) // manager := mining.NewManagerForSimulation() // keeps simulated miners isolated from the real autostart state. manager := mining.NewManagerForSimulation() @@ -82,8 +82,8 @@ Available presets: simulatedConfig.Name, simulatedConfig.Algorithm, simulatedConfig.BaseHashrate) } - // service, err := mining.NewService(manager, listenAddr, displayAddr, namespace) // serves the simulator on http://127.0.0.1:9090. - service, err := mining.NewService(manager, listenAddress, displayAddress, namespace) + // service, err := mining.NewService(manager, listenAddress, displayAddress, apiNamespace) // serves the simulator on http://127.0.0.1:9090. + service, err := mining.NewService(manager, listenAddress, displayAddress, apiNamespace) if err != nil { return fmt.Errorf("failed to create new service: %w", err) } @@ -97,8 +97,8 @@ Available presets: }() fmt.Printf("\n=== SIMULATION MODE ===\n") - fmt.Printf("Mining service started on http://%s:%d\n", displayHost, port) - fmt.Printf("Swagger documentation is available at http://%s:%d%s/swagger/index.html\n", displayHost, port, namespace) + fmt.Printf("Mining service started on http://%s:%d\n", displayHost, servePort) + fmt.Printf("Swagger documentation is available at http://%s:%d%s/swagger/index.html\n", displayHost, servePort, apiNamespace) fmt.Printf("\nSimulating %d miner(s). Press Ctrl+C to stop.\n", simulatedMinerCount) fmt.Printf("Note: All data is simulated - no actual mining is occurring.\n\n") @@ -161,9 +161,9 @@ func init() { simulateCmd.Flags().IntVar(&simulationHashrate, "hashrate", 0, "Custom base hashrate (overrides preset)") simulateCmd.Flags().StringVar(&simulationAlgorithm, "algorithm", "", "Custom algorithm (overrides preset)") - simulateCmd.Flags().StringVar(&host, "host", "127.0.0.1", "Host to listen on") - simulateCmd.Flags().IntVarP(&port, "port", "p", 9090, "Port to listen on") - simulateCmd.Flags().StringVarP(&namespace, "namespace", "n", "/api/v1/mining", "API namespace") + simulateCmd.Flags().StringVar(&serveHost, "host", "127.0.0.1", "Host to listen on") + simulateCmd.Flags().IntVarP(&servePort, "port", "p", 9090, "Port to listen on") + simulateCmd.Flags().StringVarP(&apiNamespace, "namespace", "n", "/api/v1/mining", "API namespace") rootCmd.AddCommand(simulateCmd) } diff --git a/pkg/mining/mining.go b/pkg/mining/mining.go index 5a3f69d..affedbd 100644 --- a/pkg/mining/mining.go +++ b/pkg/mining/mining.go @@ -66,7 +66,7 @@ type SystemInfo struct { InstalledMinersInfo []*InstallationDetails `json:"installed_miners_info"` } -// Config{Miner: "xmrig", Pool: "stratum+tcp://pool.example.com:3333", Wallet: "4ABC...", Threads: 4} +// Config{Miner: "xmrig", Pool: "stratum+tcp://pool.example.com:3333", Wallet: "44Affq5kSiGBoZ...", Threads: 4} type Config struct { Miner string `json:"miner"` Pool string `json:"pool"` @@ -131,7 +131,7 @@ type Config struct { Seed string `json:"seed,omitempty"` Hash string `json:"hash,omitempty"` NoDMI bool `json:"noDMI,omitempty"` - // GPU-specific options (for XMRig dual CPU+GPU mining) + // Config{GPUEnabled: true, GPUPool: "stratum+ssl://gpu.pool.example.com:4444", GPUWallet: "44Affq5kSiGBoZ..."} enables the GPU mining fields. GPUEnabled bool `json:"gpuEnabled,omitempty"` // Enable GPU mining GPUPool string `json:"gpuPool,omitempty"` // Separate pool for GPU (can differ from CPU) GPUWallet string `json:"gpuWallet,omitempty"` // Wallet for GPU pool (defaults to main Wallet) @@ -147,61 +147,61 @@ type Config struct { } // if err := config.Validate(); err != nil { return err } -func (c *Config) Validate() error { +func (config *Config) Validate() error { // Pool URL validation - if c.Pool != "" { + if config.Pool != "" { // Block shell metacharacters in pool URL - if containsShellChars(c.Pool) { + if containsShellChars(config.Pool) { return ErrInvalidConfig("pool URL contains invalid characters") } } // Wallet validation (basic alphanumeric + special chars allowed in addresses) - if c.Wallet != "" { - if containsShellChars(c.Wallet) { + if config.Wallet != "" { + if containsShellChars(config.Wallet) { return ErrInvalidConfig("wallet address contains invalid characters") } // Most wallet addresses are 40-128 chars - if len(c.Wallet) > 256 { + if len(config.Wallet) > 256 { return ErrInvalidConfig("wallet address too long (max 256 chars)") } } // Thread count validation - if c.Threads < 0 { + if config.Threads < 0 { return ErrInvalidConfig("threads cannot be negative") } - if c.Threads > 1024 { + if config.Threads > 1024 { return ErrInvalidConfig("threads value too high (max 1024)") } // Algorithm validation (alphanumeric, dash, slash) - if c.Algo != "" { - if !isValidAlgo(c.Algo) { + if config.Algo != "" { + if !isValidAlgo(config.Algo) { return ErrInvalidConfig("algorithm name contains invalid characters") } } // Intensity validation - if c.Intensity < 0 || c.Intensity > 100 { + if config.Intensity < 0 || config.Intensity > 100 { return ErrInvalidConfig("intensity must be between 0 and 100") } - if c.GPUIntensity < 0 || c.GPUIntensity > 100 { + if config.GPUIntensity < 0 || config.GPUIntensity > 100 { return ErrInvalidConfig("GPU intensity must be between 0 and 100") } // Donate level validation - if c.DonateLevel < 0 || c.DonateLevel > 100 { + if config.DonateLevel < 0 || config.DonateLevel > 100 { return ErrInvalidConfig("donate level must be between 0 and 100") } // CLIArgs validation - check for shell metacharacters - if c.CLIArgs != "" { - if containsShellChars(c.CLIArgs) { + if config.CLIArgs != "" { + if containsShellChars(config.CLIArgs) { return ErrInvalidConfig("CLI arguments contain invalid characters") } // Limit length to prevent abuse - if len(c.CLIArgs) > 1024 { + if len(config.CLIArgs) > 1024 { return ErrInvalidConfig("CLI arguments too long (max 1024 chars)") } } diff --git a/pkg/mining/service.go b/pkg/mining/service.go index 9fd600d..39d6c91 100644 --- a/pkg/mining/service.go +++ b/pkg/mining/service.go @@ -71,8 +71,8 @@ func sanitizeErrorDetails(details string) string { return "" } -// respondWithError(c, http.StatusNotFound, ErrCodeMinerNotFound, "xmrig not found", "process exited with code 1") -func respondWithError(c *gin.Context, status int, code string, message string, details string) { +// respondWithError(requestContext, http.StatusNotFound, ErrCodeMinerNotFound, "xmrig not found", "process exited with code 1") +func respondWithError(requestContext *gin.Context, status int, code string, message string, details string) { apiError := APIError{ Code: code, Message: message, @@ -80,7 +80,7 @@ func respondWithError(c *gin.Context, status int, code string, message string, d Retryable: isRetryableError(status), } - // respondWithError(c, http.StatusServiceUnavailable, ErrCodeServiceUnavailable, "service unavailable", "database offline") adds a retry suggestion. + // respondWithError(requestContext, http.StatusServiceUnavailable, ErrCodeServiceUnavailable, "service unavailable", "database offline") adds a retry suggestion. switch code { case ErrCodeMinerNotFound: apiError.Suggestion = "Check the miner name or install the miner first" @@ -97,12 +97,12 @@ func respondWithError(c *gin.Context, status int, code string, message string, d apiError.Retryable = true } - c.JSON(status, apiError) + requestContext.JSON(status, apiError) } -// respondWithMiningError(c, ErrMinerNotFound("xmrig")) -// respondWithMiningError(c, ErrInternal("failed to read config").WithCause(err)) -func respondWithMiningError(c *gin.Context, err *MiningError) { +// respondWithMiningError(requestContext, ErrMinerNotFound("xmrig")) +// respondWithMiningError(requestContext, ErrInternal("failed to read config").WithCause(err)) +func respondWithMiningError(requestContext *gin.Context, err *MiningError) { details := "" if err.Cause != nil { details = err.Cause.Error() @@ -122,7 +122,7 @@ func respondWithMiningError(c *gin.Context, err *MiningError) { Retryable: err.Retryable, } - c.JSON(err.StatusCode(), apiError) + requestContext.JSON(err.StatusCode(), apiError) } // isRetryableError(http.StatusServiceUnavailable) returns true. @@ -135,57 +135,57 @@ func isRetryableError(status int) bool { // router.Use(securityHeadersMiddleware()) adds X-Content-Type-Options: nosniff and Content-Security-Policy: default-src 'none' to GET /api/v1/mining/status. func securityHeadersMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - c.Header("X-Content-Type-Options", "nosniff") - c.Header("X-Frame-Options", "DENY") - c.Header("X-XSS-Protection", "1; mode=block") - c.Header("Referrer-Policy", "strict-origin-when-cross-origin") - c.Header("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'") - c.Next() + return func(requestContext *gin.Context) { + requestContext.Header("X-Content-Type-Options", "nosniff") + requestContext.Header("X-Frame-Options", "DENY") + requestContext.Header("X-XSS-Protection", "1; mode=block") + requestContext.Header("Referrer-Policy", "strict-origin-when-cross-origin") + requestContext.Header("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'") + requestContext.Next() } } // router.Use(contentTypeValidationMiddleware()) returns 415 for POST /api/v1/mining/profiles when Content-Type is text/plain. func contentTypeValidationMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - method := c.Request.Method + return func(requestContext *gin.Context) { + method := requestContext.Request.Method if method != http.MethodPost && method != http.MethodPut && method != http.MethodPatch { - c.Next() + requestContext.Next() return } - if c.Request.ContentLength == 0 { - c.Next() + if requestContext.Request.ContentLength == 0 { + requestContext.Next() return } - contentType := c.GetHeader("Content-Type") + contentType := requestContext.GetHeader("Content-Type") if strings.HasPrefix(contentType, "application/json") || strings.HasPrefix(contentType, "application/x-www-form-urlencoded") || strings.HasPrefix(contentType, "multipart/form-data") { - c.Next() + requestContext.Next() return } - respondWithError(c, http.StatusUnsupportedMediaType, ErrCodeInvalidInput, + respondWithError(requestContext, http.StatusUnsupportedMediaType, ErrCodeInvalidInput, "Unsupported Content-Type", "Use application/json for API requests") - c.Abort() + requestContext.Abort() } } // router.Use(requestIDMiddleware()) keeps X-Request-ID: trace-123 stable across the request and response. func requestIDMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - requestID := c.GetHeader("X-Request-ID") + return func(requestContext *gin.Context) { + requestID := requestContext.GetHeader("X-Request-ID") if requestID == "" { requestID = generateRequestID() } - c.Set("requestID", requestID) - c.Header("X-Request-ID", requestID) + requestContext.Set("requestID", requestID) + requestContext.Header("X-Request-ID", requestID) - c.Next() + requestContext.Next() } } @@ -198,9 +198,9 @@ func generateRequestID() string { return strconv.FormatInt(time.Now().UnixMilli(), 10) + "-" + hex.EncodeToString(randomBytes) } -// requestID := requestIDFromContext(c) returns "trace-123" after requestIDMiddleware stores the header. -func requestIDFromContext(c *gin.Context) string { - if requestIDValue, exists := c.Get("requestID"); exists { +// requestID := requestIDFromContext(requestContext) returns "trace-123" after requestIDMiddleware stores the header. +func requestIDFromContext(requestContext *gin.Context) string { + if requestIDValue, exists := requestContext.Get("requestID"); exists { if stringValue, ok := requestIDValue.(string); ok { return stringValue } @@ -208,13 +208,13 @@ func requestIDFromContext(c *gin.Context) string { return "" } -// logWithRequestContext(c, "error", "miner failed to start", logging.Fields{"type": "xmrig", "name": "xmrig-main"}) -// logWithRequestContext(c, "info", "miner started", logging.Fields{"name": "xmrig-1", "request_id": "trace-123"}) -func logWithRequestContext(c *gin.Context, level string, message string, fields logging.Fields) { +// logWithRequestContext(requestContext, "error", "miner failed to start", logging.Fields{"type": "xmrig", "name": "xmrig-main"}) +// logWithRequestContext(requestContext, "info", "miner started", logging.Fields{"name": "xmrig-1", "request_id": "trace-123"}) +func logWithRequestContext(requestContext *gin.Context, level string, message string, fields logging.Fields) { if fields == nil { fields = logging.Fields{} } - if requestID := requestIDFromContext(c); requestID != "" { + if requestID := requestIDFromContext(requestContext); requestID != "" { fields["request_id"] = requestID } switch level { @@ -231,38 +231,38 @@ func logWithRequestContext(c *gin.Context, level string, message string, fields // csrfMiddleware() allows POST /api/v1/mining/profiles when the request includes Authorization or X-Requested-With. func csrfMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { + return func(requestContext *gin.Context) { // Only check state-changing methods such as POST /api/v1/mining/profiles. - method := c.Request.Method + method := requestContext.Request.Method if method == http.MethodGet || method == http.MethodHead || method == http.MethodOptions { - c.Next() + requestContext.Next() return } // Allow requests like `Authorization: Digest username="miner-admin"` from API clients. - if c.GetHeader("Authorization") != "" { - c.Next() + if requestContext.GetHeader("Authorization") != "" { + requestContext.Next() return } // Allow requests like `X-Requested-With: XMLHttpRequest` from browser clients. - if c.GetHeader("X-Requested-With") != "" { - c.Next() + if requestContext.GetHeader("X-Requested-With") != "" { + requestContext.Next() return } // Allow requests like `Content-Type: application/json` from API clients. - contentType := c.GetHeader("Content-Type") + contentType := requestContext.GetHeader("Content-Type") if strings.HasPrefix(contentType, "application/json") { - c.Next() + requestContext.Next() return } // Reject requests like `POST /api/v1/mining/profiles` from a plain HTML form. - respondWithError(c, http.StatusForbidden, "CSRF_PROTECTION", + respondWithError(requestContext, http.StatusForbidden, "CSRF_PROTECTION", "Request blocked by CSRF protection", "Include X-Requested-With header or use application/json content type") - c.Abort() + requestContext.Abort() } }