go-proxy/api/router.go
Virgil 7d2d309529 feat(proxy): close RFC gaps
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 10:52:30 +00:00

226 lines
6.2 KiB
Go

// Package api implements the HTTP monitoring endpoints for the proxy.
//
// Registered routes:
//
// GET /1/summary — aggregated proxy stats
// GET /1/workers — per-worker hashrate table
// GET /1/miners — per-connection state table
//
// proxyapi.RegisterRoutes(apiRouter, p)
package api
import (
"encoding/json"
"net/http"
"strings"
"dappco.re/go/core/proxy"
)
// Router is the minimal route-registration surface used by RegisterRoutes.
//
// mux := http.NewServeMux()
// api.RegisterRoutes(mux, p)
type Router interface {
HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))
}
// SummaryResponse is the /1/summary JSON body.
//
// {"version":"1.0.0","mode":"nicehash","hashrate":{"total":[...]}, ...}
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.
//
// HashrateResponse{Total: [6]float64{12345.67, 11900.00, 12100.00, 11800.00, 12000.00, 12200.00}}
type HashrateResponse struct {
Total [6]float64 `json:"total"`
}
// MinersCountResponse carries current and peak miner counts.
//
// MinersCountResponse{Now: 142, Max: 200}
type MinersCountResponse struct {
Now uint64 `json:"now"`
Max uint64 `json:"max"`
}
// UpstreamResponse carries pool connection state counts.
//
// UpstreamResponse{Active: 1, Sleep: 0, Error: 0, Total: 1, Ratio: 142.0}
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.
//
// ResultsResponse{Accepted: 4821, Rejected: 3, Invalid: 0, Expired: 12}
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"`
}
// RegisterRoutes registers the proxy monitoring routes on a HTTP router.
//
// api.RegisterRoutes(http.NewServeMux(), p)
func RegisterRoutes(router Router, proxyValue *proxy.Proxy) {
if router == nil || proxyValue == nil {
return
}
router.HandleFunc("/1/summary", func(writer http.ResponseWriter, request *http.Request) {
if !allowRequest(writer, request, proxyValue.HTTPConfig()) {
return
}
summary := proxyValue.Summary()
response := SummaryResponse{
Version: "1.0.0",
Mode: proxyValue.Mode(),
Hashrate: HashrateResponse{
Total: summary.Hashrate,
},
Miners: MinersCountResponse{
Now: proxyValue.CurrentMiners(),
Max: proxyValue.MaxMiners(),
},
Workers: uint64(len(proxyValue.Workers())),
Upstreams: func() UpstreamResponse {
upstreams := proxyValue.Upstreams()
ratio := 0.0
if upstreams.Total > 0 {
ratio = float64(proxyValue.CurrentMiners()) / float64(upstreams.Total)
}
return 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,
},
}
writeJSON(writer, response)
})
router.HandleFunc("/1/workers", func(writer http.ResponseWriter, request *http.Request) {
if !allowRequest(writer, request, proxyValue.HTTPConfig()) {
return
}
type responseBody struct {
Mode string `json:"mode"`
Workers [][]interface{} `json:"workers"`
}
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),
})
}
writeJSON(writer, responseBody{
Mode: proxyValue.WorkersMode(),
Workers: rows,
})
})
router.HandleFunc("/1/miners", func(writer http.ResponseWriter, request *http.Request) {
if !allowRequest(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(),
})
}
writeJSON(writer, map[string]interface{}{
"format": []string{"id", "ip", "tx", "rx", "state", "diff", "user", "password", "rig_id", "agent"},
"miners": rows,
})
})
}
func allowRequest(writer http.ResponseWriter, request *http.Request, config proxy.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 writeJSON(writer http.ResponseWriter, value interface{}) {
writer.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(writer).Encode(value)
}