Monorepo for Tangled tangled.org
2

Configure Feed

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

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