clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

main
tinyrelay / internal / daemon / private_peer.go
private_peer.go 3.1K
1package daemon23import (4 "context"5 "encoding/base64"6 "encoding/json"7 "fmt"8 "io"9 "net"10 "net/http"11 "net/url"12 "strings"1314 "github.com/FelineStateMachine/tinyrelay/internal/policy"15 "github.com/FelineStateMachine/tinyrelay/protocol/auth"16)1718func (t *Tenant) privateHTTPAuth(ctx context.Context, method, rawURL, payloadHash string) (string, error) {19 _ = method20 _ = payloadHash21 if t == nil || t.records == nil || !t.PrivateServiceEnabled() {22 return "", fmt.Errorf("private peer: relay identity unavailable")23 }24 if policy.PrivatePeerBase(rawURL, t.Policy().PrivatePeers) == "" {25 return "", fmt.Errorf("private peer: target is no longer configured")26 }27 root, ok := auth.GRASP08RepositoryRoot(rawURL)28 if !ok {29 return "", fmt.Errorf("private peer: target is not a Git repository URL")30 }31 proof, err := t.records.SignNIP98(ctx, http.MethodGet, root, "", 0)32 if err != nil {33 return "", err34 }35 raw, err := json.Marshal(proof)36 if err != nil {37 return "", err38 }39 return "Nostr " + base64.StdEncoding.EncodeToString(raw), nil40}4142func privatePeerMatch(source string, peers []string) bool {43 return policy.PrivatePeerBase(source, peers) != ""44}4546func (t *Tenant) privatePeerReady(ctx context.Context, target string) bool {47 if !t.PrivateServiceEnabled() || policy.PrivatePeerBase(target, t.Policy().PrivatePeers) == "" {48 return false49 }50 u, err := url.Parse(target)51 if err != nil {52 return false53 }54 if u.Scheme == "ws" {55 u.Scheme = "http"56 } else if u.Scheme == "wss" {57 u.Scheme = "https"58 }59 req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)60 if err != nil {61 return false62 }63 req.Header.Set("Accept", "application/nostr+json")64 res, err := privatePeerHTTPClient(t.app.cfg.AllowPrivateRelays).Do(req)65 if err != nil {66 return false67 }68 defer res.Body.Close()69 if res.StatusCode < 200 || res.StatusCode >= 300 {70 return false71 }72 var info struct {73 Supported []string `json:"supported_grasps"`74 Private bool `json:"private_service"`75 }76 if err := json.NewDecoder(io.LimitReader(res.Body, 1<<20)).Decode(&info); err != nil {77 return false78 }79 for _, profile := range info.Supported {80 if strings.EqualFold(profile, "GRASP-08") {81 return true82 }83 }84 return false85}8687func privatePeerHTTPClient(allowPrivate bool) *http.Client {88 transport := http.DefaultTransport.(*http.Transport).Clone()89 transport.Proxy = nil90 transport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {91 host, port, err := net.SplitHostPort(address)92 if err != nil {93 return nil, err94 }95 ips, err := net.DefaultResolver.LookupIP(ctx, "ip", host)96 if err != nil {97 return nil, err98 }99 for _, ip := range ips {100 if !allowPrivate && (ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsUnspecified()) {101 continue102 }103 conn, dialErr := (&net.Dialer{}).DialContext(ctx, network, net.JoinHostPort(ip.String(), port))104 if dialErr == nil {105 return conn, nil106 }107 }108 return nil, fmt.Errorf("private peer: no usable address for %s", host)109 }110 return &http.Client{Transport: transport, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}111}