Skip to content

Commit abd5f04

Browse files
shaunnkhanory-bot
authored andcommitted
feat: domain telemetry improvements
GitOrigin-RevId: 9a0825160976ff16b7a39024e650ecfaf9ce82a5
1 parent 60679fb commit abd5f04

File tree

4 files changed

+186
-28
lines changed

4 files changed

+186
-28
lines changed

.reports/dep-licenses.csv

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,4 @@
33
"github.com/ory/x","Apache-2.0"
44
"github.com/stretchr/testify","MIT"
55
"go.opentelemetry.io/otel/sdk","Apache-2.0"
6-
"go.opentelemetry.io/otel/sdk","BSD-3-Clause"
76

cmd/server/handler.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -215,11 +215,24 @@ func publicServer(ctx context.Context, d *driver.RegistrySQL, sqaMetrics *metric
215215
}
216216

217217
func sqa(ctx context.Context, d *driver.RegistrySQL, cmd *cobra.Command) *metricsx.Service {
218-
// Safely retrieve public issuer url from config
219-
var issuerURL string
220-
if u := d.Config().IssuerURL(ctx); u != nil {
221-
issuerURL = u.Host
218+
urls := []string{
219+
d.Config().IssuerURL(ctx).Host,
220+
d.Config().PublicURL(ctx).Host,
221+
d.Config().AdminURL(ctx).Host,
222+
d.Config().ServePublic(ctx).BaseURL.Host,
223+
d.Config().ServeAdmin(ctx).BaseURL.Host,
224+
d.Config().LoginURL(ctx).Host,
225+
d.Config().LogoutURL(ctx).Host,
226+
d.Config().ConsentURL(ctx).Host,
227+
d.Config().RegistrationURL(ctx).Host,
222228
}
229+
if c, y := d.Config().CORSPublic(ctx); y {
230+
urls = append(urls, c.AllowedOrigins...)
231+
}
232+
if c, y := d.Config().CORSAdmin(ctx); y {
233+
urls = append(urls, c.AllowedOrigins...)
234+
}
235+
host := urlx.ExtractPublicAddress(urls...)
223236

224237
return metricsx.New(
225238
cmd,
@@ -286,7 +299,7 @@ func sqa(ctx context.Context, d *driver.RegistrySQL, cmd *cobra.Command) *metric
286299
BatchSize: 1000,
287300
Interval: time.Hour * 6,
288301
},
289-
Hostname: issuerURL,
302+
Hostname: host,
290303
},
291304
)
292305
}

oryx/metricsx/middleware.go

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
package metricsx
55

