Monorepo for Tangled tangled.org
2

Configure Feed

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

at icy/yovxsu 5.9 kB View raw
1package xrpc 2 3import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "maps" 9 "net/http" 10 "net/url" 11 "strings" 12 13 "github.com/bluesky-social/indigo/atproto/syntax" 14 indigoxrpc "github.com/bluesky-social/indigo/xrpc" 15 "tangled.org/core/api/tangled" 16 "tangled.org/core/knotmirror/db" 17 "tangled.org/core/knotmirror/models" 18) 19 20var mirrorToKnotNSID = map[string]string{ 21 tangled.GitTempListBranchesNSID: tangled.RepoBranchesNSID, 22 tangled.GitTempListTagsNSID: tangled.RepoTagsNSID, 23 tangled.GitTempListCommitsNSID: tangled.RepoLogNSID, 24 tangled.GitTempGetTreeNSID: tangled.RepoTreeNSID, 25 tangled.GitTempGetBranchNSID: tangled.RepoBranchNSID, 26 tangled.GitTempGetBlobNSID: tangled.RepoBlobNSID, 27 tangled.GitTempGetTagNSID: tangled.RepoTagNSID, 28 tangled.GitTempGetArchiveNSID: tangled.RepoArchiveNSID, 29 tangled.RepoBlobNSID: tangled.RepoBlobNSID, 30 tangled.GitTempListLanguagesNSID: tangled.RepoLanguagesNSID, 31} 32 33var hopByHopHeaders = map[string]bool{ 34 "Connection": true, 35 "Keep-Alive": true, 36 "Transfer-Encoding": true, 37 "Te": true, 38 "Trailer": true, 39 "Upgrade": true, 40 "Proxy-Authorization": true, 41 "Proxy-Authenticate": true, 42} 43 44type knotInfo struct { 45 baseURL string 46 repoIdentifier string 47} 48 49// validateKnotURL ensures a knot base URL is safe to proxy to. 50// It rejects URLs with path components, query strings, or fragments 51// that could be used for path injection. 52func validateKnotURL(raw string) (string, error) { 53 u, err := url.Parse(raw) 54 if err != nil { 55 return "", fmt.Errorf("invalid knot URL: %w", err) 56 } 57 if u.Scheme != "http" && u.Scheme != "https" { 58 return "", errors.New("knot URL must use http or https scheme") 59 } 60 if u.Path != "" && u.Path != "/" { 61 return "", fmt.Errorf("knot URL must not contain a path: %q", raw) 62 } 63 if u.RawQuery != "" || u.Fragment != "" { 64 return "", fmt.Errorf("knot URL must not contain query or fragment: %q", raw) 65 } 66 if u.User != nil { 67 return "", fmt.Errorf("knot URL must not contain userinfo: %q", raw) 68 } 69 // Strip trailing slash for consistent formatting 70 return strings.TrimRight(u.String(), "/"), nil 71} 72 73func (x *Xrpc) resolveKnot(ctx context.Context, repoDid syntax.DID) (*knotInfo, error) { 74 if repo, err := db.GetRepoByRepoDid(ctx, x.db, repoDid); err == nil && repo != nil { 75 knotURL := repo.KnotDomain 76 if !strings.Contains(repo.KnotDomain, "://") { 77 if host, _ := db.GetHost(ctx, x.db, repo.KnotDomain); host != nil { 78 knotURL = host.URL() 79 } else { 80 x.logger.Warn("repo is from unknown knot") 81 if x.cfg.KnotUseSSL { 82 knotURL = "https://" + knotURL 83 } else { 84 knotURL = "http://" + knotURL 85 } 86 } 87 } 88 knotURL, err = validateKnotURL(knotURL) 89 if err != nil { 90 return nil, err 91 } 92 return &knotInfo{baseURL: knotURL, repoIdentifier: repo.RepoIdentifier()}, nil 93 } 94 95 ident, err := x.resolver.ResolveIdent(ctx, repoDid.String()) 96 if err != nil { 97 return nil, fmt.Errorf("resolving repoDid %s: %w", repoDid, err) 98 } 99 knotURL, err := validateKnotURL(ident.GetServiceEndpoint("atproto_pds")) 100 if err != nil { 101 return nil, fmt.Errorf("repoDid %s: %w", repoDid, err) 102 } 103 104 xrpcc := &indigoxrpc.Client{Host: knotURL, Client: x.httpClient} 105 out, err := tangled.RepoDescribeRepo(ctx, xrpcc, repoDid.String()) 106 if err != nil { 107 x.logger.Warn("describeRepo failed; serving without metadata upsert", "knot", knotURL, "repo", repoDid, "err", err) 108 return &knotInfo{baseURL: knotURL, repoIdentifier: repoDid.String()}, nil 109 } 110 if out.RepoDid != repoDid.String() { 111 return nil, fmt.Errorf("knot %s returned mismatched repoDid: got %q, want %q", knotURL, out.RepoDid, repoDid) 112 } 113 ownerDid, err := syntax.ParseDID(out.OwnerDid) 114 if err != nil { 115 return nil, fmt.Errorf("describeRepo on %s returned invalid ownerDid %q: %w", knotURL, out.OwnerDid, err) 116 } 117 rkey, err := syntax.ParseRecordKey(out.Rkey) 118 if err != nil { 119 return nil, fmt.Errorf("describeRepo on %s returned invalid rkey %q: %w", knotURL, out.Rkey, err) 120 } 121 122 go func() { 123 pending := &models.Repo{ 124 Did: ownerDid, 125 Rkey: rkey, 126 Name: string(rkey), 127 KnotDomain: knotURL, 128 RepoDid: repoDid, 129 State: models.RepoStatePending, 130 } 131 if err := db.UpsertRepo(context.Background(), x.db, pending); err != nil { 132 x.logger.Error("failed to upsert repo after directory resolution", "err", err) 133 } 134 }() 135 136 return &knotInfo{baseURL: knotURL, repoIdentifier: repoDid.String()}, nil 137} 138 139func (x *Xrpc) proxyToKnot(w http.ResponseWriter, r *http.Request, repoDid syntax.DID) bool { 140 mirrorNSID := strings.TrimPrefix(r.URL.Path, "/xrpc/") 141 knotNSID, ok := mirrorToKnotNSID[mirrorNSID] 142 if !ok { 143 return false 144 } 145 146 knot, err := x.resolveKnot(r.Context(), repoDid) 147 if err != nil { 148 x.logger.Warn("proxy: failed to resolve knot", "repo", repoDid, "err", err) 149 return false 150 } 151 152 params := make(url.Values) 153 maps.Copy(params, r.URL.Query()) 154 params.Set("repo", knot.repoIdentifier) 155 if mirrorNSID == tangled.GitTempGetBlobNSID { 156 params.Set("raw", "true") 157 } 158 159 target := fmt.Sprintf("%s/xrpc/%s?%s", knot.baseURL, knotNSID, params.Encode()) 160 161 req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, target, nil) 162 if err != nil { 163 x.logger.Warn("proxy: failed to build request", "target", target, "err", err) 164 return false 165 } 166 167 resp, err := x.httpClient.Do(req) 168 if err != nil { 169 x.logger.Warn("proxy: knot request failed", "target", target, "err", err) 170 return false 171 } 172 defer resp.Body.Close() 173 174 for k, vv := range resp.Header { 175 if hopByHopHeaders[k] { 176 continue 177 } 178 for _, v := range vv { 179 w.Header().Add(k, v) 180 } 181 } 182 w.WriteHeader(resp.StatusCode) 183 if _, err := io.Copy(w, resp.Body); err != nil { 184 x.logger.Warn("proxy: response copy interrupted", "target", target, "err", err) 185 } 186 187 x.logger.Info("proxy: served from knot", "repo", repoDid, "knot", knot.baseURL, "status", resp.StatusCode) 188 return true 189}