Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Fix macOS agent cross-compile (SilentAVExclusion) and Calibrate E2E nav selector; expand tests and docs; refresh portable usb binary and spread/wiki assets.
266 lines
6.4 KiB
Go
266 lines
6.4 KiB
Go
package api
|
|
|
|
import (
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
cachedLocalIPs []string
|
|
cachedLocalIPsAt time.Time
|
|
cachedLocalIPsMu sync.Mutex
|
|
localIPsCacheTTL = 30 * time.Second
|
|
)
|
|
|
|
type ServerInfo struct {
|
|
Port int `json:"port"`
|
|
Host string `json:"host"`
|
|
LocalIPs []string `json:"local_ips"`
|
|
LANURL string `json:"lan_url"`
|
|
TunnelURL string `json:"tunnel_url,omitempty"`
|
|
CloudflaredConfigured bool `json:"cloudflared_configured"`
|
|
SuggestedURL string `json:"suggested_url"`
|
|
DashboardURL string `json:"dashboard_url"`
|
|
WebSocketURL string `json:"websocket_url"`
|
|
}
|
|
|
|
// GetServerInfo returns URLs workers and droppers should use to reach this deck.
|
|
// When the dashboard is opened via HTTPS reverse proxy (e.g. Cloudflare tunnel),
|
|
// suggested_url uses https and omits :443 — not the local listen port (8989).
|
|
// lan_url is always the LAN http endpoint for workers on the same network.
|
|
func GetServerInfo(w http.ResponseWriter, r *http.Request, publicURLOverride string, listenPort int, cloudflaredConfigured bool) {
|
|
if listenPort <= 0 {
|
|
listenPort = 8989
|
|
}
|
|
|
|
localIPs := listLocalIPv4Cached()
|
|
lanURL := lanURLFromIPs(localIPs, listenPort)
|
|
tunnelURL := tunnelURLFromRequest(r)
|
|
suggestedURL := resolveSuggestedURL(r, publicURLOverride, listenPort, localIPs)
|
|
|
|
host := r.Host
|
|
port := listenPort
|
|
if h, p, err := net.SplitHostPort(r.Host); err == nil {
|
|
host = h
|
|
if parsed := parsePort(p); parsed > 0 {
|
|
port = parsed
|
|
}
|
|
}
|
|
|
|
info := ServerInfo{
|
|
Port: port,
|
|
Host: host,
|
|
LocalIPs: localIPs,
|
|
LANURL: lanURL,
|
|
TunnelURL: tunnelURL,
|
|
CloudflaredConfigured: cloudflaredConfigured,
|
|
SuggestedURL: suggestedURL,
|
|
DashboardURL: suggestedURL,
|
|
WebSocketURL: httpToWS(suggestedURL) + "/ws/agent",
|
|
}
|
|
|
|
writeJSON(w, info)
|
|
}
|
|
|
|
func lanURLFromIPs(localIPs []string, listenPort int) string {
|
|
if len(localIPs) == 0 {
|
|
return ""
|
|
}
|
|
return formatBaseURL("http", localIPs[0], listenPort)
|
|
}
|
|
|
|
func tunnelURLFromRequest(r *http.Request) string {
|
|
scheme := requestScheme(r)
|
|
if scheme != "https" {
|
|
return ""
|
|
}
|
|
host := requestHost(r)
|
|
hostOnly, port := hostAndPort(host, scheme)
|
|
if isLoopbackHost(hostOnly) {
|
|
return ""
|
|
}
|
|
return formatBaseURL(scheme, hostOnly, port)
|
|
}
|
|
|
|
func resolveSuggestedURL(r *http.Request, publicOverride string, listenPort int, localIPs []string) string {
|
|
if norm := normalizePublicURL(publicOverride); norm != "" {
|
|
return norm
|
|
}
|
|
return externalBaseFromRequest(r, listenPort, localIPs)
|
|
}
|
|
|
|
func externalBaseFromRequest(r *http.Request, listenPort int, localIPs []string) string {
|
|
scheme := requestScheme(r)
|
|
host := requestHost(r)
|
|
hostOnly, port := hostAndPort(host, scheme)
|
|
|
|
if isLoopbackHost(hostOnly) && len(localIPs) > 0 {
|
|
hostOnly = localIPs[0]
|
|
scheme = "http"
|
|
port = listenPort
|
|
}
|
|
|
|
return formatBaseURL(scheme, hostOnly, port)
|
|
}
|
|
|
|
func requestScheme(r *http.Request) string {
|
|
if r.TLS != nil {
|
|
return "https"
|
|
}
|
|
if p := strings.TrimSpace(strings.Split(r.Header.Get("X-Forwarded-Proto"), ",")[0]); p != "" {
|
|
return strings.ToLower(p)
|
|
}
|
|
return "http"
|
|
}
|
|
|
|
func requestHost(r *http.Request) string {
|
|
if h := strings.TrimSpace(strings.Split(r.Header.Get("X-Forwarded-Host"), ",")[0]); h != "" {
|
|
return h
|
|
}
|
|
return r.Host
|
|
}
|
|
|
|
func hostAndPort(host string, scheme string) (hostOnly string, port int) {
|
|
if h, p, err := net.SplitHostPort(host); err == nil {
|
|
return strings.Trim(h, "[]"), parsePort(p)
|
|
}
|
|
if strings.Count(host, ":") == 1 && !strings.Contains(host, "]") {
|
|
parts := strings.SplitN(host, ":", 2)
|
|
return parts[0], parsePort(parts[1])
|
|
}
|
|
hostOnly = strings.Trim(host, "[]")
|
|
if scheme == "https" {
|
|
return hostOnly, 443
|
|
}
|
|
return hostOnly, 80
|
|
}
|
|
|
|
func formatBaseURL(scheme, host string, port int) string {
|
|
host = strings.Trim(host, "[]")
|
|
if (scheme == "https" && port == 443) || (scheme == "http" && port == 80) {
|
|
return scheme + "://" + host
|
|
}
|
|
return scheme + "://" + net.JoinHostPort(host, itoa(port))
|
|
}
|
|
|
|
func normalizePublicURL(raw string) string {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return ""
|
|
}
|
|
u, err := url.Parse(raw)
|
|
if err != nil || u.Scheme == "" || u.Host == "" {
|
|
return strings.TrimRight(raw, "/")
|
|
}
|
|
_, port := hostAndPort(u.Host, u.Scheme)
|
|
u.Host = formatURLHost(u.Hostname(), port, u.Scheme)
|
|
u.Path = ""
|
|
u.RawPath = ""
|
|
u.RawQuery = ""
|
|
u.Fragment = ""
|
|
return strings.TrimRight(u.String(), "/")
|
|
}
|
|
|
|
func formatURLHost(hostname string, port int, scheme string) string {
|
|
if (scheme == "https" && port == 443) || (scheme == "http" && port == 80) {
|
|
return hostname
|
|
}
|
|
return net.JoinHostPort(hostname, itoa(port))
|
|
}
|
|
|
|
func httpToWS(base string) string {
|
|
if strings.HasPrefix(base, "https://") {
|
|
return "wss://" + strings.TrimPrefix(base, "https://")
|
|
}
|
|
return "ws://" + strings.TrimPrefix(base, "http://")
|
|
}
|
|
|
|
func listLocalIPv4Cached() []string {
|
|
cachedLocalIPsMu.Lock()
|
|
defer cachedLocalIPsMu.Unlock()
|
|
if cachedLocalIPs != nil && time.Since(cachedLocalIPsAt) < localIPsCacheTTL {
|
|
out := make([]string, len(cachedLocalIPs))
|
|
copy(out, cachedLocalIPs)
|
|
return out
|
|
}
|
|
cachedLocalIPs = listLocalIPv4()
|
|
cachedLocalIPsAt = time.Now()
|
|
out := make([]string, len(cachedLocalIPs))
|
|
copy(out, cachedLocalIPs)
|
|
return out
|
|
}
|
|
|
|
func listLocalIPv4() []string {
|
|
var ips []string
|
|
ifaces, err := net.Interfaces()
|
|
if err != nil {
|
|
return ips
|
|
}
|
|
for _, iface := range ifaces {
|
|
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
|
|
continue
|
|
}
|
|
addrs, err := iface.Addrs()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, addr := range addrs {
|
|
var ip net.IP
|
|
switch v := addr.(type) {
|
|
case *net.IPNet:
|
|
ip = v.IP
|
|
case *net.IPAddr:
|
|
ip = v.IP
|
|
}
|
|
if ip == nil || ip.IsLoopback() {
|
|
continue
|
|
}
|
|
ip = ip.To4()
|
|
if ip == nil {
|
|
continue
|
|
}
|
|
ips = append(ips, ip.String())
|
|
}
|
|
}
|
|
return ips
|
|
}
|
|
|
|
func isLoopbackHost(host string) bool {
|
|
return host == "localhost" || host == "127.0.0.1" || host == "::1"
|
|
}
|
|
|
|
func parsePort(s string) int {
|
|
n := 0
|
|
for _, ch := range s {
|
|
if ch < '0' || ch > '9' {
|
|
return 8989
|
|
}
|
|
n = n*10 + int(ch-'0')
|
|
if n > 65535 {
|
|
return 8989
|
|
}
|
|
}
|
|
if n == 0 {
|
|
return 8989
|
|
}
|
|
return n
|
|
}
|
|
|
|
func itoa(n int) string {
|
|
if n == 0 {
|
|
return "0"
|
|
}
|
|
var buf [16]byte
|
|
i := len(buf)
|
|
for n > 0 {
|
|
i--
|
|
buf[i] = byte('0' + n%10)
|
|
n /= 10
|
|
}
|
|
return string(buf[i:])
|
|
}
|