mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-10 19:59:04 +02:00
116 lines
3.6 KiB
Go
116 lines
3.6 KiB
Go
package outbound
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/ory/fosite"
|
|
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
|
|
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
|
)
|
|
|
|
const envVarPrefix = "OUTBOUND_ALLOWED_HOSTS_"
|
|
|
|
// Purpose identifies a feature that sends requests to URLs that admins or third parties control
|
|
type Purpose string
|
|
|
|
const (
|
|
PurposeClientLogo Purpose = "client_logo"
|
|
PurposeClientMetadata Purpose = "client_metadata"
|
|
PurposeSCIM Purpose = "scim"
|
|
PurposeBackchannelLogout Purpose = "backchannel_logout"
|
|
PurposeFederatedJWKS Purpose = "federated_jwks"
|
|
PurposeLDAPPicture Purpose = "ldap_picture"
|
|
)
|
|
|
|
var AllPurposes = []Purpose{
|
|
PurposeClientLogo,
|
|
PurposeClientMetadata,
|
|
PurposeSCIM,
|
|
PurposeBackchannelLogout,
|
|
PurposeFederatedJWKS,
|
|
PurposeLDAPPicture,
|
|
}
|
|
|
|
func (p Purpose) EnvVar() string {
|
|
return envVarPrefix + strings.ToUpper(string(p))
|
|
}
|
|
|
|
func (p Purpose) allowlist(config *common.EnvConfigSchema) string {
|
|
switch p {
|
|
case PurposeClientLogo:
|
|
return config.OutboundAllowedHostsClientLogo
|
|
case PurposeClientMetadata:
|
|
return config.OutboundAllowedHostsClientMetadata
|
|
case PurposeSCIM:
|
|
return config.OutboundAllowedHostsSCIM
|
|
case PurposeBackchannelLogout:
|
|
return config.OutboundAllowedHostsBackchannelLogout
|
|
case PurposeFederatedJWKS:
|
|
return config.OutboundAllowedHostsFederatedJWKS
|
|
case PurposeLDAPPicture:
|
|
return config.OutboundAllowedHostsLDAPPicture
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// Clients holds one guarded transport and client per purpose
|
|
type Clients struct {
|
|
transports map[Purpose]http.RoundTripper
|
|
clients map[Purpose]*http.Client
|
|
}
|
|
|
|
// New builds the guarded clients from the OUTBOUND_ALLOWED_HOSTS_* environment variables
|
|
func New(config *common.EnvConfigSchema) (*Clients, error) {
|
|
// LOCAL_IPV6_RANGES are blocked like private ranges and opened up by the "private" keyword
|
|
localIPv6, err := ParseLocalIPv6Ranges(config.LocalIPv6Ranges)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid LOCAL_IPV6_RANGES: %w", err)
|
|
}
|
|
|
|
c := &Clients{
|
|
transports: make(map[Purpose]http.RoundTripper, len(AllPurposes)),
|
|
clients: make(map[Purpose]*http.Client, len(AllPurposes)),
|
|
}
|
|
for _, purpose := range AllPurposes {
|
|
policy, err := ParsePolicy(purpose.allowlist(config), localIPv6)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid %s: %w", purpose.EnvVar(), err)
|
|
}
|
|
|
|
// The operator configured the icon library, which lets a self-hosted mirror live on the local network
|
|
if purpose == PurposeClientLogo && config.IconLibraryEnabled() {
|
|
if u, err := url.Parse(config.IconLibraryURL); err == nil && u.Hostname() != "" {
|
|
policy.allowedHosts = append(policy.allowedHosts, normalizeHostname(u.Hostname()))
|
|
}
|
|
}
|
|
|
|
// Preserve Fosite's header limit before wrapping the transport so it applies to trusted hosts too
|
|
var base *http.Transport
|
|
if purpose == PurposeClientMetadata {
|
|
base = http.DefaultTransport.(*http.Transport).Clone() //nolint:forcetypeassert // The default transport is always an *http.Transport
|
|
base.MaxResponseHeaderBytes = fosite.DefaultCIMDMaxSize * 8
|
|
}
|
|
|
|
transport := NewTransport(base, purpose, policy)
|
|
c.transports[purpose] = transport
|
|
c.clients[purpose] = &http.Client{Transport: otelhttp.NewTransport(transport)}
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
// Client returns the guarded HTTP client for the purpose, instrumented with OpenTelemetry
|
|
func (c *Clients) Client(purpose Purpose) *http.Client {
|
|
return c.clients[purpose]
|
|
}
|
|
|
|
// Transport returns the guarded transport for the purpose without instrumentation, for callers that add their own
|
|
func (c *Clients) Transport(purpose Purpose) http.RoundTripper {
|
|
return c.transports[purpose]
|
|
}
|