From a5e3eafbc0807d11aabe3218450421ff1a8cfae2 Mon Sep 17 00:00:00 2001 From: rkonfj Date: Sat, 15 Mar 2025 18:18:51 +0800 Subject: [PATCH] peermap: add admin api --- cmd/pgmap/main.go | 2 + disco/disco.go | 20 ------- disco/ws/ws.go | 7 ++- langs/error.go | 40 +++++++++++++ langs/must.go | 8 +++ {disco => langs}/nonce.go | 2 +- peermap/admin/admin_v1.go | 106 +++++++++++++++++++++++++++++++++++ peermap/admin/types/types.go | 7 +++ peermap/auth/auth.go | 6 +- peermap/peermap.go | 25 +++++++-- 10 files changed, 192 insertions(+), 31 deletions(-) create mode 100644 langs/error.go create mode 100644 langs/must.go rename {disco => langs}/nonce.go (96%) create mode 100644 peermap/admin/admin_v1.go create mode 100644 peermap/admin/types/types.go diff --git a/cmd/pgmap/main.go b/cmd/pgmap/main.go index c2ce2b7..0e6b08a 100644 --- a/cmd/pgmap/main.go +++ b/cmd/pgmap/main.go @@ -14,6 +14,7 @@ import ( "syscall" "github.com/sigcn/pg/peermap" + "github.com/sigcn/pg/peermap/admin" ) var ( @@ -100,6 +101,7 @@ func run(commandConfig peermap.Config, configPath string) error { cfg, _ := peermap.ReadConfig(configPath) cfg.Overwrite(commandConfig) + admin.Version = Version srv, err := peermap.New(cfg) if err != nil { return err diff --git a/disco/disco.go b/disco/disco.go index 42e7543..c0232fa 100644 --- a/disco/disco.go +++ b/disco/disco.go @@ -1,9 +1,6 @@ package disco import ( - "encoding/json" - "fmt" - "io" "log/slog" "net" "net/url" @@ -58,23 +55,6 @@ const ( CONTROL_SERVER_CONNECTED ControlCode = 50 ) -type Error struct { - Code int `json:"code"` - Msg string `json:"msg"` -} - -func (e Error) Wrap(err error) Error { - return Error{Code: e.Code, Msg: fmt.Sprintf("%s: %s", e.Msg, err)} -} - -func (e Error) Error() string { - return fmt.Sprintf("ENO%d: %s", e.Code, e.Msg) -} - -func (e Error) MarshalTo(w io.Writer) { - json.NewEncoder(w).Encode(e) -} - type NATType string func (t NATType) AccurateThan(t1 NATType) bool { diff --git a/disco/ws/ws.go b/disco/ws/ws.go index 177cf7e..f410bc0 100644 --- a/disco/ws/ws.go +++ b/disco/ws/ws.go @@ -20,6 +20,7 @@ import ( "github.com/gorilla/websocket" "github.com/sigcn/pg/disco" + "github.com/sigcn/pg/langs" "golang.org/x/time/rate" ) @@ -206,7 +207,7 @@ func (c *WSConn) dial(ctx context.Context, server string) error { handshake := http.Header{} handshake.Set("X-Network", networkSecret.Secret) handshake.Set("X-PeerID", c.peerID.String()) - handshake.Set("X-Nonce", disco.NewNonce()) + handshake.Set("X-Nonce", langs.NewNonce()) handshake.Set("X-Metadata", c.metadata.Encode()) if server == "" { server = c.server.URL @@ -229,7 +230,7 @@ func (c *WSConn) dial(ctx context.Context, server string) error { return fmt.Errorf("dial server %s: 404 not found", server) } if httpResp != nil && httpResp.StatusCode == http.StatusForbidden { - var err disco.Error + var err langs.Error json.NewDecoder(httpResp.Body).Decode(&err) defer httpResp.Body.Close() return err @@ -253,7 +254,7 @@ func (c *WSConn) dial(ctx context.Context, server string) error { } c.rawConn.Store(conn) - c.nonce = disco.MustParseNonce(httpResp.Header.Get("X-Nonce")) + c.nonce = langs.MustParseNonce(httpResp.Header.Get("X-Nonce")) c.connectedServer = server c.activeTime.Store(time.Now().Unix()) conn.SetPingHandler(func(appData string) error { diff --git a/langs/error.go b/langs/error.go new file mode 100644 index 0000000..0364782 --- /dev/null +++ b/langs/error.go @@ -0,0 +1,40 @@ +package langs + +import ( + "encoding/json" + "fmt" + "io" +) + +type Error struct { + Code int `json:"code"` + Msg string `json:"msg"` +} + +func (e Error) Wrap(err error) Error { + return Error{Code: e.Code, Msg: fmt.Sprintf("%s: %s", e.Msg, err)} +} + +func (e Error) Error() string { + return fmt.Sprintf("ENO%d: %s", e.Code, e.Msg) +} + +func (e Error) MarshalTo(w io.Writer) { + json.NewEncoder(w).Encode(e) +} + +func Err(err error) Error { + if knownErr, ok := err.(Error); ok { + return knownErr + } + return Error{Code: 5000, Msg: err.Error()} +} + +type Data[T any] struct { + Error + Data T `json:"data"` +} + +func (d Data[T]) MarshalTo(w io.Writer) { + json.NewEncoder(w).Encode(d) +} diff --git a/langs/must.go b/langs/must.go new file mode 100644 index 0000000..101dbc1 --- /dev/null +++ b/langs/must.go @@ -0,0 +1,8 @@ +package langs + +func Must[T any](v T, err error) T { + if err != nil { + panic(err) + } + return v +} diff --git a/disco/nonce.go b/langs/nonce.go similarity index 96% rename from disco/nonce.go rename to langs/nonce.go index 3663e56..f1d8aac 100644 --- a/disco/nonce.go +++ b/langs/nonce.go @@ -1,4 +1,4 @@ -package disco +package langs import ( "crypto/rand" diff --git a/peermap/admin/admin_v1.go b/peermap/admin/admin_v1.go new file mode 100644 index 0000000..3cbd288 --- /dev/null +++ b/peermap/admin/admin_v1.go @@ -0,0 +1,106 @@ +package admin + +import ( + "errors" + "net/http" + "net/url" + "runtime/debug" + "sync" + + "github.com/sigcn/pg/langs" + "github.com/sigcn/pg/peermap/admin/types" + "github.com/sigcn/pg/peermap/auth" +) + +var ( + Version string = "dev" + ErrForbidden = langs.Error{Code: 10000, Msg: "forbidden"} +) + +type AdministratorV1 struct { + Auth *auth.Authenticator + PeerStore types.PeerStore + + mux http.ServeMux + initOnce sync.Once +} + +func (a *AdministratorV1) init() { + a.initOnce.Do(func() { + a.mux.HandleFunc("GET /pg/apis/v1/admin/peers", a.handleQueryPeers) + a.mux.HandleFunc("GET /pg/apis/v1/admin/server_info", a.handleQueryServerInfo) + }) +} + +func (a *AdministratorV1) ServeHTTP(w http.ResponseWriter, r *http.Request) { + a.init() + a.mux.ServeHTTP(w, r) +} + +func (a *AdministratorV1) handleQueryPeers(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("X-Token") + secret, err := a.Auth.ParseSecret(token) + if err != nil { + langs.Err(err).MarshalTo(w) + return + } + if !secret.Admin { + ErrForbidden.MarshalTo(w) + return + } + peers, err := a.PeerStore.Peers(secret.Network) + if err != nil { + langs.Err(err).MarshalTo(w) + return + } + langs.Data[[]url.Values]{Data: peers}.MarshalTo(w) +} + +func (a *AdministratorV1) handleQueryServerInfo(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("X-Token") + secret, err := a.Auth.ParseSecret(token) + if err != nil { + langs.Err(err).MarshalTo(w) + return + } + if !secret.Admin { + ErrForbidden.MarshalTo(w) + return + } + + info, err := readBuildInfo() + if err != nil { + langs.Err(err).MarshalTo(w) + } + langs.Data[any]{Data: serverInfo{Version: Version, buildInfo: info}}.MarshalTo(w) +} + +type buildInfo struct { + GoVersion string `json:"go_version"` + VCSRevision string `json:"vcs_revision"` + VCSTime string `json:"vcs_time"` +} + +func readBuildInfo() (buildInfo buildInfo, err error) { + info, ok := debug.ReadBuildInfo() + if !ok { + err = errors.ErrUnsupported + return + } + buildInfo.GoVersion = info.GoVersion + for _, s := range info.Settings { + if s.Key == "vcs.revision" { + buildInfo.VCSRevision = s.Value + continue + } + if s.Key == "vcs.time" { + buildInfo.VCSTime = s.Value + } + } + return +} + +type serverInfo struct { + Version string `json:"version"` + buildInfo +} diff --git a/peermap/admin/types/types.go b/peermap/admin/types/types.go new file mode 100644 index 0000000..979a7a9 --- /dev/null +++ b/peermap/admin/types/types.go @@ -0,0 +1,7 @@ +package types + +import "net/url" + +type PeerStore interface { + Peers(network string) ([]url.Values, error) +} diff --git a/peermap/auth/auth.go b/peermap/auth/auth.go index 090c4de..9a18065 100644 --- a/peermap/auth/auth.go +++ b/peermap/auth/auth.go @@ -4,15 +4,15 @@ import ( "crypto/sha256" "encoding/base64" "encoding/json" - "errors" "time" + "github.com/sigcn/pg/langs" "github.com/sigcn/pg/secure/aescbc" ) var ( - ErrInvalidToken = errors.New("invalid token") - ErrTokenExpired = errors.New("token expired") + ErrInvalidToken = langs.Error{Code: 9000, Msg: "invalid token"} + ErrTokenExpired = langs.Error{Code: 9001, Msg: "token expired"} ) type JSONSecret struct { diff --git a/peermap/peermap.go b/peermap/peermap.go index 87e9113..098f739 100644 --- a/peermap/peermap.go +++ b/peermap/peermap.go @@ -18,6 +18,8 @@ import ( "github.com/gorilla/websocket" "github.com/sigcn/pg/disco" + "github.com/sigcn/pg/langs" + "github.com/sigcn/pg/peermap/admin" "github.com/sigcn/pg/peermap/auth" "github.com/sigcn/pg/peermap/exporter" exporterauth "github.com/sigcn/pg/peermap/exporter/auth" @@ -26,8 +28,9 @@ import ( ) var ( - ErrAddressAlreadyInuse = disco.Error{Code: 4000, Msg: "the network address is already in use"} - ErrNetworkSecretExpired = disco.Error{Code: 4030, Msg: "network secret is expired"} + ErrAddressAlreadyInuse = langs.Error{Code: 4000, Msg: "the network address is already in use"} + ErrNetworkSecretExpired = langs.Error{Code: 4030, Msg: "network secret is expired"} + ErrNetworkNotFound = langs.Error{Code: 60000, Msg: "network not found"} _ io.ReadWriter = (*peerConn)(nil) ) @@ -611,7 +614,7 @@ func (pm *PeerMap) HandlePeerPacketConnect(w http.ResponseWriter, r *http.Reques } peerID := r.Header.Get("X-PeerID") - nonce := disco.MustParseNonce(r.Header.Get("X-Nonce")) + nonce := langs.MustParseNonce(r.Header.Get("X-Nonce")) pm.networkMapMutex.RLock() networkCtx, ok := pm.networkMap[jsonSecret.Network] @@ -715,6 +718,19 @@ func (pm *PeerMap) Grant(network, state string) (disco.NetworkSecret, error) { return pm.generateSecret(n, strings.HasPrefix(state, "PG_ADM")) } +func (pm *PeerMap) Peers(network string) (peers []url.Values, err error) { + netctx, ok := pm.getNetwork(network) + if !ok { + return nil, ErrNetworkNotFound + } + netctx.peersMutex.RLock() + for _, v := range netctx.peers { + peers = append(peers, v.metadata) + } + netctx.peersMutex.RUnlock() + return +} + func (pm *PeerMap) newNetworkContext(state NetState) *networkContext { return &networkContext{ id: state.ID, @@ -768,15 +784,16 @@ func New(cfg Config) (*PeerMap, error) { mux := http.NewServeMux() pm.httpServer = &http.Server{Handler: mux, Addr: cfg.Listen} + mux.Handle("/pg/apis/v1/admin/", &admin.AdministratorV1{Auth: pm.authenticator, PeerStore: &pm}) mux.HandleFunc("GET /pg", pm.HandlePeerPacketConnect) mux.HandleFunc("GET /pg/networks", pm.HandleQueryNetworks) mux.HandleFunc("GET /pg/peers", pm.HandleQueryNetworkPeers) mux.HandleFunc("GET /pg/networks/{network}/meta", pm.HandleGetNetworkMeta) mux.HandleFunc("PUT /pg/networks/{network}/meta", pm.HandlePutNetworkMeta) + mux.Handle("GET /oidc/authorize/{provider}", &oidc.Authority{Grant: pm.Grant}) mux.HandleFunc("GET /oidc", oidc.OIDCSelector) mux.HandleFunc("GET /oidc/secret", oidc.OIDCSecret) mux.HandleFunc("GET /oidc/{provider}", oidc.OIDCAuthURL) - mux.Handle("GET /oidc/authorize/{provider}", &oidc.Authority{Grant: pm.Grant}) return &pm, nil }