opnsense-exporter/opnsense/client.go

193 lines
5.2 KiB
Go
Raw Normal View History

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, address, 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, address),
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,
}
}