This repository has no description
6.2 kB
208 lines
1package xrpc
2
3import (
4 "bytes"
5 "encoding/json"
6 "errors"
7 "log/slog"
8 "net/http"
9 "os"
10 "path/filepath"
11 "strings"
12
13 securejoin "github.com/cyphar/filepath-securejoin"
14 "github.com/go-chi/chi/v5"
15 "tangled.org/core/api/tangled"
16 "tangled.org/core/gitutil"
17 "tangled.org/core/idresolver"
18 "tangled.org/core/knotserver/config"
19 "tangled.org/core/knotserver/db"
20 "tangled.org/core/knotserver/sandbox"
21 "tangled.org/core/notifier"
22 "tangled.org/core/rbac"
23 "tangled.org/core/repoident"
24 xrpcerr "tangled.org/core/xrpc/errors"
25 "tangled.org/core/xrpc/serviceauth"
26)
27
28const ActorDid = serviceauth.ActorDid
29
30type DidIngester interface {
31 AddDid(did string)
32 RemoveDid(did string)
33}
34
35type Xrpc struct {
36 Config *config.Config
37 Db *db.DB
38 Ingester DidIngester
39 Enforcer *rbac.Enforcer
40 Logger *slog.Logger
41 Notifier *notifier.Notifier
42 Resolver *idresolver.Resolver
43 ServiceAuth *serviceauth.ServiceAuth
44 Sandbox sandbox.Backend
45}
46
47func (x *Xrpc) Router() http.Handler {
48 r := chi.NewRouter()
49
50 r.Group(func(r chi.Router) {
51 r.Use(x.ServiceAuth.VerifyServiceAuth)
52
53 r.Post("/"+tangled.RepoSetDefaultBranchNSID, x.SetDefaultBranch)
54 r.Post("/"+tangled.RepoDeleteBranchNSID, x.DeleteBranch)
55 r.Post("/"+tangled.RepoCreateNSID, x.CreateRepo)
56 r.Post("/"+tangled.RepoDeleteNSID, x.DeleteRepo)
57 r.Post("/"+tangled.RepoForkStatusNSID, x.ForkStatus)
58 r.Post("/"+tangled.RepoForkSyncNSID, x.ForkSync)
59 r.Post("/"+tangled.RepoHiddenRefNSID, x.HiddenRef)
60 r.Post("/"+tangled.RepoMergeNSID, x.Merge)
61 r.Post("/"+tangled.KnotAddMemberNSID, x.AddMember)
62 r.Post("/"+tangled.KnotRemoveMemberNSID, x.RemoveMember)
63 r.Post("/"+tangled.RepoAddCollaboratorNSID, x.AddCollaborator)
64 r.Post("/"+tangled.RepoRemoveCollaboratorNSID, x.RemoveCollaborator)
65 })
66
67 // merge check is an open endpoint
68 //
69 // TODO: should we constrain this more?
70 // - we can calculate on PR submit/resubmit/gitRefUpdate etc.
71 // - use ETags on clients to keep requests to a minimum
72 r.Post("/"+tangled.RepoMergeCheckNSID, x.MergeCheck)
73
74 // repo query endpoints (no auth required)
75 r.Get("/"+tangled.RepoTreeNSID, x.RepoTree)
76 r.Get("/"+tangled.RepoLogNSID, x.RepoLog)
77 r.Get("/"+tangled.RepoBranchesNSID, x.RepoBranches)
78 r.Get("/"+tangled.RepoTagsNSID, x.RepoTags)
79 r.Get("/"+tangled.RepoTagNSID, x.RepoTag)
80 r.Get("/"+tangled.RepoBlobNSID, x.RepoBlob)
81 r.Get("/"+tangled.RepoDiffNSID, x.RepoDiff)
82 r.Get("/"+tangled.RepoCompareNSID, x.RepoCompare)
83 r.Get("/"+tangled.RepoGetDefaultBranchNSID, x.RepoGetDefaultBranch)
84 r.Get("/"+tangled.RepoDescribeRepoNSID, x.RepoDescribeRepo)
85 r.Get("/"+tangled.RepoBranchNSID, x.RepoBranch)
86 r.Get("/"+tangled.RepoArchiveNSID, x.RepoArchive)
87 r.Get("/"+tangled.RepoLanguagesNSID, x.RepoLanguages)
88 r.Get("/"+tangled.RepoListCollaboratorsNSID, x.ListCollaborators)
89
90 // knot query endpoints (no auth required)
91 r.Get("/"+tangled.KnotListKeysNSID, x.ListKeys)
92 r.Get("/"+tangled.KnotListMembersNSID, x.ListMembers)
93 r.Get("/"+tangled.KnotVersionNSID, x.Version)
94
95 // service query endpoints (no auth required)
96 r.Get("/"+tangled.OwnerNSID, x.Owner)
97
98 return r
99}
100
101type resolvedRepo struct {
102 path string
103 name gitutil.RepoName
104}
105
106func (x *Xrpc) parseRepoParam(repo string) (string, error) {
107 resolved, err := x.resolveRepo(repo)
108 return resolved.path, err
109}
110
111func (x *Xrpc) resolveRepo(repo string) (resolvedRepo, error) {
112 if repo == "" || !strings.HasPrefix(repo, "did:") {
113 return resolvedRepo{}, xrpcerr.NewXrpcError(
114 xrpcerr.WithTag("InvalidRequest"),
115 xrpcerr.WithMessage("missing or invalid repo parameter, expected a repo DID"),
116 )
117 }
118
119 if !strings.Contains(repo, "/") {
120 repoPath, _, repoName, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repo)
121 if err != nil {
122 return resolvedRepo{}, xrpcerr.RepoNotFoundError
123 }
124 return resolvedRepo{path: repoPath, name: gitutil.RepoName(repoName)}, nil
125 }
126
127 parts := strings.SplitN(repo, "/", 2)
128 ownerDid, repoName := parts[0], parts[1]
129
130 repoDid, err := x.Db.GetRepoDid(ownerDid, repoName)
131 if err == nil {
132 repoPath, _, _, resolveErr := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid)
133 if resolveErr == nil {
134 return resolvedRepo{path: repoPath, name: gitutil.RepoName(repoName)}, nil
135 }
136 }
137
138 repoPath, joinErr := securejoin.SecureJoin(x.Config.Repo.ScanPath, filepath.Join(ownerDid, repoName))
139 if joinErr != nil {
140 return resolvedRepo{}, xrpcerr.RepoNotFoundError
141 }
142 if _, statErr := os.Stat(repoPath); statErr != nil {
143 return resolvedRepo{}, xrpcerr.RepoNotFoundError
144 }
145 return resolvedRepo{path: repoPath, name: gitutil.RepoName(repoName)}, nil
146}
147
148func (x *Xrpc) resolveRepoDID(repo *string, ownerDid, name string) (repoident.RepoDid, string, error) {
149 raw, err := x.selectRepoDID(repo, ownerDid, name)
150 if err != nil {
151 return "", "", err
152 }
153
154 repoDid, err := repoident.NewRepoDid(raw)
155 if err != nil {
156 return "", "", err
157 }
158
159 repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid.String())
160 if err != nil {
161 return "", "", err
162 }
163 return repoDid, repoPath, nil
164}
165
166func (x *Xrpc) selectRepoDID(repo *string, ownerDid, name string) (string, error) {
167 if repo != nil && *repo != "" {
168 return *repo, nil
169 }
170 return x.Db.GetRepoDid(ownerDid, name)
171}
172
173func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) {
174 w.Header().Set("Content-Type", "application/json")
175 w.WriteHeader(status)
176 json.NewEncoder(w).Encode(e)
177}
178
179type limitWriter struct {
180 buf bytes.Buffer
181 limit int
182 written int
183}
184
185var errResponseTooLarge = errors.New("response too large")
186
187func (lw *limitWriter) Write(p []byte) (int, error) {
188 if lw.written+len(p) > lw.limit {
189 return 0, errResponseTooLarge
190 }
191 n, err := lw.buf.Write(p)
192 lw.written += n
193 return n, err
194}
195
196func (x *Xrpc) writeJson(w http.ResponseWriter, response any) {
197 lw := &limitWriter{limit: x.Config.Server.MaxResponseKB * 1024}
198 if err := json.NewEncoder(lw).Encode(response); err != nil {
199 if errors.Is(err, errResponseTooLarge) {
200 writeError(w, xrpcerr.RequestTooLargeError, http.StatusRequestEntityTooLarge)
201 } else {
202 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
203 }
204 return
205 }
206 w.Header().Set("Content-Type", "application/json")
207 w.Write(lw.buf.Bytes())
208}