This repository has no description
0

Configure Feed

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

core / knotserver / xrpc / xrpc.go
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}