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 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}