this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

URL helpers

+175 -12
+53
cmd/relayered/relay/host.go
··· 3 3 import ( 4 4 "context" 5 5 "errors" 6 + "fmt" 7 + "net/url" 8 + "strings" 6 9 10 + "github.com/bluesky-social/indigo/atproto/syntax" 7 11 "github.com/bluesky-social/indigo/cmd/relayered/relay/models" 8 12 9 13 "gorm.io/gorm" ··· 28 32 29 33 return &host, nil 30 34 } 35 + 36 + // parses, normalizes, and validates a raw URL (HTTP or WebSocket) in to a hostname for subscriptions 37 + // 38 + // Hostnames much be DNS names, not IP addresses 39 + func ParseHostname(raw string) (hostname string, noSSL bool, err error) { 40 + u, err := url.Parse(raw) 41 + noSSL = false 42 + switch u.Scheme { 43 + case "https", "wss": 44 + // pass 45 + case "http", "ws": 46 + noSSL = true 47 + default: 48 + return "", false, fmt.Errorf("unsupported URL scheme: %s", u.Scheme) 49 + } 50 + // 'localhost' (exact string) is allowed *with* a required port number; SSL is optional 51 + if u.Hostname() == "localhost" { 52 + if u.Port() == "" { 53 + return "", false, fmt.Errorf("port number is required for localhost") 54 + } 55 + return u.Host, noSSL, nil 56 + } 57 + 58 + // port numbers not allowed otherwise 59 + if u.Port() != "" { 60 + return "", false, fmt.Errorf("port number not allowed for non-local names") 61 + } 62 + 63 + // check it is a real hostname (eg, not IP address or single-word alias) 64 + // TODO: more SSRF protection here? eg disallow '.local' 65 + h, err := syntax.ParseHandle(u.Host) 66 + if err != nil { 67 + return "", false, fmt.Errorf("not a public hostname") 68 + } 69 + // lower-case in reponse 70 + return h.Normalize().String(), noSSL, nil 71 + } 72 + 73 + func IsTrustedHostname(hostname string, domains []string) bool { 74 + for _, d := range domains { 75 + if hostname == d { 76 + return true 77 + } 78 + if strings.HasPrefix(d, "*") && strings.HasSuffix(hostname, d[1:]) { 79 + return true 80 + } 81 + } 82 + return false 83 + }
+1
cmd/relayered/relay/host_checker_test.go
··· 30 30 assert.Error(err) 31 31 } 32 32 33 + // NOTE: this test does live network resolutions 33 34 func TestLiveHostChecker(t *testing.T) { 34 35 assert := assert.New(t) 35 36 ctx := t.Context()
+62
cmd/relayered/relay/host_test.go
··· 1 + package relay 2 + 3 + import ( 4 + "testing" 5 + 6 + "github.com/stretchr/testify/assert" 7 + ) 8 + 9 + type HostnameFixture struct { 10 + Val string 11 + Error bool 12 + Hostname string 13 + NoSSL bool 14 + } 15 + 16 + func TestParseHostname(t *testing.T) { 17 + assert := assert.New(t) 18 + 19 + fixtures := []HostnameFixture{ 20 + HostnameFixture{Val: "asdf", Error: true}, 21 + HostnameFixture{Val: "https://pds.example.com", Hostname: "pds.example.com", NoSSL: false}, 22 + HostnameFixture{Val: "http://pds.example.com", Hostname: "pds.example.com", NoSSL: true}, 23 + HostnameFixture{Val: "ws://pds.example.com", Hostname: "pds.example.com", NoSSL: true}, 24 + HostnameFixture{Val: "pds.example.com", Error: true}, 25 + HostnameFixture{Val: "https://8.8.8.8", Error: true}, 26 + HostnameFixture{Val: "https://internal", Error: true}, 27 + HostnameFixture{Val: "https://service.local", Hostname: "service.local", NoSSL: false}, // TODO: SSRF 28 + } 29 + 30 + for _, f := range fixtures { 31 + hostname, noSSL, err := ParseHostname(f.Val) 32 + if f.Error { 33 + assert.Error(err) 34 + continue 35 + } 36 + assert.Equal(f.Hostname, hostname) 37 + assert.Equal(f.NoSSL, noSSL) 38 + } 39 + } 40 + 41 + type TrustedFixture struct { 42 + Val string 43 + Domains []string 44 + Trusted bool 45 + } 46 + 47 + func TestIsTrustedDomain(t *testing.T) { 48 + assert := assert.New(t) 49 + 50 + fixtures := []TrustedFixture{ 51 + TrustedFixture{Val: "evil.com", Domains: []string{"good.com"}, Trusted: false}, 52 + TrustedFixture{Val: "pds.host.example.com", Domains: []string{"*.example.com"}, Trusted: true}, 53 + TrustedFixture{Val: "pds.host.example.com", Domains: []string{"example.com"}, Trusted: false}, 54 + TrustedFixture{Val: "pds.host.example.com", Domains: []string{"*.good.com"}, Trusted: false}, 55 + TrustedFixture{Val: "pds.host.example.com", Domains: []string{"*.good.com", "pds.host.example.com"}, Trusted: true}, 56 + TrustedFixture{Val: "good.com", Domains: []string{"*.good.com"}, Trusted: false}, 57 + } 58 + 59 + for _, f := range fixtures { 60 + assert.Equal(f.Trusted, IsTrustedHostname(f.Val, f.Domains)) 61 + } 62 + }
+23
cmd/relayered/relay/models/methods.go
··· 1 + package models 2 + 3 + import ( 4 + "fmt" 5 + ) 6 + 7 + // returns base HTTP URL for the host: scheme, hostname, optional port, no path segment 8 + func (h *Host) BaseURL() string { 9 + scheme := "https" 10 + if h.NoSSL { 11 + scheme = "http" 12 + } 13 + return fmt.Sprintf("%s://%s", scheme, h.Hostname) 14 + } 15 + 16 + // returns websocket URL for the host: scheme, hostname, optional port, and path. 17 + func (h *Host) SubscribeReposURL() string { 18 + scheme := "wss" 19 + if h.NoSSL { 20 + scheme = "ws" 21 + } 22 + return fmt.Sprintf("%s://%s/xrpc/com.atproto.sync.subscribeRepos", scheme, h.Hostname) 23 + }
+31
cmd/relayered/relay/models/methods_test.go
··· 1 + package models 2 + 3 + import ( 4 + "testing" 5 + 6 + "github.com/stretchr/testify/assert" 7 + ) 8 + 9 + func TestHostURLs(t *testing.T) { 10 + assert := assert.New(t) 11 + 12 + h := Host{ 13 + Hostname: "pds.example.com", 14 + NoSSL: false, 15 + } 16 + 17 + assert.Equal("https://pds.example.com", h.BaseURL()) 18 + assert.Equal("wss://pds.example.com/xrpc/com.atproto.sync.subscribeRepos", h.SubscribeReposURL()) 19 + 20 + h.NoSSL = true 21 + assert.Equal("http://pds.example.com", h.BaseURL()) 22 + assert.Equal("ws://pds.example.com/xrpc/com.atproto.sync.subscribeRepos", h.SubscribeReposURL()) 23 + 24 + lh := Host{ 25 + Hostname: "localhost:4321", 26 + NoSSL: true, 27 + } 28 + 29 + assert.Equal("http://localhost:4321", lh.BaseURL()) 30 + assert.Equal("ws://localhost:4321/xrpc/com.atproto.sync.subscribeRepos", lh.SubscribeReposURL()) 31 + }
+5 -12
cmd/relayered/relay/slurper.go
··· 94 94 logger = slog.Default() 95 95 } 96 96 97 - // NOTE: unused second argument is not an 'error 97 + // NOTE: discarded second argument is not an `error` type 98 98 newHostPerDayLimiter, _ := slidingwindow.NewLimiter(time.Hour*24, config.NewHostPerDayLimit, windowFunc) 99 99 100 100 s := &Slurper{ ··· 267 267 HandshakeTimeout: time.Second * 5, 268 268 } 269 269 270 - protocol := "ws" 271 - if s.Config.SSL { 272 - protocol = "wss" 273 - } 274 - 275 270 // cursor by 200 events to smooth over unclean shutdowns 276 271 if host.LastSeq > 200 { 277 272 host.LastSeq -= 200 ··· 293 288 default: 294 289 } 295 290 296 - var url string 291 + u := host.SubscribeReposURL() 297 292 if newHost { 298 - url = fmt.Sprintf("%s://%s/xrpc/com.atproto.sync.subscribeRepos", protocol, host.Hostname) 299 - } else { 300 - url = fmt.Sprintf("%s://%s/xrpc/com.atproto.sync.subscribeRepos?cursor=%d", protocol, host.Hostname, cursor) 293 + u = fmt.Sprintf("%s?cursor=%d", u, cursor) 301 294 } 302 - con, res, err := d.DialContext(ctx, url, nil) 295 + con, res, err := d.DialContext(ctx, u, nil) 303 296 if err != nil { 304 297 s.log.Warn("dialing failed", "host", host.Hostname, "err", err, "backoff", backoff) 305 298 time.Sleep(sleepForBackoff(backoff)) ··· 317 310 continue 318 311 } 319 312 320 - s.log.Info("event subscription response", "code", res.StatusCode, "url", url) 313 + s.log.Info("event subscription response", "code", res.StatusCode, "url", u) 321 314 322 315 curCursor := cursor 323 316 if err := s.handleConnection(ctx, host, con, &cursor, sub); err != nil {