Files
ITWorx Pulse release export bd774932d5
Public source validation / validate (push) Failing after 3m8s
Publish ITWorx Pulse source
2026-09-03 02:09:19 +02:00

134 lines
4.0 KiB
Go

package reverseproxy
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
type TokenSource interface {
Token(context.Context) (string, error)
}
type NPMClient struct {
BaseURL string
Token TokenSource
HTTPClient *http.Client
MaxBody int64
Now func() time.Time
}
func (c NPMClient) ListRoutes(ctx context.Context) ([]RawRoute, error) {
if ctx == nil {
return nil, errors.New("reverse proxy context is nil")
}
if err := ctx.Err(); err != nil {
return nil, err
}
base, err := url.Parse(strings.TrimSpace(c.BaseURL))
if err != nil || (base.Scheme != "http" && base.Scheme != "https") || base.Host == "" || base.User != nil || base.RawQuery != "" || base.Fragment != "" {
return nil, errors.New("nginx proxy manager base URL is invalid")
}
token := ""
if c.Token != nil {
token, err = c.Token.Token(ctx)
if err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return nil, err
}
return nil, errors.New("nginx proxy manager credential unavailable")
}
}
client := c.HTTPClient
if client == nil {
client = http.DefaultClient
}
requestURL := strings.TrimRight(base.String(), "/") + "/api/nginx/proxy-hosts"
request, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
if err != nil {
return nil, errors.New("create nginx proxy manager request")
}
request.Header.Set("Accept", "application/json")
if strings.TrimSpace(token) != "" {
request.Header.Set("Authorization", "Bearer "+token)
}
response, err := client.Do(request)
if err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return nil, err
}
return nil, errors.New("nginx proxy manager request failed")
}
defer response.Body.Close()
maxBody := c.MaxBody
if maxBody == 0 {
maxBody = 2 << 20
}
if maxBody < 1024 || maxBody > 8<<20 {
return nil, errors.New("nginx proxy manager body limit is outside bounds")
}
if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {
return nil, fmt.Errorf("nginx proxy manager returned status %d", response.StatusCode)
}
body, err := io.ReadAll(io.LimitReader(response.Body, maxBody+1))
if err != nil {
return nil, errors.New("read nginx proxy manager response")
}
if int64(len(body)) > maxBody {
return nil, errors.New("nginx proxy manager response exceeds bounds")
}
var hosts []npmHost
if err := json.Unmarshal(body, &hosts); err != nil {
return nil, errors.New("decode nginx proxy manager response")
}
now := time.Now().UTC()
if c.Now != nil {
now = c.Now().UTC()
}
routes := make([]RawRoute, 0, len(hosts))
for _, host := range hosts {
if len(host.DomainNames) > 32 {
return nil, errors.New("nginx proxy manager hostnames exceed bounds")
}
id := string(host.ID)
if decoded, decodeErr := strconv.Unquote(id); decodeErr == nil {
id = decoded
}
if strings.TrimSpace(id) == "" || len(id) > 128 {
return nil, errors.New("nginx proxy manager route id is invalid")
}
scheme := strings.ToLower(strings.TrimSpace(host.ForwardScheme))
if scheme == "" {
scheme = "http"
}
targetServiceID := ""
if value, ok := host.Meta["pulse_service_id"].(string); ok {
targetServiceID = value
}
for _, domain := range host.DomainNames {
routes = append(routes, RawRoute{ID: id + ":" + domain, SourceID: "npm", Hostname: domain, Scheme: scheme, Port: host.Port, TargetServiceID: targetServiceID, TargetHost: host.ForwardHost, Enabled: host.Enabled, Description: "Nginx Proxy Manager", ObservedAt: now})
}
}
if len(routes) > 1000 {
return nil, errors.New("nginx proxy manager route count exceeds bounds")
}
return routes, nil
}
type npmHost struct {
ID json.RawMessage `json:"id"`
DomainNames []string `json:"domain_names"`
ForwardScheme string `json:"forward_scheme"`
ForwardHost string `json:"forward_host"`
Port int `json:"forward_port"`
Enabled bool `json:"enabled"`
Meta map[string]any `json:"meta"`
}