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