Monorepo for Tangled tangled.org
5

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