This repository has no description
5.8 kB
198 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.RepoSetDefaultBranchNSID, x.SetDefaultBranch)
53 r.Post("/"+tangled.RepoDeleteBranchNSID, x.DeleteBranch)
54 r.Post("/"+tangled.RepoCreateNSID, x.CreateRepo)
55 r.Post("/"+tangled.RepoDeleteNSID, x.DeleteRepo)
56 r.Post("/"+tangled.RepoForkStatusNSID, x.ForkStatus)
57 r.Post("/"+tangled.RepoForkSyncNSID, x.ForkSync)
58 r.Post("/"+tangled.RepoHiddenRefNSID, x.HiddenRef)
59 r.Post("/"+tangled.RepoMergeNSID, x.Merge)
60 r.Post("/"+tangled.KnotAddMemberNSID, x.AddMember)
61 r.Post("/"+tangled.KnotRemoveMemberNSID, x.RemoveMember)
62 r.Post("/"+tangled.RepoAddCollaboratorNSID, x.AddCollaborator)
63 r.Post("/"+tangled.RepoRemoveCollaboratorNSID, x.RemoveCollaborator)
64 })
65
66 // merge check is an open endpoint
67 //
68 // TODO: should we constrain this more?
69 // - we can calculate on PR submit/resubmit/gitRefUpdate etc.
70 // - use ETags on clients to keep requests to a minimum
71 r.Post("/"+tangled.RepoMergeCheckNSID, x.MergeCheck)
72
73 // repo query endpoints (no auth required)
74 r.Get("/"+tangled.RepoTreeNSID, x.RepoTree)
75 r.Get("/"+tangled.RepoLogNSID, x.RepoLog)
76 r.Get("/"+tangled.RepoBranchesNSID, x.RepoBranches)
77 r.Get("/"+tangled.RepoTagsNSID, x.RepoTags)
78 r.Get("/"+tangled.RepoTagNSID, x.RepoTag)
79 r.Get("/"+tangled.RepoBlobNSID, x.RepoBlob)
80 r.Get("/"+tangled.RepoDiffNSID, x.RepoDiff)
81 r.Get("/"+tangled.RepoCompareNSID, x.RepoCompare)
82 r.Get("/"+tangled.RepoGetDefaultBranchNSID, x.RepoGetDefaultBranch)
83 r.Get("/"+tangled.RepoDescribeRepoNSID, x.RepoDescribeRepo)
84 r.Get("/"+tangled.RepoBranchNSID, x.RepoBranch)
85 r.Get("/"+tangled.RepoArchiveNSID, x.RepoArchive)
86 r.Get("/"+tangled.RepoLanguagesNSID, x.RepoLanguages)
87 r.Get("/"+tangled.RepoListCollaboratorsNSID, x.ListCollaborators)
88 r.Get("/"+tangled.RepoCheckPushAllowedNSID, x.CheckPushAllowed)
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
101func (x *Xrpc) parseRepoParam(repo string) (string, error) {
102 if repo == "" || !strings.HasPrefix(repo, "did:") {
103 return "", xrpcerr.NewXrpcError(
104 xrpcerr.WithTag("InvalidRequest"),
105 xrpcerr.WithMessage("missing or invalid repo parameter, expected a repo DID"),
106 )
107 }
108
109 if !strings.Contains(repo, "/") {
110 repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repo)
111 if err != nil {
112 return "", xrpcerr.RepoNotFoundError
113 }
114 return repoPath, nil
115 }
116
117 parts := strings.SplitN(repo, "/", 2)
118 ownerDid, repoName := parts[0], parts[1]
119
120 repoDid, err := x.Db.GetRepoDid(ownerDid, repoName)
121 if err == nil {
122 repoPath, _, _, resolveErr := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid)
123 if resolveErr == nil {
124 return repoPath, nil
125 }
126 }
127
128 repoPath, joinErr := securejoin.SecureJoin(x.Config.Repo.ScanPath, filepath.Join(ownerDid, repoName))
129 if joinErr != nil {
130 return "", xrpcerr.RepoNotFoundError
131 }
132 if _, statErr := os.Stat(repoPath); statErr != nil {
133 return "", xrpcerr.RepoNotFoundError
134 }
135 return repoPath, nil
136}
137
138func (x *Xrpc) resolveRepoDID(repo *string, ownerDid, name string) (repoident.RepoDid, string, error) {
139 raw, err := x.selectRepoDID(repo, ownerDid, name)
140 if err != nil {
141 return "", "", err
142 }
143
144 repoDid, err := repoident.NewRepoDid(raw)
145 if err != nil {
146 return "", "", err
147 }
148
149 repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid.String())
150 if err != nil {
151 return "", "", err
152 }
153 return repoDid, repoPath, nil
154}
155
156func (x *Xrpc) selectRepoDID(repo *string, ownerDid, name string) (string, error) {
157 if repo != nil && *repo != "" {
158 return *repo, nil
159 }
160 return x.Db.GetRepoDid(ownerDid, name)
161}
162
163func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) {
164 w.Header().Set("Content-Type", "application/json")
165 w.WriteHeader(status)
166 json.NewEncoder(w).Encode(e)
167}
168
169type limitWriter struct {
170 buf bytes.Buffer
171 limit int
172 written int
173}
174
175var errResponseTooLarge = errors.New("response too large")
176
177func (lw *limitWriter) Write(p []byte) (int, error) {
178 if lw.written+len(p) > lw.limit {
179 return 0, errResponseTooLarge
180 }
181 n, err := lw.buf.Write(p)
182 lw.written += n
183 return n, err
184}
185
186func (x *Xrpc) writeJson(w http.ResponseWriter, response any) {
187 lw := &limitWriter{limit: x.Config.Server.MaxResponseKB * 1024}
188 if err := json.NewEncoder(lw).Encode(response); err != nil {
189 if errors.Is(err, errResponseTooLarge) {
190 writeError(w, xrpcerr.RequestTooLargeError, http.StatusRequestEntityTooLarge)
191 } else {
192 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
193 }
194 return
195 }
196 w.Header().Set("Content-Type", "application/json")
197 w.Write(lw.buf.Bytes())
198}