Public source validation / validate (push) Failing after 3m8s
134 lines
4.0 KiB
Go
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"`
|
|
}
|