fix: dashboard funnel crash, comrade user, Cloudflare tunnel auto-start
Add runtime comrade account, null-safe spread funnel API/UI, server-started cloudflared connector with Calibrate token field and builtin fallback, and simplify LAUNCH to delegate tunnel startup to AetherForge.
This commit is contained in:
9
server/internal/cloudflared/launcher_stub.go
Normal file
9
server/internal/cloudflared/launcher_stub.go
Normal file
@@ -0,0 +1,9 @@
|
||||
//go:build !windows
|
||||
|
||||
package cloudflared
|
||||
|
||||
// Start is a no-op on non-Windows builds.
|
||||
func Start(_, _, _ string) error { return nil }
|
||||
|
||||
// Stop is a no-op on non-Windows builds.
|
||||
func Stop() {}
|
||||
139
server/internal/cloudflared/launcher_windows.go
Normal file
139
server/internal/cloudflared/launcher_windows.go
Normal file
@@ -0,0 +1,139 @@
|
||||
//go:build windows
|
||||
|
||||
package cloudflared
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const downloadURL = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe"
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
running *exec.Cmd
|
||||
)
|
||||
|
||||
// Start runs cloudflared tunnel with the Zero Trust connector token (background, no service install).
|
||||
func Start(dataDir, deckRoot, token string) error {
|
||||
token = trimToken(token)
|
||||
if token == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
bin, err := ensureBinary(deckRoot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if running != nil && running.Process != nil {
|
||||
log.Printf("[tunnel] Cloudflare connector already running (pid %d)", running.Process.Pid)
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := exec.Command(bin, "tunnel", "--no-autoupdate", "run", "--token", token)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("cloudflared start: %w", err)
|
||||
}
|
||||
running = cmd
|
||||
|
||||
go func(c *exec.Cmd) {
|
||||
if err := c.Wait(); err != nil {
|
||||
log.Printf("[tunnel] cloudflared exited: %v", err)
|
||||
}
|
||||
mu.Lock()
|
||||
if running == c {
|
||||
running = nil
|
||||
}
|
||||
mu.Unlock()
|
||||
}(cmd)
|
||||
|
||||
log.Printf("[tunnel] Cloudflare connector started (pid %d)", cmd.Process.Pid)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop terminates the background cloudflared process started by Start.
|
||||
func Stop() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
_ = stopLocked()
|
||||
}
|
||||
|
||||
func stopLocked() error {
|
||||
if running == nil || running.Process == nil {
|
||||
return nil
|
||||
}
|
||||
_ = running.Process.Kill()
|
||||
_, _ = running.Process.Wait()
|
||||
running = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureBinary(deckRoot string) (string, error) {
|
||||
candidates := []string{
|
||||
filepath.Join(deckRoot, "tools", "cloudflared.exe"),
|
||||
}
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
candidates = append(candidates, filepath.Join(filepath.Dir(exe), "tools", "cloudflared.exe"))
|
||||
}
|
||||
for _, p := range candidates {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
if st, err := os.Stat(p); err == nil && !st.IsDir() {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
bin := candidates[0]
|
||||
if deckRoot == "" {
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
bin = filepath.Join(filepath.Dir(exe), "tools", "cloudflared.exe")
|
||||
}
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(bin), 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
log.Printf("[tunnel] Downloading cloudflared to %s", bin)
|
||||
resp, err := http.Get(downloadURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("download cloudflared: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("download cloudflared: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
f, err := os.OpenFile(bin, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0755)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = io.Copy(f, resp.Body)
|
||||
closeErr := f.Close()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if closeErr != nil {
|
||||
return "", closeErr
|
||||
}
|
||||
return bin, nil
|
||||
}
|
||||
|
||||
func trimToken(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if len(s) >= 2 {
|
||||
if (s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '\'' && s[len(s)-1] == '\'') {
|
||||
s = strings.TrimSpace(s[1 : len(s)-1])
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user