Files
edgevpn/api/api.go
mudler 8826daf815 ⚙️ Add wip/experimental peerguard and peergater
Peerguard and peergater are two components that work together to gate
peers and add them to a trusted zone.

This allows to isolate nodes from the p2p network and avoid to rotate
network tokens in case of leaks.

For the moment an ECDSA auth provider is implemented as sample purpose,
documentation will follow up on how to use them and how to write them
up.
2022-04-23 00:39:59 +02:00

353 lines
9.3 KiB
Go

// Copyright © 2021-2022 Ettore Di Giacinto <mudler@mocaccino.org>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see <http://www.gnu.org/licenses/>.
package api
import (
"context"
"embed"
"fmt"
"io/fs"
"net"
"net/http"
_ "net/http/pprof"
"path/filepath"
"strings"
"time"
"github.com/libp2p/go-libp2p-core/metrics"
"github.com/libp2p/go-libp2p-core/network"
"github.com/libp2p/go-libp2p-core/peer"
p2pprotocol "github.com/libp2p/go-libp2p-core/protocol"
"github.com/miekg/dns"
apiTypes "github.com/mudler/edgevpn/api/types"
"github.com/labstack/echo/v4"
"github.com/mudler/edgevpn/pkg/node"
"github.com/mudler/edgevpn/pkg/protocol"
"github.com/mudler/edgevpn/pkg/services"
"github.com/mudler/edgevpn/pkg/types"
)
//go:embed public
var embededFiles embed.FS
func getFileSystem() http.FileSystem {
fsys, err := fs.Sub(embededFiles, "public")
if err != nil {
panic(err)
}
return http.FS(fsys)
}
const (
MachineURL = "/api/machines"
UsersURL = "/api/users"
ServiceURL = "/api/services"
BlockchainURL = "/api/blockchain"
LedgerURL = "/api/ledger"
SummaryURL = "/api/summary"
FileURL = "/api/files"
NodesURL = "/api/nodes"
DNSURL = "/api/dns"
MetricsURL = "/api/metrics"
PeerstoreURL = "/api/peerstore"
PeerGateURL = "/api/peergate"
)
func API(ctx context.Context, l string, defaultInterval, timeout time.Duration, e *node.Node, bwc metrics.Reporter, debugMode bool) error {
ledger, _ := e.Ledger()
ec := echo.New()
if strings.HasPrefix(l, "unix://") {
unixListener, err := net.Listen("unix", strings.ReplaceAll(l, "unix://", ""))
if err != nil {
return err
}
ec.Listener = unixListener
}
assetHandler := http.FileServer(getFileSystem())
if debugMode {
ec.GET("/debug/pprof/*", echo.WrapHandler(http.DefaultServeMux))
}
if bwc != nil {
ec.GET(MetricsURL, func(c echo.Context) error {
return c.JSON(http.StatusOK, bwc.GetBandwidthTotals())
})
ec.GET(filepath.Join(MetricsURL, "protocol"), func(c echo.Context) error {
return c.JSON(http.StatusOK, bwc.GetBandwidthByProtocol())
})
ec.GET(filepath.Join(MetricsURL, "peer"), func(c echo.Context) error {
return c.JSON(http.StatusOK, bwc.GetBandwidthByPeer())
})
ec.GET(filepath.Join(MetricsURL, "peer", ":peer"), func(c echo.Context) error {
return c.JSON(http.StatusOK, bwc.GetBandwidthForPeer(peer.ID(c.Param("peer"))))
})
ec.GET(filepath.Join(MetricsURL, "protocol", ":protocol"), func(c echo.Context) error {
return c.JSON(http.StatusOK, bwc.GetBandwidthForProtocol(p2pprotocol.ID(c.Param("protocol"))))
})
}
// Get data from ledger
ec.GET(FileURL, func(c echo.Context) error {
list := []*types.File{}
for _, v := range ledger.CurrentData()[protocol.FilesLedgerKey] {
machine := &types.File{}
v.Unmarshal(machine)
list = append(list, machine)
}
return c.JSON(http.StatusOK, list)
})
if e.PeerGater() != nil {
ec.PUT(fmt.Sprintf("%s/:state", PeerGateURL), func(c echo.Context) error {
state := c.Param("state")
switch state {
case "enable":
e.PeerGater().Enable()
case "disable":
e.PeerGater().Disable()
}
return c.JSON(http.StatusOK, e.PeerGater().Enabled())
})
ec.GET(PeerGateURL, func(c echo.Context) error {
return c.JSON(http.StatusOK, e.PeerGater().Enabled())
})
}
ec.GET(SummaryURL, func(c echo.Context) error {
files := len(ledger.CurrentData()[protocol.FilesLedgerKey])
machines := len(ledger.CurrentData()[protocol.MachinesLedgerKey])
users := len(ledger.CurrentData()[protocol.UsersLedgerKey])
services := len(ledger.CurrentData()[protocol.ServicesLedgerKey])
peers, err := e.MessageHub.ListPeers()
if err != nil {
return err
}
onChainNodes := len(peers)
p2pPeers := len(e.Host().Network().Peerstore().Peers())
nodeID := e.Host().ID().String()
blockchain := ledger.Index()
return c.JSON(http.StatusOK, types.Summary{
Files: files,
Machines: machines,
Users: users,
Services: services,
BlockChain: blockchain,
OnChainNodes: onChainNodes,
Peers: p2pPeers,
NodeID: nodeID,
})
})
ec.GET(MachineURL, func(c echo.Context) error {
list := []*apiTypes.Machine{}
online := services.AvailableNodes(ledger, 20*time.Minute)
for _, v := range ledger.CurrentData()[protocol.MachinesLedgerKey] {
machine := &types.Machine{}
v.Unmarshal(machine)
m := &apiTypes.Machine{Machine: *machine}
if e.Host().Network().Connectedness(peer.ID(machine.PeerID)) == network.Connected {
m.Connected = true
}
peers, err := e.MessageHub.ListPeers()
if err != nil {
return err
}
for _, p := range peers {
if p.String() == machine.PeerID {
m.OnChain = true
}
}
for _, a := range online {
if a == machine.PeerID {
m.Online = true
}
}
list = append(list, m)
}
return c.JSON(http.StatusOK, list)
})
ec.GET(NodesURL, func(c echo.Context) error {
list := []apiTypes.Peer{}
peers, err := e.MessageHub.ListPeers()
if err != nil {
return err
}
// Sum up state also from services
online := services.AvailableNodes(ledger, 10*time.Minute)
p := map[string]interface{}{}
for _, v := range online {
p[v] = nil
}
for _, v := range peers {
_, exists := p[v.String()]
if !exists {
p[v.String()] = nil
}
}
for id, _ := range p {
list = append(list, apiTypes.Peer{ID: id, Online: true})
}
return c.JSON(http.StatusOK, list)
})
ec.GET(PeerstoreURL, func(c echo.Context) error {
list := []apiTypes.Peer{}
for _, v := range e.Host().Network().Peerstore().Peers() {
list = append(list, apiTypes.Peer{ID: v.String()})
}
return c.JSON(http.StatusOK, list)
})
ec.GET(UsersURL, func(c echo.Context) error {
user := []*types.User{}
for _, v := range ledger.CurrentData()[protocol.UsersLedgerKey] {
u := &types.User{}
v.Unmarshal(u)
user = append(user, u)
}
return c.JSON(http.StatusOK, user)
})
ec.GET(ServiceURL, func(c echo.Context) error {
list := []*types.Service{}
for _, v := range ledger.CurrentData()[protocol.ServicesLedgerKey] {
srvc := &types.Service{}
v.Unmarshal(srvc)
list = append(list, srvc)
}
return c.JSON(http.StatusOK, list)
})
ec.GET("/*", echo.WrapHandler(http.StripPrefix("/", assetHandler)))
ec.GET(BlockchainURL, func(c echo.Context) error {
return c.JSON(http.StatusOK, ledger.LastBlock())
})
ec.GET(LedgerURL, func(c echo.Context) error {
return c.JSON(http.StatusOK, ledger.CurrentData())
})
ec.GET(fmt.Sprintf("%s/:bucket/:key", LedgerURL), func(c echo.Context) error {
bucket := c.Param("bucket")
key := c.Param("key")
return c.JSON(http.StatusOK, ledger.CurrentData()[bucket][key])
})
ec.GET(fmt.Sprintf("%s/:bucket", LedgerURL), func(c echo.Context) error {
bucket := c.Param("bucket")
return c.JSON(http.StatusOK, ledger.CurrentData()[bucket])
})
announcing := struct{ State string }{"Announcing"}
// Store arbitrary data
ec.PUT(fmt.Sprintf("%s/:bucket/:key/:value", LedgerURL), func(c echo.Context) error {
bucket := c.Param("bucket")
key := c.Param("key")
value := c.Param("value")
ledger.Persist(context.Background(), defaultInterval, timeout, bucket, key, value)
return c.JSON(http.StatusOK, announcing)
})
ec.GET(DNSURL, func(c echo.Context) error {
res := []apiTypes.DNS{}
for r, e := range ledger.CurrentData()[protocol.DNSKey] {
var t types.DNS
e.Unmarshal(&t)
d := map[string]string{}
for k, v := range t {
d[dns.TypeToString[uint16(k)]] = v
}
res = append(res,
apiTypes.DNS{
Regex: r,
Records: d,
})
}
return c.JSON(http.StatusOK, res)
})
// Announce dns
ec.POST(DNSURL, func(c echo.Context) error {
d := new(apiTypes.DNS)
if err := c.Bind(d); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
entry := make(types.DNS)
for r, e := range d.Records {
entry[dns.Type(dns.StringToType[r])] = e
}
services.PersistDNSRecord(context.Background(), ledger, defaultInterval, timeout, d.Regex, entry)
return c.JSON(http.StatusOK, announcing)
})
// Delete data from ledger
ec.DELETE(fmt.Sprintf("%s/:bucket", LedgerURL), func(c echo.Context) error {
bucket := c.Param("bucket")
ledger.AnnounceDeleteBucket(context.Background(), defaultInterval, timeout, bucket)
return c.JSON(http.StatusOK, announcing)
})
ec.DELETE(fmt.Sprintf("%s/:bucket/:key", LedgerURL), func(c echo.Context) error {
bucket := c.Param("bucket")
key := c.Param("key")
ledger.AnnounceDeleteBucketKey(context.Background(), defaultInterval, timeout, bucket, key)
return c.JSON(http.StatusOK, announcing)
})
ec.HideBanner = true
if err := ec.Start(l); err != nil && err != http.ErrServerClosed {
return err
}
go func() {
<-ctx.Done()
ct, cancel := context.WithTimeout(context.Background(), 10*time.Second)
ec.Shutdown(ct)
cancel()
}()
return nil
}