66
import (
7-
"cmp"
87
"context"
98
"crypto/sha256"
109
"encoding/hex"
@@ -18,27 +17,32 @@ import (
1817
"sync"
1918
"time"
2019

21-
"github.com/ory/x/httpx"
22-
2320
"google.golang.org/grpc"
21+
"google.golang.org/grpc/metadata"
2422
"google.golang.org/grpc/status"
2523

26-
"github.com/ory/x/configx"
27-
28-
"github.com/spf13/cobra"
29-
3024
"github.com/gofrs/uuid"
25+
"github.com/spf13/cobra"
3126

3227
"github.com/ory/x/cmdx"
28+
"github.com/ory/x/configx"
29+
"github.com/ory/x/httpx"
3330
"github.com/ory/x/logrusx"
3431
"github.com/ory/x/resilience"
32+
"github.com/ory/x/urlx"
3533

3634
"github.com/ory/analytics-go/v5"
3735
)
3836

37+
const (
38+
XForwardedHostHeader = "X-Forwarded-Host"
39+
AuthorityHeader = ":authority"
40+
)
41+
3942
var (
40-
instance *Service
41-
lock sync.Mutex
43+
instance *Service
44+
lock sync.Mutex
45+
knownHeaders = []string{AuthorityHeader, XForwardedHostHeader}
4246
)
4347

4448
// Service helps with providing context on metrics.
@@ -282,16 +286,15 @@ func (sw *Service) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.
282286

283287
latency := time.Since(start).Milliseconds()
284288
path := sw.anonymizePath(r.URL.Path)
285-
host := sw.determineURLHost(r.Header.Get("X-Forwarded-Host"), r.Host)
289+
host := urlx.ExtractPublicAddress(sw.o.Hostname, r.Header.Get(XForwardedHostHeader), r.Host)
286290

287291
// Collecting request info
288292
stat, _ := httpx.GetResponseMeta(rw)
289293

290294
if err := sw.c.Enqueue(analytics.Page{
291-
InstanceId: sw.instanceId,
292-
DeploymentId: sw.o.DeploymentId,
293-
Project: sw.o.Service,
294-
295+
InstanceId: sw.instanceId,
296+
DeploymentId: sw.o.DeploymentId,
297+
Project: sw.o.Service,
295298
UrlHost: host,
296299
UrlPath: path,
297300
RequestCode: stat,
@@ -316,11 +319,21 @@ func (sw *Service) UnaryInterceptor(ctx context.Context, req interface{}, info *
316319

317320
latency := time.Since(start).Milliseconds()
318321

319-
if err := sw.c.Enqueue(analytics.Page{
320-
InstanceId: sw.instanceId,
321-
DeploymentId: sw.o.DeploymentId,
322-
Project: sw.o.Service,
322+
hosts := []string{sw.o.Hostname}
323+
if md, ok := metadata.FromIncomingContext(ctx); ok {
324+
for _, h := range knownHeaders {
325+
if v := md.Get(h); len(v) > 0 {
326+
hosts = append(hosts, v[0])
327+
}
328+
}
329+
}
330+
host := urlx.ExtractPublicAddress(hosts...)
323331

332+
if err := sw.c.Enqueue(analytics.Page{
333+
InstanceId: sw.instanceId,
334+
DeploymentId: sw.o.DeploymentId,
335+
Project: sw.o.Service,
336+
UrlHost: host,
324337
UrlPath: info.FullMethod,
325338
RequestCode: int(status.Code(err)),
326339
RequestLatency: int(latency),
@@ -369,7 +382,3 @@ func (sw *Service) anonymizeQuery(query url.Values, salt string) string {
369382
}
370383
return query.Encode()
371384
}
372-
373-
func (sw *Service) determineURLHost(xForwardedHostHeader, hostHeader string) string {
374-
return cmp.Or(sw.o.Hostname, xForwardedHostHeader, hostHeader)
375-
}

oryx/urlx/extract.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Copyright © 2023 Ory Corp
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package urlx
5+
6+
import (
7+
"context"
8+
"net"
9+
"net/url"
10+
"strings"
11+
"sync"
12+
"time"
13+
)
14+
15+
// hostCache caches DNS lookup results for hostnames to avoid repeated lookups.
16+
// The cache is thread-safe and stores true/false whether a hostname resolves to public IPs.
17+
type hostCache struct {
18+
mu sync.RWMutex
19+
cache map[string]bool
20+
}
21+
22+
// get retrieves a cached value for a hostname. Returns value and whether it was found.
23+
func (hc *hostCache) get(hostname string) (bool, bool) {
24+
hc.mu.RLock()
25+
defer hc.mu.RUnlock()
26+
isPublic, found := hc.cache[hostname]
27+
return isPublic, found
28+
}
29+
30+
// set stores the lookup result for a hostname.
31+
func (hc *hostCache) set(hostname string, isPublic bool) {
32+
hc.mu.Lock()
33+
defer hc.mu.Unlock()
34+
hc.cache[hostname] = isPublic
35+
}
36+
37+
// localCache lives for the lifetime of the main process. The cache
38+
// size is not expected to grow more than a few hundred bytes.
39+
var localCache = &hostCache{
40+
cache: make(map[string]bool),
41+
}
42+
43+
// ExtractPublicAddress iterates over parameters and extracts the first public
44+
// address found. Parameter values are assumed to be in priority order. Returns
45+
// an empty string if only private addresses are available.
46+
func ExtractPublicAddress(values ...string) string {
47+
for _, value := range values {
48+
if value == "" || value == "*" {
49+
continue
50+
}
51+
host := value
52+
53+
// parse URL addresses
54+
if u, err := url.Parse(value); err == nil && len(u.Host) > 1 {
55+
host = removeWildcardsFromHostname(u.Host)
56+
}
57+
58+
// strip port on both URL and non-URL addresses
59+
hostname, _, err := net.SplitHostPort(host)
60+
if err != nil {
61+
hostname = host
62+
}
63+
64+
// for IP addresses
65+
if ip := net.ParseIP(hostname); ip != nil {
66+
if !isPrivateIP(ip) {
67+
return host
68+
}
69+
continue
70+
}
71+
72+
// for hostnames, first check cache
73+
if isPublic, found := localCache.get(hostname); found {
74+
if isPublic {
75+
return host
76+
}
77+
continue
78+
}
79+
80+
// otherwise, perform DNS lookup & cache result
81+
isPublic := isPublicHostname(hostname)
82+
localCache.set(hostname, isPublic)
83+
if isPublic {
84+
return host
85+
}
86+
}
87+
88+
return ""
89+
}
90+
91+
// isPrivateIP checks if an IP address is private (RFC 1918/4193).
92+
func isPrivateIP(ip net.IP) bool {
93+
return ip.IsPrivate() ||
94+
ip.IsLoopback() ||
95+
ip.IsLinkLocalUnicast() ||
96+
ip.IsLinkLocalMulticast() ||
97+
ip.IsUnspecified() // 0.0.0.0 or ::
98+
}
99+
100+
// isPublicHostname performs DNS lookup to determine if hostname resolves to public IPs.
101+
// Returns true if at least one resolved IP is public, false if all are private or lookup fails.
102+
func isPublicHostname(hostname string) bool {
103+
// avoid DNS lookup if localhost
104+
lower := strings.ToLower(hostname)
105+
if lower == "localhost" {
106+
return false
107+
}
108+
109+
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
110+
defer cancel()
111+
112+
ips, err := net.DefaultResolver.LookupIPAddr(ctx, hostname)
113+
if err != nil {
114+
return false
115+
}
116+
117+
for _, ip := range ips {
118+
if !isPrivateIP(ip.IP) {
119+
return true
120+
}
121+
}
122+
123+
return false
124+
}
125+
126+
// removeWildcardsFromHostname removes wildcard segments from a hostname string
127+
// by splitting on dots and filtering out asterisk-only segments.
128+
func removeWildcardsFromHostname(hostname string) string {
129+
sep := strings.Split(hostname, ".")
130+
clean := make([]string, 0, len(sep))
131+
for _, s := range sep {
132+
if s != "*" && s != "" {
133+
clean = append(clean, s)
134+
}
135+
}
136+
return strings.Join(clean, ".")
137+
}

0 commit comments

Comments
 (0)