go-proxy/proxy_http_runtime.go
Virgil e523fd0740 refactor(api): share monitoring route registration
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 16:45:13 +00:00

244 lines
6.3 KiB
Go

package proxy
import (
"context"
"encoding/json"
"net"
"net/http"
"strconv"
"strings"
"time"
)
const proxyAPIVersion = "1.0.0"
// RouteRegistrar is the minimal route-registration surface used by RegisterMonitoringRoutes.
//
// mux := http.NewServeMux()
// RegisterMonitoringRoutes(mux, p)
type RouteRegistrar interface {
HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))
}
// SummaryResponse is the /1/summary JSON body.
type SummaryResponse struct {
Version string `json:"version"`
Mode string `json:"mode"`
Hashrate HashrateResponse `json:"hashrate"`
Miners MinersCountResponse `json:"miners"`
Workers uint64 `json:"workers"`
Upstreams UpstreamResponse `json:"upstreams"`
Results ResultsResponse `json:"results"`
}
// HashrateResponse carries the per-window hashrate array.
type HashrateResponse struct {
Total [6]float64 `json:"total"`
}
// MinersCountResponse carries current and peak miner counts.
type MinersCountResponse struct {
Now uint64 `json:"now"`
Max uint64 `json:"max"`
}
// UpstreamResponse carries pool connection state counts.
type UpstreamResponse struct {
Active uint64 `json:"active"`
Sleep uint64 `json:"sleep"`
Error uint64 `json:"error"`
Total uint64 `json:"total"`
Ratio float64 `json:"ratio"`
}
// ResultsResponse carries share acceptance statistics.
type ResultsResponse struct {
Accepted uint64 `json:"accepted"`
Rejected uint64 `json:"rejected"`
Invalid uint64 `json:"invalid"`
Expired uint64 `json:"expired"`
AvgTime uint32 `json:"avg_time"`
Latency uint32 `json:"latency"`
HashesTotal uint64 `json:"hashes_total"`
Best [10]uint64 `json:"best"`
}
func startHTTPServer(p *Proxy) {
if p == nil || p.config == nil || !p.config.HTTP.Enabled || p.httpServer != nil {
return
}
mux := http.NewServeMux()
RegisterMonitoringRoutes(mux, p)
address := net.JoinHostPort(p.config.HTTP.Host, strconv.Itoa(int(p.config.HTTP.Port)))
listener, errorValue := net.Listen("tcp", address)
if errorValue != nil {
return
}
server := &http.Server{
Handler: mux,
}
p.httpServer = server
go func() {
_ = server.Serve(listener)
}()
}
func stopHTTPServer(p *Proxy) {
if p == nil || p.httpServer == nil {
return
}
server := p.httpServer
p.httpServer = nil
shutdownContext, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = server.Shutdown(shutdownContext)
}
// RegisterMonitoringRoutes mounts the monitoring endpoints on any router with HandleFunc.
//
// mux := http.NewServeMux()
// RegisterMonitoringRoutes(mux, p)
func RegisterMonitoringRoutes(router RouteRegistrar, proxyValue *Proxy) {
if router == nil || proxyValue == nil {
return
}
router.HandleFunc("/1/summary", func(writer http.ResponseWriter, request *http.Request) {
if !allowHTTPRequest(writer, request, proxyValue.HTTPConfig()) {
return
}
summary := proxyValue.Summary()
upstreams := proxyValue.Upstreams()
ratio := 0.0
if upstreams.Total > 0 {
ratio = float64(proxyValue.CurrentMiners()) / float64(upstreams.Total)
}
response := SummaryResponse{
Version: proxyAPIVersion,
Mode: proxyValue.Mode(),
Hashrate: HashrateResponse{
Total: summary.Hashrate,
},
Miners: MinersCountResponse{
Now: proxyValue.CurrentMiners(),
Max: proxyValue.MaxMiners(),
},
Workers: uint64(len(proxyValue.Workers())),
Upstreams: UpstreamResponse{
Active: upstreams.Active,
Sleep: upstreams.Sleep,
Error: upstreams.Error,
Total: upstreams.Total,
Ratio: ratio,
},
Results: ResultsResponse{
Accepted: summary.Accepted,
Rejected: summary.Rejected,
Invalid: summary.Invalid,
Expired: summary.Expired,
AvgTime: summary.AvgTime,
Latency: summary.AvgLatency,
HashesTotal: summary.Hashes,
Best: summary.TopDiff,
},
}
writeHTTPJSON(writer, response)
})
router.HandleFunc("/1/workers", func(writer http.ResponseWriter, request *http.Request) {
if !allowHTTPRequest(writer, request, proxyValue.HTTPConfig()) {
return
}
records := proxyValue.Workers()
rows := make([][]interface{}, 0, len(records))
for _, record := range records {
rows = append(rows, []interface{}{
record.Name,
record.LastIP,
record.Connections,
record.Accepted,
record.Rejected,
record.Invalid,
record.Hashes,
record.LastHashAt.Unix(),
record.Hashrate(60),
record.Hashrate(600),
record.Hashrate(3600),
record.Hashrate(43200),
record.Hashrate(86400),
})
}
writeHTTPJSON(writer, map[string]interface{}{
"mode": proxyValue.WorkersMode(),
"workers": rows,
})
})
router.HandleFunc("/1/miners", func(writer http.ResponseWriter, request *http.Request) {
if !allowHTTPRequest(writer, request, proxyValue.HTTPConfig()) {
return
}
miners := proxyValue.Miners()
rows := make([][]interface{}, 0, len(miners))
for _, miner := range miners {
ip := ""
if remote := miner.RemoteAddr(); remote != nil {
ip = remote.String()
}
rows = append(rows, []interface{}{
miner.ID(),
ip,
miner.TX(),
miner.RX(),
miner.State(),
miner.Diff(),
miner.User(),
"********",
miner.RigID(),
miner.Agent(),
})
}
writeHTTPJSON(writer, map[string]interface{}{
"format": []string{"id", "ip", "tx", "rx", "state", "diff", "user", "password", "rig_id", "agent"},
"miners": rows,
})
})
}
func allowHTTPRequest(writer http.ResponseWriter, request *http.Request, config HTTPConfig) bool {
if request == nil {
return false
}
if config.AccessToken != "" {
header := request.Header.Get("Authorization")
prefix := "Bearer "
if !strings.HasPrefix(header, prefix) || strings.TrimSpace(strings.TrimPrefix(header, prefix)) != config.AccessToken {
writer.Header().Set("WWW-Authenticate", "Bearer")
http.Error(writer, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return false
}
}
if config.Restricted && request.Method != http.MethodGet {
http.Error(writer, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return false
}
return true
}
func writeHTTPJSON(writer http.ResponseWriter, value interface{}) {
writer.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(writer).Encode(value)
}