193 lines
5.2 KiB
Go
193 lines
5.2 KiB
Go
|
package opnsense
|
||
|
|
||
|
import (
|
||
|
"crypto/tls"
|
||
|
"crypto/x509"
|
||
|
"encoding/json"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"net/http"
|
||
|
"regexp"
|
||
|
"runtime"
|
||
|
"time"
|
||
|
|
||
|
"github.com/go-kit/log"
|
||
|
"github.com/go-kit/log/level"
|
||
|
)
|
||
|
|
||
|
// MaxRetries is the maximum number of retries
|
||
|
// when a request to the OPNsense API fails
|
||
|
const MaxRetries = 3
|
||
|
|
||
|
// EndpointName is the custom type for name of an endpoint definition
|
||
|
type EndpointName string
|
||
|
|
||
|
// EndpointPath is the custom type for url path of an endpoint definition
|
||
|
type EndpointPath string
|
||
|
|
||
|
// Client is an OPNsense API client
|
||
|
type Client struct {
|
||
|
log log.Logger
|
||
|
baseURL string
|
||
|
key string
|
||
|
secret string
|
||
|
sslInsecure bool
|
||
|
endpoints map[EndpointName]EndpointPath
|
||
|
httpClient *http.Client
|
||
|
headers map[string]string
|
||
|
gatewayLossRegex *regexp.Regexp
|
||
|
gatewayRTTRegex *regexp.Regexp
|
||
|
}
|
||
|
|
||
|
// NewClient creates a new OPNsense API Client
|
||
|
func NewClient(protocol, adress, key, secret, userAgentVersion string, sslInsecure bool, log log.Logger) (Client, error) {
|
||
|
|
||
|
sslPool, err := x509.SystemCertPool()
|
||
|
|
||
|
if err != nil {
|
||
|
return Client{}, errors.Join(fmt.Errorf("failed to load system cert pool"), err)
|
||
|
}
|
||
|
|
||
|
gatewayLossRegex, err := regexp.Compile(`\d\.\d %`)
|
||
|
|
||
|
if err != nil {
|
||
|
return Client{}, errors.Join(fmt.Errorf("failed to build regex for gatewayLoss calculation"), err)
|
||
|
}
|
||
|
|
||
|
gatewayRTTRegex, err := regexp.Compile(`\d+\.\d+ ms`)
|
||
|
if err != nil {
|
||
|
return Client{}, errors.Join(fmt.Errorf("failed to build regex for gatewayRTT calculation"), err)
|
||
|
}
|
||
|
client := Client{
|
||
|
log: log,
|
||
|
baseURL: fmt.Sprintf("%s://%s", protocol, adress),
|
||
|
key: key,
|
||
|
secret: secret,
|
||
|
gatewayLossRegex: gatewayLossRegex,
|
||
|
gatewayRTTRegex: gatewayRTTRegex,
|
||
|
endpoints: map[EndpointName]EndpointPath{
|
||
|
"services": "api/core/service/search",
|
||
|
"protocolStatistics": "api/diagnostics/interface/getProtocolStatistics",
|
||
|
"arp": "api/diagnostics/interface/search_arp",
|
||
|
"dhcpv4": "api/dhcpv4/leases/searchLease",
|
||
|
"openVPNInstances": "api/openvpn/instances/search",
|
||
|
"interfaces": "api/diagnostics/traffic/interface",
|
||
|
"systemInfo": "widgets/api/get.php?load=system%2Ctemperature",
|
||
|
"gatewaysStatus": "api/routes/gateway/status",
|
||
|
"unboundDNSStatus": "api/unbound/diagnostics/stats",
|
||
|
"cronJobs": "api/cron/settings/searchJobs",
|
||
|
},
|
||
|
headers: map[string]string{
|
||
|
"Accept": "application/json",
|
||
|
"User-Agent": fmt.Sprintf("prometheus-opnsense-exporter/%s", userAgentVersion),
|
||
|
"Accept-Encoding": "gzip, deflate, br",
|
||
|
},
|
||
|
sslInsecure: sslInsecure,
|
||
|
httpClient: &http.Client{
|
||
|
Timeout: 10 * time.Second,
|
||
|
Transport: &http.Transport{
|
||
|
TLSClientConfig: &tls.Config{
|
||
|
InsecureSkipVerify: sslInsecure,
|
||
|
RootCAs: sslPool,
|
||
|
},
|
||
|
IdleConnTimeout: 90 * time.Second,
|
||
|
TLSHandshakeTimeout: 1 * time.Second,
|
||
|
ExpectContinueTimeout: 1 * time.Second,
|
||
|
ForceAttemptHTTP2: true,
|
||
|
MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1,
|
||
|
},
|
||
|
},
|
||
|
}
|
||
|
|
||
|
return client, nil
|
||
|
}
|
||
|
|
||
|
// Endpoints returns a map of all the endpoints
|
||
|
// that are called by the client.
|
||
|
func (c *Client) Endpoints() map[EndpointName]EndpointPath {
|
||
|
return c.endpoints
|
||
|
}
|
||
|
|
||
|
// do sends a request to the OPNsense API.
|
||
|
// The response is unmarshalled
|
||
|
// into the responseStruc
|
||
|
func (c *Client) do(method string, path EndpointPath, body io.Reader, responseStruct any) *APICallError {
|
||
|
|
||
|
url := fmt.Sprintf("%s/%s", c.baseURL, string(path))
|
||
|
|
||
|
req, err := http.NewRequest(method, url, body)
|
||
|
|
||
|
if err != nil {
|
||
|
return &APICallError{
|
||
|
Endpoint: string(path),
|
||
|
Message: err.Error(),
|
||
|
StatusCode: 0,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
req.SetBasicAuth(c.key, c.secret)
|
||
|
|
||
|
for k, v := range c.headers {
|
||
|
req.Header.Add(k, v)
|
||
|
}
|
||
|
|
||
|
if method == "POST" {
|
||
|
req.Header.Add("Content-Type", "application/json;charset=utf-8")
|
||
|
}
|
||
|
|
||
|
level.Debug(c.log).
|
||
|
Log("msg", "fetching data", "component", "opnsense-client", "url", url, "method", method)
|
||
|
|
||
|
// Retry the request up to MaxRetries times
|
||
|
for i := 0; i < MaxRetries; i++ {
|
||
|
resp, err := c.httpClient.Do(req)
|
||
|
if err != nil {
|
||
|
level.Error(c.log).
|
||
|
Log("msg", "failed to send request; retrying",
|
||
|
"component", "opnsense-client",
|
||
|
"err", err.Error())
|
||
|
time.Sleep(25 * time.Millisecond)
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
body, err := io.ReadAll(resp.Body)
|
||
|
|
||
|
if err != nil {
|
||
|
return &APICallError{
|
||
|
Endpoint: string(path),
|
||
|
Message: fmt.Sprintf("failed to read response body: %s", err.Error()),
|
||
|
StatusCode: resp.StatusCode,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
resp.Body.Close()
|
||
|
|
||
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||
|
err := json.Unmarshal(body, &responseStruct)
|
||
|
if err != nil {
|
||
|
fmt.Println(url)
|
||
|
fmt.Println(string(body))
|
||
|
return &APICallError{
|
||
|
Endpoint: string(path),
|
||
|
Message: fmt.Sprintf("failed to unmarshal response body: %s", err.Error()),
|
||
|
StatusCode: resp.StatusCode,
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
} else {
|
||
|
return &APICallError{
|
||
|
Endpoint: string(path),
|
||
|
Message: string(body),
|
||
|
StatusCode: resp.StatusCode,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|
||
|
return &APICallError{
|
||
|
Endpoint: string(path),
|
||
|
Message: fmt.Sprintf("max retries of %d times reached", MaxRetries),
|
||
|
StatusCode: 0,
|
||
|
}
|
||
|
}
|