AX: clarify mining service names and comments
This commit is contained in:
parent
4e8283c84f
commit
a675d16ed6
5 changed files with 67 additions and 73 deletions
|
|
@ -31,28 +31,28 @@ var serveCmd = &cobra.Command{
|
|||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
displayHost := host
|
||||
if displayHost == "0.0.0.0" {
|
||||
displayHostName := host
|
||||
if displayHostName == "0.0.0.0" {
|
||||
var err error
|
||||
displayHost, err = getLocalIP()
|
||||
displayHostName, err = getLocalIP()
|
||||
if err != nil {
|
||||
displayHost = "localhost"
|
||||
displayHostName = "localhost"
|
||||
}
|
||||
}
|
||||
displayAddr := fmt.Sprintf("%s:%d", displayHost, port)
|
||||
listenAddr := fmt.Sprintf("%s:%d", host, port)
|
||||
displayAddress := fmt.Sprintf("%s:%d", displayHostName, port)
|
||||
listenAddress := fmt.Sprintf("%s:%d", host, port)
|
||||
|
||||
// miningManager := getServiceManager() shares the same miner lifecycle state across CLI commands.
|
||||
miningManager := getServiceManager()
|
||||
// manager := getServiceManager() shares the same miner lifecycle state across CLI commands.
|
||||
manager := getServiceManager()
|
||||
|
||||
miningService, err := mining.NewService(miningManager, listenAddr, displayAddr, namespace)
|
||||
service, err := mining.NewService(manager, listenAddress, displayAddress, namespace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create new service: %w", err)
|
||||
}
|
||||
|
||||
// miningService.ServiceStartup(ctx) starts the HTTP server while the shell keeps reading stdin.
|
||||
// service.ServiceStartup(ctx) starts the HTTP server while the shell keeps reading stdin.
|
||||
go func() {
|
||||
if err := miningService.ServiceStartup(ctx); err != nil {
|
||||
if err := service.ServiceStartup(ctx); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to start service: %v\n", err)
|
||||
cancel()
|
||||
}
|
||||
|
|
@ -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", displayHost, port)
|
||||
fmt.Printf("Swagger documentation is available at http://%s:%d%s/index.html\n", displayHost, port, miningService.SwaggerUIPath)
|
||||
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.Println("Entering interactive shell. Type 'exit' or 'quit' to stop.")
|
||||
fmt.Print(">> ")
|
||||
|
||||
|
|
@ -135,7 +135,7 @@ var serveCmd = &cobra.Command{
|
|||
continue
|
||||
}
|
||||
|
||||
miner, err := miningManager.StartMiner(context.Background(), minerType, config)
|
||||
miner, err := manager.StartMiner(context.Background(), minerType, config)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error starting miner: %v\n", err)
|
||||
} else {
|
||||
|
|
@ -147,7 +147,7 @@ var serveCmd = &cobra.Command{
|
|||
fmt.Println("Error: status command requires miner name, for example `status xmrig`")
|
||||
} else {
|
||||
minerName := commandArgs[0]
|
||||
miner, err := miningManager.GetMiner(minerName)
|
||||
miner, err := manager.GetMiner(minerName)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error getting miner status: %v\n", err)
|
||||
} else {
|
||||
|
|
@ -169,7 +169,7 @@ var serveCmd = &cobra.Command{
|
|||
fmt.Println("Error: stop command requires miner name, for example `stop xmrig`")
|
||||
} else {
|
||||
minerName := commandArgs[0]
|
||||
err := miningManager.StopMiner(context.Background(), minerName)
|
||||
err := manager.StopMiner(context.Background(), minerName)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error stopping miner: %v\n", err)
|
||||
} else {
|
||||
|
|
@ -177,7 +177,7 @@ var serveCmd = &cobra.Command{
|
|||
}
|
||||
}
|
||||
case "list":
|
||||
miners := miningManager.ListMiners()
|
||||
miners := manager.ListMiners()
|
||||
if len(miners) == 0 {
|
||||
fmt.Println("No miners currently running.")
|
||||
} else {
|
||||
|
|
@ -206,8 +206,8 @@ var serveCmd = &cobra.Command{
|
|||
case <-ctx.Done():
|
||||
}
|
||||
|
||||
// miningManager.Stop() stops miner goroutines and closes the shared manager before exit.
|
||||
miningManager.Stop()
|
||||
// manager.Stop() stops miner goroutines and closes the shared manager before exit.
|
||||
manager.Stop()
|
||||
|
||||
fmt.Println("Mining service stopped.")
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -57,8 +57,8 @@ Available presets:
|
|||
displayHost = "localhost"
|
||||
}
|
||||
}
|
||||
displayAddr := fmt.Sprintf("%s:%d", displayHost, port)
|
||||
listenAddr := fmt.Sprintf("%s:%d", host, port)
|
||||
displayAddress := fmt.Sprintf("%s:%d", displayHost, port)
|
||||
listenAddress := fmt.Sprintf("%s:%d", host, port)
|
||||
|
||||
// manager := mining.NewManagerForSimulation() // keeps simulated miners isolated from the real autostart state.
|
||||
manager := mining.NewManagerForSimulation()
|
||||
|
|
@ -83,7 +83,7 @@ Available presets:
|
|||
}
|
||||
|
||||
// service, err := mining.NewService(manager, listenAddr, displayAddr, namespace) // serves the simulator on http://127.0.0.1:9090.
|
||||
service, err := mining.NewService(manager, listenAddr, displayAddr, namespace)
|
||||
service, err := mining.NewService(manager, listenAddress, displayAddress, namespace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create new service: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,13 +20,13 @@ var startCmd = &cobra.Command{
|
|||
Long: `Start a new miner with the specified configuration.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
selectedMinerType := args[0]
|
||||
minerType := args[0]
|
||||
config := &mining.Config{
|
||||
Pool: poolAddress,
|
||||
Wallet: walletAddress,
|
||||
}
|
||||
|
||||
miner, err := getServiceManager().StartMiner(context.Background(), selectedMinerType, config)
|
||||
miner, err := getServiceManager().StartMiner(context.Background(), minerType, config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start miner: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ var statusCmd = &cobra.Command{
|
|||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
minerName := args[0]
|
||||
miningManager := getServiceManager()
|
||||
manager := getServiceManager()
|
||||
|
||||
miner, err := miningManager.GetMiner(minerName)
|
||||
miner, err := manager.GetMiner(minerName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get miner: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,10 +60,10 @@ type APIError struct {
|
|||
Retryable bool `json:"retryable"` // Can the client retry?
|
||||
}
|
||||
|
||||
// if debugErrorsEnabled { /* details exposed in API responses */ } // true when DEBUG_ERRORS=true or GIN_MODE != "release"
|
||||
// debugErrorsEnabled is true when DEBUG_ERRORS=true or GIN_MODE != "release".
|
||||
var debugErrorsEnabled = os.Getenv("DEBUG_ERRORS") == "true" || os.Getenv("GIN_MODE") != "release"
|
||||
|
||||
// sanitizeErrorDetails("exec: file not found") // => "" in production, => "exec: file not found" in debug mode
|
||||
// sanitizeErrorDetails("exec: file not found") returns "" in production and the full string in debug mode.
|
||||
func sanitizeErrorDetails(details string) string {
|
||||
if debugErrorsEnabled {
|
||||
return details
|
||||
|
|
@ -71,7 +71,7 @@ func sanitizeErrorDetails(details string) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// respondWithError(c, http.StatusNotFound, ErrCodeMinerNotFound, "xmrig not found", err.Error())
|
||||
// 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) {
|
||||
apiError := APIError{
|
||||
Code: code,
|
||||
|
|
@ -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(c, 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"
|
||||
|
|
@ -125,15 +125,15 @@ func respondWithMiningError(c *gin.Context, err *MiningError) {
|
|||
c.JSON(err.StatusCode(), apiError)
|
||||
}
|
||||
|
||||
// isRetryableError(http.StatusServiceUnavailable) // => true
|
||||
// isRetryableError(http.StatusNotFound) // => false
|
||||
// isRetryableError(http.StatusServiceUnavailable) returns true.
|
||||
// isRetryableError(http.StatusNotFound) returns false.
|
||||
func isRetryableError(status int) bool {
|
||||
return status == http.StatusServiceUnavailable ||
|
||||
status == http.StatusTooManyRequests ||
|
||||
status == http.StatusGatewayTimeout
|
||||
}
|
||||
|
||||
// router.Use(securityHeadersMiddleware()) // GET /api/v1/mining/status returns X-Content-Type-Options: nosniff and Content-Security-Policy: default-src 'none'
|
||||
// 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")
|
||||
|
|
@ -145,7 +145,7 @@ func securityHeadersMiddleware() gin.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
// router.Use(contentTypeValidationMiddleware()) // POST /api/v1/mining/profiles with Content-Type: text/plain returns 415 Unsupported Media Type
|
||||
// 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
|
||||
|
|
@ -174,7 +174,7 @@ func contentTypeValidationMiddleware() gin.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
// router.Use(requestIDMiddleware()) // GET /api/v1/mining/status with X-Request-ID: trace-123 keeps the same ID in the response
|
||||
// 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")
|
||||
|
|
@ -189,7 +189,7 @@ func requestIDMiddleware() gin.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
// requestID := generateRequestID() // "1712070000123-a1b2c3d4e5f6a7b8"
|
||||
// requestID := generateRequestID() // example: 1712070000123-a1b2c3d4e5f6a7b8
|
||||
func generateRequestID() string {
|
||||
randomBytes := make([]byte, 8)
|
||||
if _, err := rand.Read(randomBytes); err != nil {
|
||||
|
|
@ -198,7 +198,7 @@ func generateRequestID() string {
|
|||
return strconv.FormatInt(time.Now().UnixMilli(), 10) + "-" + hex.EncodeToString(randomBytes)
|
||||
}
|
||||
|
||||
// requestID := getRequestID(c) // "trace-123" after requestIDMiddleware stores the incoming X-Request-ID header
|
||||
// requestID := getRequestID(c) returns "trace-123" after requestIDMiddleware stores the incoming header.
|
||||
func getRequestID(c *gin.Context) string {
|
||||
if id, exists := c.Get("requestID"); exists {
|
||||
if stringValue, ok := id.(string); ok {
|
||||
|
|
@ -229,41 +229,36 @@ func logWithRequestID(c *gin.Context, level string, message string, fields loggi
|
|||
}
|
||||
}
|
||||
|
||||
// csrfMiddleware protects against CSRF attacks for browser-based requests.
|
||||
// For state-changing methods (POST, PUT, DELETE), it requires one of:
|
||||
// - Authorization header (API clients)
|
||||
// - X-Requested-With header (AJAX clients)
|
||||
// - Origin header matching allowed origins (already handled by CORS)
|
||||
// GET requests are always allowed as they should be idempotent.
|
||||
// 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) {
|
||||
// Only check state-changing methods
|
||||
// Only check state-changing methods such as POST /api/v1/mining/profiles.
|
||||
method := c.Request.Method
|
||||
if method == http.MethodGet || method == http.MethodHead || method == http.MethodOptions {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// Allow if Authorization header present (API client)
|
||||
// Allow requests like `Authorization: Digest username="miner-admin"` from API clients.
|
||||
if c.GetHeader("Authorization") != "" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// Allow if X-Requested-With header present (AJAX/XHR request)
|
||||
// Allow requests like `X-Requested-With: XMLHttpRequest` from browser clients.
|
||||
if c.GetHeader("X-Requested-With") != "" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// Allow if Content-Type is application/json (not sent by HTML forms)
|
||||
// Allow requests like `Content-Type: application/json` from API clients.
|
||||
contentType := c.GetHeader("Content-Type")
|
||||
if strings.HasPrefix(contentType, "application/json") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// Reject the request as potential CSRF
|
||||
// Reject requests like `POST /api/v1/mining/profiles` from a plain HTML form.
|
||||
respondWithError(c, http.StatusForbidden, "CSRF_PROTECTION",
|
||||
"Request blocked by CSRF protection",
|
||||
"Include X-Requested-With header or use application/json content type")
|
||||
|
|
@ -271,10 +266,10 @@ func csrfMiddleware() gin.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
// ctx, cancel := context.WithTimeout(context.Background(), DefaultRequestTimeout)
|
||||
// context.WithTimeout(context.Background(), DefaultRequestTimeout) keeps long API requests under 30 seconds.
|
||||
const DefaultRequestTimeout = 30 * time.Second
|
||||
|
||||
// Cache-Control header constants
|
||||
// CachePublic5Min matches GET /api/v1/mining/miners/available responses.
|
||||
const (
|
||||
CacheNoStore = "no-store"
|
||||
CacheNoCache = "no-cache"
|
||||
|
|
@ -282,10 +277,10 @@ const (
|
|||
CachePublic5Min = "public, max-age=300"
|
||||
)
|
||||
|
||||
// router.Use(cacheMiddleware()) // serves /miners/available with Cache-Control: public, max-age=300
|
||||
// router.Use(cacheMiddleware()) serves GET /miners/available with Cache-Control: public, max-age=300.
|
||||
func cacheMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Only cache GET requests
|
||||
// Only cache GET requests like /api/v1/mining/info.
|
||||
if c.Request.Method != http.MethodGet {
|
||||
c.Header("Cache-Control", CacheNoStore)
|
||||
c.Next()
|
||||
|
|
@ -294,19 +289,18 @@ func cacheMiddleware() gin.HandlerFunc {
|
|||
|
||||
path := c.Request.URL.Path
|
||||
|
||||
// Static-ish resources that can be cached briefly
|
||||
// Cache GET /api/v1/mining/miners/available for 5 minutes.
|
||||
switch {
|
||||
case strings.HasSuffix(path, "/available"):
|
||||
// Available miners list - can be cached for 5 minutes
|
||||
c.Header("Cache-Control", CachePublic5Min)
|
||||
case strings.HasSuffix(path, "/info"):
|
||||
// System info - can be cached for 1 minute
|
||||
// Cache GET /api/v1/mining/info for 1 minute.
|
||||
c.Header("Cache-Control", CachePublic1Min)
|
||||
case strings.Contains(path, "/swagger"):
|
||||
// Swagger docs - can be cached
|
||||
// Cache GET /api/v1/mining/swagger/index.html for 5 minutes.
|
||||
c.Header("Cache-Control", CachePublic5Min)
|
||||
default:
|
||||
// Dynamic data (stats, miners, profiles) - don't cache
|
||||
// Keep dynamic requests like /api/v1/mining/miners/xmrig live.
|
||||
c.Header("Cache-Control", CacheNoCache)
|
||||
}
|
||||
|
||||
|
|
@ -314,10 +308,10 @@ func cacheMiddleware() gin.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
// router.Use(requestTimeoutMiddleware(30 * time.Second)) // aborts a slow /history/miners/xmrig request after 30 seconds
|
||||
// router.Use(requestTimeoutMiddleware(30 * time.Second)) aborts GET /history/miners/xmrig after 30 seconds.
|
||||
func requestTimeoutMiddleware(timeout time.Duration) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Skip timeout for WebSocket upgrades and streaming endpoints
|
||||
// Skip timeout for WebSocket upgrades like /ws/events.
|
||||
if c.GetHeader("Upgrade") == "websocket" {
|
||||
c.Next()
|
||||
return
|
||||
|
|
@ -327,11 +321,11 @@ func requestTimeoutMiddleware(timeout time.Duration) gin.HandlerFunc {
|
|||
return
|
||||
}
|
||||
|
||||
// Create context with timeout
|
||||
// Create a request-scoped timeout for requests like GET /api/v1/mining/history/xmrig.
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), timeout)
|
||||
defer cancel()
|
||||
|
||||
// Replace request context
|
||||
// Replace the incoming request context with the timed one.
|
||||
c.Request = c.Request.WithContext(ctx)
|
||||
|
||||
var responded int32
|
||||
|
|
@ -345,9 +339,9 @@ func requestTimeoutMiddleware(timeout time.Duration) gin.HandlerFunc {
|
|||
|
||||
select {
|
||||
case <-done:
|
||||
// Request completed normally
|
||||
// Request completed normally, for example GET /api/v1/mining/status.
|
||||
case <-ctx.Done():
|
||||
// Timeout occurred - only respond if handler hasn't already
|
||||
// Timeout occurred; only respond if the handler has not already written a response.
|
||||
if atomic.CompareAndSwapInt32(&responded, 0, 1) {
|
||||
c.Abort()
|
||||
respondWithError(c, http.StatusGatewayTimeout, ErrCodeTimeout,
|
||||
|
|
@ -357,36 +351,36 @@ func requestTimeoutMiddleware(timeout time.Duration) gin.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
// conn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil) // upgrade HTTP to WebSocket
|
||||
// conn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil) upgrades GET /ws/events to WebSocket.
|
||||
var wsUpgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
// Allow connections from localhost origins only
|
||||
// Allow browser requests like `Origin: http://localhost:4200`.
|
||||
origin := r.Header.Get("Origin")
|
||||
if origin == "" {
|
||||
return true // No origin header (non-browser clients)
|
||||
return true // No Origin header, for example from curl or another non-browser client.
|
||||
}
|
||||
// Parse the origin URL properly to prevent bypass attacks
|
||||
// Parse the origin URL properly to prevent bypass attacks.
|
||||
u, err := url.Parse(origin)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
host := u.Hostname()
|
||||
// Only allow exact localhost matches
|
||||
// Allow exact localhost matches like `http://127.0.0.1:4200`.
|
||||
return host == "localhost" || host == "127.0.0.1" || host == "::1" ||
|
||||
host == "wails.localhost"
|
||||
},
|
||||
}
|
||||
|
||||
// service, err := mining.NewService(manager, ":9090", "localhost:9090", "api/v1/mining")
|
||||
func NewService(manager ManagerInterface, listenAddr string, displayAddr string, swaggerNamespace string) (*Service, error) {
|
||||
// service, err := mining.NewService(manager, ":9090", "localhost:9090", "/api/v1/mining")
|
||||
func NewService(manager ManagerInterface, listenAddress string, displayAddress string, swaggerNamespace string) (*Service, error) {
|
||||
apiBasePath := "/" + strings.Trim(swaggerNamespace, "/")
|
||||
swaggerUIPath := apiBasePath + "/swagger"
|
||||
|
||||
docs.SwaggerInfo.Title = "Mining Module API"
|
||||
docs.SwaggerInfo.Version = "1.0"
|
||||
docs.SwaggerInfo.Host = displayAddr
|
||||
docs.SwaggerInfo.Host = displayAddress
|
||||
docs.SwaggerInfo.BasePath = apiBasePath
|
||||
instanceName := "swagger_" + strings.ReplaceAll(strings.Trim(swaggerNamespace, "/"), "/", "_")
|
||||
swag.Register(instanceName, docs.SwaggerInfo)
|
||||
|
|
@ -458,13 +452,13 @@ func NewService(manager ManagerInterface, listenAddr string, displayAddr string,
|
|||
NodeService: nodeService,
|
||||
EventHub: eventHub,
|
||||
Server: &http.Server{
|
||||
Addr: listenAddr,
|
||||
Addr: listenAddress,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
},
|
||||
DisplayAddr: displayAddr,
|
||||
DisplayAddr: displayAddress,
|
||||
SwaggerInstanceName: instanceName,
|
||||
APIBasePath: apiBasePath,
|
||||
SwaggerUIPath: swaggerUIPath,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue