mirror of
https://gitcode.com/JianFeeeee/webui4frpc.git
synced 2026-09-20 00:47:57 +00:00
- Cluster: forwardToNext offline detection (leader+non-leader), WatchLeader 1s heartbeat fallback, 409 for standalone nodes, Node.NodeKey key exchange via token ring, ClusterPeers persistence + auto-rejoin, Forward delegates to forwardToNext (bugfix) - Auth: Basic Auth (flag-creds fast path) + bcrypt users (admin/viewer) + Bearer API keys (read/write/admin scope) - Frontend: UsersView (accounts+API keys), ClusterView (ring/nodeKey/tasks/topology/log), StatusView (group management, per-proxy status), CanvasView (edge toggle/group), PortEdge (disabled/group labels) - API: handlers split (canvas/forwards/users/logs), canvas export/import, forwards group start/stop/assign/delete, cluster endpoints - Docs: comprehensive README rewrite (all flags/APIs/auth/cluster), docs/cluster-api.md (cluster management API reference) - Deploy: run-cluster.sh now 4-node ring + 1 isolated standalone, test-forward.sh updated for 4 nodes - Removed plan.md (design notes consolidated into README + API docs)
70 lines
2.2 KiB
Go
70 lines
2.2 KiB
Go
// Join helper: a newcomer POSTs its JoinInfo to a target node's join endpoint
|
|
// and adopts the returned ring state (leader preserved, newcomer inserted as
|
|
// the target's successor).
|
|
package cluster
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// GenerateNodeKey returns a random 16-byte hex string for use as a cluster
|
|
// admission key. Called on first startup when no key is persisted yet.
|
|
func GenerateNodeKey() string {
|
|
b := make([]byte, 16)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return fmt.Sprintf("%x", time.Now().UnixNano())
|
|
}
|
|
return fmt.Sprintf("%x", b)
|
|
}
|
|
|
|
// JoinRing asks target to add us to its ring and returns the adopted state.
|
|
func (e *Engine) JoinRing(ctx context.Context, targetAddr string, ji JoinInfo) error {
|
|
url := fmt.Sprintf("http://%s/api/manager/cluster/join", targetAddr)
|
|
body, err := json.Marshal(ji)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
if e.User != "" || e.Pass != "" {
|
|
req.SetBasicAuth(e.User, e.Pass)
|
|
}
|
|
cli := &http.Client{Timeout: 8 * time.Second}
|
|
resp, err := cli.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("join %s -> HTTP %d", targetAddr, resp.StatusCode)
|
|
}
|
|
var out struct {
|
|
State State `json:"state"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
|
return err
|
|
}
|
|
e.AdoptState(out.State)
|
|
return nil
|
|
}
|
|
|
|
// JoinRingAddr wraps JoinRing for HTTP handlers: it builds the newcomer's
|
|
// JoinInfo from this node's own id/addr/version/cache (so the caller doesn't
|
|
// touch the engine's unexported fields) and asks targetAddr to sponsor us
|
|
// into its ring. joinKey is the sponsor's nodeKey — the sponsor verifies it
|
|
// before admitting. This is the runtime "加入集群" path (vs. the startup
|
|
// bootstrap call in cmd/webui4frpc/main.go).
|
|
func (e *Engine) JoinRingAddr(ctx context.Context, targetAddr, joinKey string) error {
|
|
ji := JoinInfo{ID: e.ID, Addr: e.myAddr, Version: e.Version, Cache: e.Cache, JoinKey: joinKey}
|
|
return e.JoinRing(ctx, targetAddr, ji)
|
|
}
|