Monorepo for Tangled
tangled.org
1package xrpc
2
3import (
4 "context"
5 "fmt"
6 "io"
7 "net/http"
8 "net/url"
9 "strings"
10
11 "github.com/bluesky-social/indigo/api/atproto"
12 "github.com/bluesky-social/indigo/atproto/syntax"
13 indigoxrpc "github.com/bluesky-social/indigo/xrpc"
14 "tangled.org/core/api/tangled"
15 "tangled.org/core/knotmirror/db"
16)
17
18var mirrorToKnotNSID = map[string]string{
19 tangled.GitTempListBranchesNSID: tangled.RepoBranchesNSID,
20 tangled.GitTempListTagsNSID: tangled.RepoTagsNSID,
21 tangled.GitTempListCommitsNSID: tangled.RepoLogNSID,
22 tangled.GitTempGetTreeNSID: tangled.RepoTreeNSID,
23 tangled.GitTempGetBranchNSID: tangled.RepoBranchNSID,
24 tangled.GitTempGetBlobNSID: tangled.RepoBlobNSID,
25 tangled.GitTempGetTagNSID: tangled.RepoTagNSID,
26 tangled.GitTempGetArchiveNSID: tangled.RepoArchiveNSID,
27 tangled.GitTempListLanguagesNSID: tangled.RepoLanguagesNSID,
28}
29
30var hopByHopHeaders = map[string]bool{
31 "Connection": true,
32 "Keep-Alive": true,
33 "Transfer-Encoding": true,
34 "Te": true,
35 "Trailer": true,
36 "Upgrade": true,
37 "Proxy-Authorization": true,
38 "Proxy-Authenticate": true,
39}
40
41type knotInfo struct {
42 baseURL string
43 didSlashRepo string
44}
45
46func (x *Xrpc) resolveKnot(ctx context.Context, repoAt syntax.ATURI) (*knotInfo, error) {
47 repo, err := db.GetRepoByAtUri(ctx, x.db, repoAt)
48 if err == nil && repo != nil {
49 knotURL := repo.KnotDomain
50 if !strings.Contains(repo.KnotDomain, "://") {
51 if host, _ := db.GetHost(ctx, x.db, repo.KnotDomain); host != nil {
52 knotURL = host.URL()
53 } else {
54 x.logger.Warn("repo is from unknown knot")
55 if x.cfg.KnotUseSSL {
56 knotURL = "https://" + knotURL
57 } else {
58 knotURL = "http://" + knotURL
59 }
60 }
61 }
62 return &knotInfo{baseURL: knotURL, didSlashRepo: repo.DidSlashRepo()}, nil
63 }
64
65 owner, err := x.resolver.ResolveIdent(ctx, repoAt.Authority().String())
66 if err != nil {
67 return nil, fmt.Errorf("resolving repo owner: %w", err)
68 }
69
70 xrpcc := indigoxrpc.Client{Host: owner.PDSEndpoint()}
71 out, err := atproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String())
72 if err != nil {
73 return nil, fmt.Errorf("fetching repo record from PDS: %w", err)
74 }
75
76 record := out.Value.Val.(*tangled.Repo)
77 knotURL := record.Knot
78 if !strings.Contains(record.Knot, "://") {
79 if host, _ := db.GetHost(ctx, x.db, record.Knot); host != nil {
80 knotURL = host.URL()
81 } else {
82 x.logger.Warn("repo is from unknown knot")
83 if x.cfg.KnotUseSSL {
84 knotURL = "https://" + knotURL
85 } else {
86 knotURL = "http://" + knotURL
87 }
88 }
89 }
90
91 return &knotInfo{
92 baseURL: knotURL,
93 didSlashRepo: fmt.Sprintf("%s/%s", owner.DID, record.Name),
94 }, nil
95}
96
97func (x *Xrpc) proxyToKnot(w http.ResponseWriter, r *http.Request, repoAt syntax.ATURI) bool {
98 mirrorNSID := strings.TrimPrefix(r.URL.Path, "/xrpc/")
99 knotNSID, ok := mirrorToKnotNSID[mirrorNSID]
100 if !ok {
101 return false
102 }
103
104 knot, err := x.resolveKnot(r.Context(), repoAt)
105 if err != nil {
106 x.logger.Warn("proxy: failed to resolve knot", "repo", repoAt, "err", err)
107 return false
108 }
109
110 params := make(url.Values)
111 for k, v := range r.URL.Query() {
112 params[k] = v
113 }
114 params.Set("repo", knot.didSlashRepo)
115
116 target := fmt.Sprintf("%s/xrpc/%s?%s", knot.baseURL, knotNSID, params.Encode())
117
118 req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, target, nil)
119 if err != nil {
120 x.logger.Warn("proxy: failed to build request", "target", target, "err", err)
121 return false
122 }
123
124 resp, err := x.httpClient.Do(req)
125 if err != nil {
126 x.logger.Warn("proxy: knot request failed", "target", target, "err", err)
127 return false
128 }
129 defer resp.Body.Close()
130
131 for k, vv := range resp.Header {
132 if hopByHopHeaders[k] {
133 continue
134 }
135 for _, v := range vv {
136 w.Header().Add(k, v)
137 }
138 }
139 w.WriteHeader(resp.StatusCode)
140 if _, err := io.Copy(w, resp.Body); err != nil {
141 x.logger.Warn("proxy: response copy interrupted", "target", target, "err", err)
142 }
143
144 x.logger.Info("proxy: served from knot", "repo", repoAt, "knot", knot.baseURL, "status", resp.StatusCode)
145 return true
146}