AX: clarify CLI and service naming
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run

This commit is contained in:
Virgil 2026-04-04 06:44:32 +00:00
parent 0893b0ef9e
commit 53b2156216
5 changed files with 103 additions and 103 deletions

View file

@ -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
}
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)")
}
}

View file

@ -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()
}
}