mirror of
https://github.com/h44z/wg-portal.git
synced 2025-08-12 08:12:23 +00:00
wip: implement mikrotik rest api client (#426)
This commit is contained in:
parent
d5ce889e4f
commit
e934232e0b
@ -89,6 +89,10 @@ func NewLocalController(cfg *config.Config) (*LocalController, error) {
|
|||||||
return repo, nil
|
return repo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c LocalController) GetId() domain.InterfaceBackend {
|
||||||
|
return config.LocalBackendName
|
||||||
|
}
|
||||||
|
|
||||||
// region wireguard-related
|
// region wireguard-related
|
||||||
|
|
||||||
func (c LocalController) GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error) {
|
func (c LocalController) GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error) {
|
||||||
|
@ -2,38 +2,261 @@ package wgcontroller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
"github.com/h44z/wg-portal/internal/lowlevel"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MikrotikController struct {
|
type MikrotikController struct {
|
||||||
|
coreCfg *config.Config
|
||||||
|
cfg *config.BackendMikrotik
|
||||||
|
|
||||||
|
client *lowlevel.MikrotikApiClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMikrotikController() (*MikrotikController, error) {
|
func NewMikrotikController(coreCfg *config.Config, cfg *config.BackendMikrotik) (*MikrotikController, error) {
|
||||||
return &MikrotikController{}, nil
|
client, err := lowlevel.NewMikrotikApiClient(coreCfg, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create Mikrotik API client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &MikrotikController{
|
||||||
|
coreCfg: coreCfg,
|
||||||
|
cfg: cfg,
|
||||||
|
|
||||||
|
client: client,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c MikrotikController) GetId() domain.InterfaceBackend {
|
||||||
|
return domain.InterfaceBackend(c.cfg.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// region wireguard-related
|
// region wireguard-related
|
||||||
|
|
||||||
func (c MikrotikController) GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error) {
|
func (c MikrotikController) GetInterfaces(ctx context.Context) ([]domain.PhysicalInterface, error) {
|
||||||
// TODO implement me
|
wgReply := c.client.Query(ctx, "/interface/wireguard", &lowlevel.MikrotikRequestOptions{
|
||||||
panic("implement me")
|
PropList: []string{
|
||||||
|
".id", "name", "public-key", "private-key", "listen-port", "mtu", "disabled", "running",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if wgReply.Status != lowlevel.MikrotikApiStatusOk {
|
||||||
|
return nil, fmt.Errorf("failed to query interfaces: %v", wgReply.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
interfaces := make([]domain.PhysicalInterface, 0, len(wgReply.Data))
|
||||||
|
for _, wg := range wgReply.Data {
|
||||||
|
physicalInterface, err := c.loadInterfaceData(ctx, wg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
interfaces = append(interfaces, *physicalInterface)
|
||||||
|
}
|
||||||
|
|
||||||
|
return interfaces, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c MikrotikController) GetInterface(_ context.Context, id domain.InterfaceIdentifier) (
|
func (c MikrotikController) GetInterface(ctx context.Context, id domain.InterfaceIdentifier) (
|
||||||
*domain.PhysicalInterface,
|
*domain.PhysicalInterface,
|
||||||
error,
|
error,
|
||||||
) {
|
) {
|
||||||
// TODO implement me
|
wgReply := c.client.Query(ctx, "/interface/wireguard", &lowlevel.MikrotikRequestOptions{
|
||||||
panic("implement me")
|
PropList: []string{
|
||||||
|
".id", "name", "public-key", "private-key", "listen-port", "mtu", "disabled", "running",
|
||||||
|
},
|
||||||
|
Filters: map[string]string{
|
||||||
|
"name": string(id),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if wgReply.Status != lowlevel.MikrotikApiStatusOk {
|
||||||
|
return nil, fmt.Errorf("failed to query interface %s: %v", id, wgReply.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(wgReply.Data) == 0 {
|
||||||
|
return nil, fmt.Errorf("interface %s not found", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.loadInterfaceData(ctx, wgReply.Data[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c MikrotikController) GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier) (
|
func (c MikrotikController) loadInterfaceData(
|
||||||
|
ctx context.Context,
|
||||||
|
wireGuardObj lowlevel.GenericJsonObject,
|
||||||
|
) (*domain.PhysicalInterface, error) {
|
||||||
|
deviceId := wireGuardObj.GetString(".id")
|
||||||
|
deviceName := wireGuardObj.GetString("name")
|
||||||
|
ifaceReply := c.client.Get(ctx, "/interface/"+deviceId, &lowlevel.MikrotikRequestOptions{
|
||||||
|
PropList: []string{
|
||||||
|
"name", "rx-byte", "tx-byte",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if ifaceReply.Status != lowlevel.MikrotikApiStatusOk {
|
||||||
|
return nil, fmt.Errorf("failed to query interface %s: %v", deviceId, ifaceReply.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
addrV4Reply := c.client.Query(ctx, "/ip/address", &lowlevel.MikrotikRequestOptions{
|
||||||
|
PropList: []string{
|
||||||
|
"address", "network",
|
||||||
|
},
|
||||||
|
Filters: map[string]string{
|
||||||
|
"interface": deviceName,
|
||||||
|
"dynamic": "false", // we only want static addresses
|
||||||
|
"disabled": "false", // we only want addresses that are not disabled
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if addrV4Reply.Status != lowlevel.MikrotikApiStatusOk {
|
||||||
|
return nil, fmt.Errorf("failed to query IPv4 addresses for interface %s: %v", deviceId, addrV4Reply.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
addrV6Reply := c.client.Query(ctx, "/ipv6/address", &lowlevel.MikrotikRequestOptions{
|
||||||
|
PropList: []string{
|
||||||
|
"address", "network",
|
||||||
|
},
|
||||||
|
Filters: map[string]string{
|
||||||
|
"interface": deviceName,
|
||||||
|
"dynamic": "false", // we only want static addresses
|
||||||
|
"disabled": "false", // we only want addresses that are not disabled
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if addrV6Reply.Status != lowlevel.MikrotikApiStatusOk {
|
||||||
|
return nil, fmt.Errorf("failed to query IPv6 addresses for interface %s: %v", deviceId, addrV6Reply.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
interfaceModel, err := c.convertWireGuardInterface(wireGuardObj, ifaceReply.Data, addrV4Reply.Data,
|
||||||
|
addrV6Reply.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("interface convert failed for %s: %w", deviceName, err)
|
||||||
|
}
|
||||||
|
return &interfaceModel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c MikrotikController) convertWireGuardInterface(
|
||||||
|
wg, iface lowlevel.GenericJsonObject,
|
||||||
|
ipv4, ipv6 []lowlevel.GenericJsonObject,
|
||||||
|
) (
|
||||||
|
domain.PhysicalInterface,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
// read data from wgctrl interface
|
||||||
|
|
||||||
|
addresses := make([]domain.Cidr, 0, len(ipv4)+len(ipv6))
|
||||||
|
for _, addr := range append(ipv4, ipv6...) {
|
||||||
|
addrStr := addr.GetString("address")
|
||||||
|
if addrStr == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cidr, err := domain.CidrFromString(addrStr)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
addresses = append(addresses, cidr)
|
||||||
|
}
|
||||||
|
|
||||||
|
pi := domain.PhysicalInterface{
|
||||||
|
Identifier: domain.InterfaceIdentifier(wg.GetString("name")),
|
||||||
|
KeyPair: domain.KeyPair{
|
||||||
|
PrivateKey: wg.GetString("private-key"),
|
||||||
|
PublicKey: wg.GetString("public-key"),
|
||||||
|
},
|
||||||
|
ListenPort: wg.GetInt("listen-port"),
|
||||||
|
Addresses: addresses,
|
||||||
|
Mtu: wg.GetInt("mtu"),
|
||||||
|
FirewallMark: 0,
|
||||||
|
DeviceUp: wg.GetBool("running"),
|
||||||
|
ImportSource: "mikrotik",
|
||||||
|
DeviceType: "Mikrotik",
|
||||||
|
BytesUpload: uint64(iface.GetInt("tx-byte")),
|
||||||
|
BytesDownload: uint64(iface.GetInt("rx-byte")),
|
||||||
|
}
|
||||||
|
|
||||||
|
return pi, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c MikrotikController) GetPeers(ctx context.Context, deviceId domain.InterfaceIdentifier) (
|
||||||
[]domain.PhysicalPeer,
|
[]domain.PhysicalPeer,
|
||||||
error,
|
error,
|
||||||
) {
|
) {
|
||||||
// TODO implement me
|
wgReply := c.client.Query(ctx, "/interface/wireguard/peers", &lowlevel.MikrotikRequestOptions{
|
||||||
panic("implement me")
|
PropList: []string{
|
||||||
|
".id", "name", "allowed-address", "client-address", "client-endpoint", "client-keepalive", "comment",
|
||||||
|
"current-endpoint-address", "current-endpoint-port", "last-handshake", "persistent-keepalive",
|
||||||
|
"public-key", "private-key", "preshared-key", "mtu", "disabled", "rx", "tx", "responder",
|
||||||
|
},
|
||||||
|
Filters: map[string]string{
|
||||||
|
"interface": string(deviceId),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if wgReply.Status != lowlevel.MikrotikApiStatusOk {
|
||||||
|
return nil, fmt.Errorf("failed to query peers for %s: %v", deviceId, wgReply.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(wgReply.Data) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
peers := make([]domain.PhysicalPeer, 0, len(wgReply.Data))
|
||||||
|
for _, peer := range wgReply.Data {
|
||||||
|
peerModel, err := c.convertWireGuardPeer(peer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("peer convert failed for %v: %w", peer.GetString("name"), err)
|
||||||
|
}
|
||||||
|
peers = append(peers, peerModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
return peers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c MikrotikController) convertWireGuardPeer(peer lowlevel.GenericJsonObject) (domain.PhysicalPeer, error) {
|
||||||
|
keepAliveSeconds := 0
|
||||||
|
duration, err := time.ParseDuration(peer.GetString("client-keepalive"))
|
||||||
|
if err == nil {
|
||||||
|
keepAliveSeconds = int(duration.Seconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
currentEndpoint := ""
|
||||||
|
if peer.GetString("current-endpoint-address") != "" && peer.GetString("current-endpoint-port") != "" {
|
||||||
|
currentEndpoint = peer.GetString("current-endpoint-address") + ":" + peer.GetString("current-endpoint-port")
|
||||||
|
}
|
||||||
|
|
||||||
|
lastHandshakeTime := time.Time{}
|
||||||
|
if peer.GetString("last-handshake") != "" {
|
||||||
|
relDuration, err := time.ParseDuration(peer.GetString("last-handshake"))
|
||||||
|
if err == nil {
|
||||||
|
lastHandshakeTime = time.Now().Add(-relDuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allowedAddresses, _ := domain.CidrsFromString(peer.GetString("allowed-address"))
|
||||||
|
|
||||||
|
peerModel := domain.PhysicalPeer{
|
||||||
|
Identifier: domain.PeerIdentifier(peer.GetString("public-key")),
|
||||||
|
Endpoint: currentEndpoint,
|
||||||
|
AllowedIPs: allowedAddresses,
|
||||||
|
KeyPair: domain.KeyPair{
|
||||||
|
PublicKey: peer.GetString("public-key"),
|
||||||
|
PrivateKey: peer.GetString("private-key"),
|
||||||
|
},
|
||||||
|
PresharedKey: domain.PreSharedKey(peer.GetString("preshared-key")),
|
||||||
|
PersistentKeepalive: keepAliveSeconds,
|
||||||
|
LastHandshake: lastHandshakeTime,
|
||||||
|
ProtocolVersion: 0, // Mikrotik does not support protocol versioning, so we set it to 0
|
||||||
|
BytesUpload: uint64(peer.GetInt("rx")),
|
||||||
|
BytesDownload: uint64(peer.GetInt("tx")),
|
||||||
|
|
||||||
|
BackendExtras: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
peerModel.BackendExtras["MT-NAME"] = peer.GetString("name")
|
||||||
|
peerModel.BackendExtras["MT-COMMENT"] = peer.GetString("comment")
|
||||||
|
peerModel.BackendExtras["MT-RESPONDER"] = peer.GetString("responder")
|
||||||
|
peerModel.BackendExtras["MT-ENDPOINT"] = peer.GetString("client-endpoint")
|
||||||
|
peerModel.BackendExtras["MT-IP"] = peer.GetString("client-address")
|
||||||
|
|
||||||
|
return peerModel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c MikrotikController) SaveInterface(
|
func (c MikrotikController) SaveInterface(
|
||||||
@ -42,12 +265,12 @@ func (c MikrotikController) SaveInterface(
|
|||||||
updateFunc func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error),
|
updateFunc func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error),
|
||||||
) error {
|
) error {
|
||||||
// TODO implement me
|
// TODO implement me
|
||||||
panic("implement me")
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c MikrotikController) DeleteInterface(_ context.Context, id domain.InterfaceIdentifier) error {
|
func (c MikrotikController) DeleteInterface(_ context.Context, id domain.InterfaceIdentifier) error {
|
||||||
// TODO implement me
|
// TODO implement me
|
||||||
panic("implement me")
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c MikrotikController) SavePeer(
|
func (c MikrotikController) SavePeer(
|
||||||
@ -57,7 +280,7 @@ func (c MikrotikController) SavePeer(
|
|||||||
updateFunc func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error),
|
updateFunc func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error),
|
||||||
) error {
|
) error {
|
||||||
// TODO implement me
|
// TODO implement me
|
||||||
panic("implement me")
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c MikrotikController) DeletePeer(
|
func (c MikrotikController) DeletePeer(
|
||||||
@ -66,7 +289,7 @@ func (c MikrotikController) DeletePeer(
|
|||||||
id domain.PeerIdentifier,
|
id domain.PeerIdentifier,
|
||||||
) error {
|
) error {
|
||||||
// TODO implement me
|
// TODO implement me
|
||||||
panic("implement me")
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion wireguard-related
|
// endregion wireguard-related
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type InterfaceController interface {
|
type InterfaceController interface {
|
||||||
|
GetId() domain.InterfaceBackend
|
||||||
GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error)
|
GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error)
|
||||||
GetInterface(_ context.Context, id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error)
|
GetInterface(_ context.Context, id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error)
|
||||||
GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier) ([]domain.PhysicalPeer, error)
|
GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier) ([]domain.PhysicalPeer, error)
|
||||||
@ -92,7 +93,7 @@ func (c *ControllerManager) registerMikrotikControllers() error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
controller, err := wgcontroller.NewMikrotikController() // TODO: Pass backendConfig to the controller constructor
|
controller, err := wgcontroller.NewMikrotikController(c.cfg, &backendConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create Mikrotik controller for backend %s: %w", backendConfig.Id, err)
|
return fmt.Errorf("failed to create Mikrotik controller for backend %s: %w", backendConfig.Id, err)
|
||||||
}
|
}
|
||||||
|
@ -149,7 +149,7 @@ func (m Manager) ImportNewInterfaces(ctx context.Context, filter ...domain.Inter
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = m.importInterface(ctx, &physicalInterface, physicalPeers)
|
err = m.importInterface(ctx, wgBackend, &physicalInterface, physicalPeers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("import of %s failed: %w", physicalInterface.Identifier, err)
|
return 0, fmt.Errorf("import of %s failed: %w", physicalInterface.Identifier, err)
|
||||||
}
|
}
|
||||||
@ -770,7 +770,12 @@ func (m Manager) getFreshListenPort(ctx context.Context) (port int, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Manager) importInterface(ctx context.Context, in *domain.PhysicalInterface, peers []domain.PhysicalPeer) error {
|
func (m Manager) importInterface(
|
||||||
|
ctx context.Context,
|
||||||
|
backend InterfaceController,
|
||||||
|
in *domain.PhysicalInterface,
|
||||||
|
peers []domain.PhysicalPeer,
|
||||||
|
) error {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
iface := domain.ConvertPhysicalInterface(in)
|
iface := domain.ConvertPhysicalInterface(in)
|
||||||
iface.BaseModel = domain.BaseModel{
|
iface.BaseModel = domain.BaseModel{
|
||||||
@ -779,6 +784,7 @@ func (m Manager) importInterface(ctx context.Context, in *domain.PhysicalInterfa
|
|||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
iface.Backend = backend.GetId()
|
||||||
iface.PeerDefAllowedIPsStr = iface.AddressStr()
|
iface.PeerDefAllowedIPsStr = iface.AddressStr()
|
||||||
|
|
||||||
existingInterface, err := m.db.GetInterface(ctx, iface.Identifier)
|
existingInterface, err := m.db.GetInterface(ctx, iface.Identifier)
|
||||||
@ -843,6 +849,18 @@ func (m Manager) importPeer(ctx context.Context, in *domain.Interface, p *domain
|
|||||||
peer.DisplayName = "Autodetected Client (" + peer.Interface.PublicKey[0:8] + ")"
|
peer.DisplayName = "Autodetected Client (" + peer.Interface.PublicKey[0:8] + ")"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if p.BackendExtras != nil {
|
||||||
|
if val, ok := p.BackendExtras["MT-NAME"]; ok {
|
||||||
|
peer.DisplayName = val.(string)
|
||||||
|
}
|
||||||
|
if val, ok := p.BackendExtras["MT-COMMENT"]; ok {
|
||||||
|
peer.Notes = val.(string)
|
||||||
|
}
|
||||||
|
if val, ok := p.BackendExtras["MT-ENDPOINT"]; ok {
|
||||||
|
peer.Endpoint = domain.NewConfigOption(val.(string), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err := m.db.SavePeer(ctx, peer.Identifier, func(_ *domain.Peer) (*domain.Peer, error) {
|
err := m.db.SavePeer(ctx, peer.Identifier, func(_ *domain.Peer) (*domain.Peer, error) {
|
||||||
return peer, nil
|
return peer, nil
|
||||||
})
|
})
|
||||||
|
@ -2,6 +2,7 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const LocalBackendName = "local"
|
const LocalBackendName = "local"
|
||||||
@ -46,7 +47,11 @@ type BackendBase struct {
|
|||||||
type BackendMikrotik struct {
|
type BackendMikrotik struct {
|
||||||
BackendBase `yaml:",inline"` // Embed the base fields
|
BackendBase `yaml:",inline"` // Embed the base fields
|
||||||
|
|
||||||
ApiUrl string `yaml:"api_url"` // The base URL of the Mikrotik API (e.g., "https://10.10.10.10:8729/rest")
|
ApiUrl string `yaml:"api_url"` // The base URL of the Mikrotik API (e.g., "https://10.10.10.10:8729/rest")
|
||||||
ApiUser string `yaml:"api_user"`
|
ApiUser string `yaml:"api_user"`
|
||||||
ApiPassword string `yaml:"api_password"`
|
ApiPassword string `yaml:"api_password"`
|
||||||
|
ApiVerifyTls bool `yaml:"api_verify_tls"` // Whether to verify the TLS certificate of the Mikrotik API
|
||||||
|
ApiTimeout time.Duration `yaml:"api_timeout"` // Timeout for API requests (default: 30 seconds)
|
||||||
|
|
||||||
|
Debug bool `yaml:"debug"` // Enable debug logging for the Mikrotik backend
|
||||||
}
|
}
|
||||||
|
@ -181,6 +181,8 @@ type PhysicalPeer struct {
|
|||||||
|
|
||||||
BytesUpload uint64 // upload bytes are the number of bytes that the remote peer has sent to the server
|
BytesUpload uint64 // upload bytes are the number of bytes that the remote peer has sent to the server
|
||||||
BytesDownload uint64 // upload bytes are the number of bytes that the remote peer has received from the server
|
BytesDownload uint64 // upload bytes are the number of bytes that the remote peer has received from the server
|
||||||
|
|
||||||
|
BackendExtras map[string]any // additional backend specific extras, e.g. for the mikrotik backend this contains the name of the peer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p PhysicalPeer) GetPresharedKey() *wgtypes.Key {
|
func (p PhysicalPeer) GetPresharedKey() *wgtypes.Key {
|
||||||
|
@ -12,8 +12,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetupLogging initializes the global logger with the given level and format
|
// GetLoggingHandler initializes a slog.Handler based on the provided logging level and format options.
|
||||||
func SetupLogging(level string, pretty, json bool) {
|
func GetLoggingHandler(level string, pretty, json bool) slog.Handler {
|
||||||
var logLevel = new(slog.LevelVar)
|
var logLevel = new(slog.LevelVar)
|
||||||
|
|
||||||
switch strings.ToLower(level) {
|
switch strings.ToLower(level) {
|
||||||
@ -46,6 +46,13 @@ func SetupLogging(level string, pretty, json bool) {
|
|||||||
handler = slog.NewTextHandler(output, opts)
|
handler = slog.NewTextHandler(output, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupLogging initializes the global logger with the given level and format
|
||||||
|
func SetupLogging(level string, pretty, json bool) {
|
||||||
|
handler := GetLoggingHandler(level, pretty, json)
|
||||||
|
|
||||||
logger := slog.New(handler)
|
logger := slog.New(handler)
|
||||||
|
|
||||||
slog.SetDefault(logger)
|
slog.SetDefault(logger)
|
||||||
|
291
internal/lowlevel/mikrotik.go
Normal file
291
internal/lowlevel/mikrotik.go
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
package lowlevel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal"
|
||||||
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MikrotikApiResponse[T any] struct {
|
||||||
|
Status string
|
||||||
|
Code int
|
||||||
|
Data T `json:"data,omitempty"`
|
||||||
|
Error *MikrotikApiError `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MikrotikApiError struct {
|
||||||
|
Code int `json:"error,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
Details string `json:"details,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e MikrotikApiError) String() string {
|
||||||
|
return fmt.Sprintf("API error %d: %s - %s", e.Code, e.Message, e.Details)
|
||||||
|
}
|
||||||
|
|
||||||
|
type GenericJsonObject map[string]any
|
||||||
|
|
||||||
|
func (JsonObject GenericJsonObject) GetString(key string) string {
|
||||||
|
if value, ok := JsonObject[key]; ok {
|
||||||
|
if strValue, ok := value.(string); ok {
|
||||||
|
return strValue
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("%v", value) // Convert to string if not already
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (JsonObject GenericJsonObject) GetInt(key string) int {
|
||||||
|
if value, ok := JsonObject[key]; ok {
|
||||||
|
if intValue, ok := value.(int); ok {
|
||||||
|
return intValue
|
||||||
|
} else {
|
||||||
|
if floatValue, ok := value.(float64); ok {
|
||||||
|
return int(floatValue) // Convert float64 to int
|
||||||
|
}
|
||||||
|
if strValue, ok := value.(string); ok {
|
||||||
|
if intValue, err := strconv.Atoi(strValue); err == nil {
|
||||||
|
return intValue // Convert string to int if possible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (JsonObject GenericJsonObject) GetBool(key string) bool {
|
||||||
|
if value, ok := JsonObject[key]; ok {
|
||||||
|
if boolValue, ok := value.(bool); ok {
|
||||||
|
return boolValue
|
||||||
|
} else {
|
||||||
|
if intValue, ok := value.(int); ok {
|
||||||
|
return intValue == 1 // Convert int to bool (1 is true, 0 is false)
|
||||||
|
}
|
||||||
|
if floatValue, ok := value.(float64); ok {
|
||||||
|
return int(floatValue) == 1 // Convert float64 to bool (1.0 is true, 0.0 is false)
|
||||||
|
}
|
||||||
|
if strValue, ok := value.(string); ok {
|
||||||
|
boolValue, err := strconv.ParseBool(strValue)
|
||||||
|
if err == nil {
|
||||||
|
return boolValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type MikrotikRequestOptions struct {
|
||||||
|
Filters map[string]string `json:"filters,omitempty"`
|
||||||
|
PropList []string `json:"proplist,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *MikrotikRequestOptions) GetPath(base string) string {
|
||||||
|
if o == nil {
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := url.Parse(base)
|
||||||
|
if err != nil {
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
|
query := path.Query()
|
||||||
|
for k, v := range o.Filters {
|
||||||
|
query.Set(k, v)
|
||||||
|
}
|
||||||
|
if len(o.PropList) > 0 {
|
||||||
|
query.Set(".proplist", strings.Join(o.PropList, ","))
|
||||||
|
}
|
||||||
|
path.RawQuery = query.Encode()
|
||||||
|
return path.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type MikrotikApiClient struct {
|
||||||
|
coreCfg *config.Config
|
||||||
|
cfg *config.BackendMikrotik
|
||||||
|
|
||||||
|
client *http.Client
|
||||||
|
log *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMikrotikApiClient(coreCfg *config.Config, cfg *config.BackendMikrotik) (*MikrotikApiClient, error) {
|
||||||
|
c := &MikrotikApiClient{
|
||||||
|
coreCfg: coreCfg,
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.ApiTimeout == 0 {
|
||||||
|
cfg.ApiTimeout = 30 * time.Second // Default timeout for API requests
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.setup()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.debugLog("Mikrotik api client created", "api_url", cfg.ApiUrl)
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MikrotikApiClient) setup() error {
|
||||||
|
m.client = &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: !m.cfg.ApiVerifyTls,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Timeout: m.cfg.ApiTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.cfg.Debug {
|
||||||
|
m.log = slog.New(internal.GetLoggingHandler("debug",
|
||||||
|
m.coreCfg.Advanced.LogPretty,
|
||||||
|
m.coreCfg.Advanced.LogJson).
|
||||||
|
WithAttrs([]slog.Attr{
|
||||||
|
{
|
||||||
|
Key: "mikrotik-bid", Value: slog.StringValue(m.cfg.Id),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MikrotikApiClient) debugLog(msg string, args ...any) {
|
||||||
|
if m.log != nil {
|
||||||
|
m.log.Debug("[MT-API] "+msg, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MikrotikApiClient) getFullPath(command string) string {
|
||||||
|
path, err := url.JoinPath(m.cfg.ApiUrl, command)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MikrotikApiClient) prepareGetRequest(ctx context.Context, fullUrl string) (*http.Request, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", fullUrl, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
if m.cfg.ApiUser != "" && m.cfg.ApiPassword != "" {
|
||||||
|
req.SetBasicAuth(m.cfg.ApiUser, m.cfg.ApiPassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
MikrotikApiStatusOk = "success"
|
||||||
|
MikrotikApiStatusError = "error"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MikrotikApiErrorCodeUnknown = iota + 600
|
||||||
|
MikrotikApiErrorCodeRequestPreparationFailed
|
||||||
|
MikrotikApiErrorCodeRequestFailed
|
||||||
|
MikrotikApiErrorCodeResponseDecodeFailed
|
||||||
|
)
|
||||||
|
|
||||||
|
func errToApiResponse[T any](code int, message string, err error) MikrotikApiResponse[T] {
|
||||||
|
return MikrotikApiResponse[T]{
|
||||||
|
Status: MikrotikApiStatusError,
|
||||||
|
Code: code,
|
||||||
|
Error: &MikrotikApiError{
|
||||||
|
Code: code,
|
||||||
|
Message: message,
|
||||||
|
Details: err.Error(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHttpResponse[T any](resp *http.Response, err error) MikrotikApiResponse[T] {
|
||||||
|
if err != nil {
|
||||||
|
return errToApiResponse[T](MikrotikApiErrorCodeRequestFailed, "failed to execute request", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func(Body io.ReadCloser) {
|
||||||
|
err := Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to close response body", "error", err)
|
||||||
|
}
|
||||||
|
}(resp.Body)
|
||||||
|
|
||||||
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||||
|
var data T
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||||
|
return errToApiResponse[T](MikrotikApiErrorCodeResponseDecodeFailed, "failed to decode response", err)
|
||||||
|
}
|
||||||
|
return MikrotikApiResponse[T]{Status: MikrotikApiStatusOk, Code: resp.StatusCode, Data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiErr MikrotikApiError
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&apiErr); err != nil {
|
||||||
|
return errToApiResponse[T](resp.StatusCode, "unknown error, unparsable response", err)
|
||||||
|
} else {
|
||||||
|
return MikrotikApiResponse[T]{Status: MikrotikApiStatusError, Code: resp.StatusCode, Error: &apiErr}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MikrotikApiClient) Query(
|
||||||
|
ctx context.Context,
|
||||||
|
command string,
|
||||||
|
opts *MikrotikRequestOptions,
|
||||||
|
) MikrotikApiResponse[[]GenericJsonObject] {
|
||||||
|
apiCtx, cancel := context.WithTimeout(ctx, m.cfg.ApiTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
fullUrl := opts.GetPath(m.getFullPath(command))
|
||||||
|
|
||||||
|
req, err := m.prepareGetRequest(apiCtx, fullUrl)
|
||||||
|
if err != nil {
|
||||||
|
return errToApiResponse[[]GenericJsonObject](MikrotikApiErrorCodeRequestPreparationFailed,
|
||||||
|
"failed to create request", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
m.debugLog("executing API query", "url", fullUrl)
|
||||||
|
response := parseHttpResponse[[]GenericJsonObject](m.client.Do(req))
|
||||||
|
m.debugLog("retrieved API query result", "url", fullUrl, "duration", time.Since(start).String())
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MikrotikApiClient) Get(
|
||||||
|
ctx context.Context,
|
||||||
|
command string,
|
||||||
|
opts *MikrotikRequestOptions,
|
||||||
|
) MikrotikApiResponse[GenericJsonObject] {
|
||||||
|
apiCtx, cancel := context.WithTimeout(ctx, m.cfg.ApiTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
fullUrl := opts.GetPath(m.getFullPath(command))
|
||||||
|
|
||||||
|
req, err := m.prepareGetRequest(apiCtx, fullUrl)
|
||||||
|
if err != nil {
|
||||||
|
return errToApiResponse[GenericJsonObject](MikrotikApiErrorCodeRequestPreparationFailed,
|
||||||
|
"failed to create request", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
m.debugLog("executing API get", "url", fullUrl)
|
||||||
|
response := parseHttpResponse[GenericJsonObject](m.client.Do(req))
|
||||||
|
m.debugLog("retrieved API get result", "url", fullUrl, "duration", time.Since(start).String())
|
||||||
|
return response
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user