mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-16 08:15:05 +00:00
132 lines
4.7 KiB
Go
132 lines
4.7 KiB
Go
package bitrix24
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// Sentinel errors from derivePublicURL. Callers log-and-continue: capture
|
|
// failure must NOT block install completion (tokens are already minted by the
|
|
// time we get here).
|
|
var (
|
|
errPublicURLEmpty = errors.New("derivePublicURL: empty host")
|
|
errPublicURLPrivate = errors.New("derivePublicURL: host is private/loopback")
|
|
)
|
|
|
|
// derivePublicURL extracts the gateway's externally reachable URL from a
|
|
// request that hit the install handler. Bitrix24 only invokes our install
|
|
// endpoint via the URL the portal admin pasted into the application config —
|
|
// so r.Host (or X-Forwarded-Host behind a reverse proxy) is by construction
|
|
// the URL Bitrix24 will use for all subsequent event callbacks.
|
|
//
|
|
// Scheme resolution priority:
|
|
// 1. X-Forwarded-Proto header (set by Cloudflare Tunnel / nginx), normalized:
|
|
// "http"/"https" pass through as-is; "ws"/"wss" map to "http"/"https"
|
|
// (same transport, describes a WebSocket upgrade rather than a scheme
|
|
// change); any other value is ignored.
|
|
// 2. r.TLS != nil → https
|
|
// 3. Otherwise → http (honest to what we observed). If a reverse proxy is
|
|
// terminating TLS without forwarding the proto header, fix the proxy
|
|
// config — guessing https here would silently mask the misconfiguration.
|
|
// Downstream imbot.register will reject http URLs explicitly.
|
|
//
|
|
// Host resolution priority:
|
|
// 1. X-Forwarded-Host (reverse proxy)
|
|
// 2. r.Host (direct connect)
|
|
//
|
|
// Private/loopback hosts are rejected to prevent the operator from
|
|
// accidentally pinning the portal to a URL Bitrix24 cannot reach (e.g.
|
|
// authorizing via a Tailscale URL when the public ingress is elsewhere).
|
|
func derivePublicURL(r *http.Request) (string, error) {
|
|
scheme := "https"
|
|
if r.TLS == nil {
|
|
scheme = "http"
|
|
}
|
|
if proto := strings.ToLower(strings.TrimSpace(r.Header.Get("X-Forwarded-Proto"))); proto != "" {
|
|
switch proto {
|
|
case "http", "https":
|
|
scheme = proto
|
|
case "ws":
|
|
scheme = "http"
|
|
case "wss":
|
|
scheme = "https"
|
|
}
|
|
// any other value is left ignored — keeps the TLS-derived default
|
|
}
|
|
|
|
host := strings.TrimSpace(r.Header.Get("X-Forwarded-Host"))
|
|
if host == "" {
|
|
host = strings.TrimSpace(r.Host)
|
|
}
|
|
if host == "" {
|
|
return "", errPublicURLEmpty
|
|
}
|
|
|
|
// X-Forwarded-Host may contain a comma-separated list (RFC 7239 style); take
|
|
// the first hop, which is the original client-facing host.
|
|
if idx := strings.Index(host, ","); idx >= 0 {
|
|
host = strings.TrimSpace(host[:idx])
|
|
}
|
|
|
|
// Strip port for the privacy check but keep the original host (with port)
|
|
// for the URL string — a non-standard port is legitimate (e.g. dev tunnel).
|
|
hostOnly := host
|
|
if h, _, err := net.SplitHostPort(host); err == nil {
|
|
hostOnly = h
|
|
}
|
|
if isPrivateOrLoopback(hostOnly) {
|
|
return "", errPublicURLPrivate
|
|
}
|
|
|
|
return scheme + "://" + host, nil
|
|
}
|
|
|
|
// isPrivateOrLoopback reports whether host (no port) refers to a network
|
|
// location that Bitrix24-side cannot reach. Hostnames that aren't literal IPs
|
|
// are assumed public — we don't DNS-resolve to inspect the underlying address
|
|
// because the lookup adds latency and a dev "localtunnel.example.com" pointing
|
|
// to 127.0.0.1 is still a valid public URL from Bitrix24's perspective.
|
|
func isPrivateOrLoopback(host string) bool {
|
|
host = strings.ToLower(strings.TrimSpace(host))
|
|
if host == "" || host == "localhost" || strings.HasSuffix(host, ".localhost") {
|
|
return true
|
|
}
|
|
ip := net.ParseIP(host)
|
|
if ip == nil {
|
|
return false
|
|
}
|
|
return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsUnspecified()
|
|
}
|
|
|
|
// capturePublicURL is the install-handler glue: derive the gateway URL from
|
|
// the incoming request and persist it on the portal. Both errors and successes
|
|
// are best-effort logged; the caller continues regardless because the install
|
|
// (tokens) has already succeeded by the time we get here.
|
|
//
|
|
// Optionally promotes the URL into the gateway-wide snapshot when promote is
|
|
// non-nil. This path is trustworthy because /bitrix24/install only succeeds
|
|
// after Bitrix24's own OAuth-state validation — a forged Host header alone
|
|
// can't reach this code path.
|
|
func capturePublicURL(ctx context.Context, portal *Portal, req *http.Request, promote func(string)) {
|
|
url, err := derivePublicURL(req)
|
|
if err != nil {
|
|
slog.Warn("bitrix24 install: derive public_url failed",
|
|
"tenant", portal.TenantID(), "portal", portal.Name(),
|
|
"host", req.Host, "x_forwarded_host", req.Header.Get("X-Forwarded-Host"),
|
|
"err", err)
|
|
return
|
|
}
|
|
if err := portal.UpdatePublicURL(ctx, url); err != nil {
|
|
slog.Warn("bitrix24 install: persist public_url failed",
|
|
"tenant", portal.TenantID(), "portal", portal.Name(),
|
|
"url", url, "err", err)
|
|
}
|
|
if promote != nil {
|
|
promote(url)
|
|
}
|
|
}
|