peermap: add admin api

This commit is contained in:
rkonfj
2025-03-15 18:18:51 +08:00
parent e85ea23d62
commit a5e3eafbc0
10 changed files with 192 additions and 31 deletions
+2
View File
@@ -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
-20
View File
@@ -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 {
+4 -3
View File
@@ -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 {
+40
View File
@@ -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)
}
+8
View File
@@ -0,0 +1,8 @@
package langs
func Must[T any](v T, err error) T {
if err != nil {
panic(err)
}
return v
}
+1 -1
View File
@@ -1,4 +1,4 @@
package disco
package langs
import (
"crypto/rand"
+106
View File
@@ -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
}
+7
View File
@@ -0,0 +1,7 @@
package types
import "net/url"
type PeerStore interface {
Peers(network string) ([]url.Values, error)
}
+3 -3
View File
@@ -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 {
+21 -4
View File
@@ -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
}