mirror of
https://github.com/h44z/wg-portal.git
synced 2025-06-28 01:07:03 +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
|
||||
}
|
||||
|
||||
func (c LocalController) GetId() domain.InterfaceBackend {
|
||||
return config.LocalBackendName
|
||||
}
|
||||
|
||||
// region wireguard-related
|
||||
|
||||
func (c LocalController) GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error) {
|
||||
|
@ -2,38 +2,261 @@ package wgcontroller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
"github.com/h44z/wg-portal/internal/lowlevel"
|
||||
)
|
||||
|
||||
type MikrotikController struct {
|
||||
coreCfg *config.Config
|
||||
cfg *config.BackendMikrotik
|
||||
|
||||
client *lowlevel.MikrotikApiClient
|
||||
}
|
||||
|
||||
func NewMikrotikController() (*MikrotikController, error) {
|
||||
return &MikrotikController{}, nil
|
||||
func NewMikrotikController(coreCfg *config.Config, cfg *config.BackendMikrotik) (*MikrotikController, error) {
|
||||
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
|
||||
|
||||
func (c MikrotikController) GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error) {
|
||||
// TODO implement me
|
||||
panic("implement me")
|
||||
func (c MikrotikController) GetInterfaces(ctx context.Context) ([]domain.PhysicalInterface, error) {
|
||||
wgReply := c.client.Query(ctx, "/interface/wireguard", &lowlevel.MikrotikRequestOptions{
|
||||
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,
|
||||
error,
|
||||
) {
|
||||
// TODO implement me
|
||||
panic("implement me")
|
||||
wgReply := c.client.Query(ctx, "/interface/wireguard", &lowlevel.MikrotikRequestOptions{
|
||||
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,
|
||||
error,
|
||||
) {
|
||||
// TODO implement me
|
||||
panic("implement me")
|
||||
wgReply := c.client.Query(ctx, "/interface/wireguard/peers", &lowlevel.MikrotikRequestOptions{
|
||||
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(
|
||||
@ -42,12 +265,12 @@ func (c MikrotikController) SaveInterface(
|
||||
updateFunc func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error),
|
||||
) error {
|
||||
// TODO implement me
|
||||
panic("implement me")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c MikrotikController) DeleteInterface(_ context.Context, id domain.InterfaceIdentifier) error {
|
||||
// TODO implement me
|
||||
panic("implement me")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c MikrotikController) SavePeer(
|
||||
@ -57,7 +280,7 @@ func (c MikrotikController) SavePeer(
|
||||
updateFunc func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error),
|
||||
) error {
|
||||
// TODO implement me
|
||||
panic("implement me")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c MikrotikController) DeletePeer(
|
||||
@ -66,7 +289,7 @@ func (c MikrotikController) DeletePeer(
|
||||
id domain.PeerIdentifier,
|
||||
) error {
|
||||
// TODO implement me
|
||||
panic("implement me")
|
||||
return nil
|
||||
}
|
||||
|
||||
// endregion wireguard-related
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
)
|
||||
|
||||
type InterfaceController interface {
|
||||
GetId() domain.InterfaceBackend
|
||||
GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error)
|
||||
GetInterface(_ context.Context, id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error)
|
||||
GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier) ([]domain.PhysicalPeer, error)
|
||||
@ -92,7 +93,7 @@ func (c *ControllerManager) registerMikrotikControllers() error {
|
||||
continue
|
||||
}
|
||||
|
||||
controller, err := wgcontroller.NewMikrotikController() // TODO: Pass backendConfig to the controller constructor
|
||||
controller, err := wgcontroller.NewMikrotikController(c.cfg, &backendConfig)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
err = m.importInterface(ctx, &physicalInterface, physicalPeers)
|
||||
err = m.importInterface(ctx, wgBackend, &physicalInterface, physicalPeers)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
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()
|
||||
iface := domain.ConvertPhysicalInterface(in)
|
||||
iface.BaseModel = domain.BaseModel{
|
||||
@ -779,6 +784,7 @@ func (m Manager) importInterface(ctx context.Context, in *domain.PhysicalInterfa
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
iface.Backend = backend.GetId()
|
||||
iface.PeerDefAllowedIPsStr = iface.AddressStr()
|
||||
|
||||
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] + ")"
|
||||
}
|
||||
|
||||
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) {
|
||||
return peer, nil
|
||||
})
|
||||
|
@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
const LocalBackendName = "local"
|
||||
@ -46,7 +47,11 @@ type BackendBase struct {
|
||||
type BackendMikrotik struct {
|
||||
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")
|
||||
ApiUser string `yaml:"api_user"`
|
||||
ApiPassword string `yaml:"api_password"`
|
||||
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"`
|
||||
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
|
||||
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 {
|
||||
|
@ -12,8 +12,8 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// SetupLogging initializes the global logger with the given level and format
|
||||
func SetupLogging(level string, pretty, json bool) {
|
||||
// GetLoggingHandler initializes a slog.Handler based on the provided logging level and format options.
|
||||
func GetLoggingHandler(level string, pretty, json bool) slog.Handler {
|
||||
var logLevel = new(slog.LevelVar)
|
||||
|
||||
switch strings.ToLower(level) {
|
||||
@ -46,6 +46,13 @@ func SetupLogging(level string, pretty, json bool) {
|
||||
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)
|
||||
|
||||
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