forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
1package knotserver 2 3import ( 4 "context" 5 "fmt" 6 "log/slog" 7 "net/http" 8 "runtime/debug" 9 10 "github.com/go-chi/chi/v5" 11 "tangled.sh/tangled.sh/core/idresolver" 12 "tangled.sh/tangled.sh/core/jetstream" 13 "tangled.sh/tangled.sh/core/knotserver/config" 14 "tangled.sh/tangled.sh/core/knotserver/db" 15 "tangled.sh/tangled.sh/core/knotserver/xrpc" 16 tlog "tangled.sh/tangled.sh/core/log" 17 "tangled.sh/tangled.sh/core/notifier" 18 "tangled.sh/tangled.sh/core/rbac" 19 "tangled.sh/tangled.sh/core/types" 20) 21 22type Handle struct { 23 c *config.Config 24 db *db.DB 25 jc *jetstream.JetstreamClient 26 e *rbac.Enforcer 27 l *slog.Logger 28 n *notifier.Notifier 29 resolver *idresolver.Resolver 30 31 // init is a channel that is closed when the knot has been initailized 32 // i.e. when the first user (knot owner) has been added. 33 init chan struct{} 34 knotInitialized bool 35} 36 37func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, jc *jetstream.JetstreamClient, l *slog.Logger, n *notifier.Notifier) (http.Handler, error) { 38 r := chi.NewRouter() 39 40 h := Handle{ 41 c: c, 42 db: db, 43 e: e, 44 l: l, 45 jc: jc, 46 n: n, 47 resolver: idresolver.DefaultResolver(), 48 init: make(chan struct{}), 49 } 50 51 err := e.AddKnot(rbac.ThisServer) 52 if err != nil { 53 return nil, fmt.Errorf("failed to setup enforcer: %w", err) 54 } 55 56 // Check if the knot knows about any Dids; 57 // if it does, it is already initialized and we can repopulate the 58 // Jetstream subscriptions. 59 dids, err := db.GetAllDids() 60 if err != nil { 61 return nil, fmt.Errorf("failed to get all Dids: %w", err) 62 } 63 64 if len(dids) > 0 { 65 h.knotInitialized = true 66 close(h.init) 67 for _, d := range dids { 68 h.jc.AddDid(d) 69 } 70 } 71 72 err = h.jc.StartJetstream(ctx, h.processMessages) 73 if err != nil { 74 return nil, fmt.Errorf("failed to start jetstream: %w", err) 75 } 76 77 r.Get("/", h.Index) 78 r.Get("/capabilities", h.Capabilities) 79 r.Get("/version", h.Version) 80 r.Route("/{did}", func(r chi.Router) { 81 // Repo routes 82 r.Route("/{name}", func(r chi.Router) { 83 r.Route("/collaborator", func(r chi.Router) { 84 r.Use(h.VerifySignature) 85 r.Post("/add", h.AddRepoCollaborator) 86 }) 87 88 r.Route("/languages", func(r chi.Router) { 89 r.With(h.VerifySignature) 90 r.Get("/", h.RepoLanguages) 91 r.Get("/{ref}", h.RepoLanguages) 92 }) 93 94 r.Get("/", h.RepoIndex) 95 r.Get("/info/refs", h.InfoRefs) 96 r.Post("/git-upload-pack", h.UploadPack) 97 r.Post("/git-receive-pack", h.ReceivePack) 98 r.Get("/compare/{rev1}/{rev2}", h.Compare) // git diff-tree compare of two objects 99 100 r.With(h.VerifySignature).Post("/hidden-ref/{forkRef}/{remoteRef}", h.NewHiddenRef) 101 102 r.Route("/merge", func(r chi.Router) { 103 r.With(h.VerifySignature) 104 r.Post("/", h.Merge) 105 r.Post("/check", h.MergeCheck) 106 }) 107 108 r.Route("/tree/{ref}", func(r chi.Router) { 109 r.Get("/", h.RepoIndex) 110 r.Get("/*", h.RepoTree) 111 }) 112 113 r.Route("/blob/{ref}", func(r chi.Router) { 114 r.Get("/*", h.Blob) 115 }) 116 117 r.Route("/raw/{ref}", func(r chi.Router) { 118 r.Get("/*", h.BlobRaw) 119 }) 120 121 r.Get("/log/{ref}", h.Log) 122 r.Get("/archive/{file}", h.Archive) 123 r.Get("/commit/{ref}", h.Diff) 124 r.Get("/tags", h.Tags) 125 r.Route("/branches", func(r chi.Router) { 126 r.Get("/", h.Branches) 127 r.Get("/{branch}", h.Branch) 128 r.Route("/default", func(r chi.Router) { 129 r.Get("/", h.DefaultBranch) 130 r.With(h.VerifySignature).Put("/", h.SetDefaultBranch) 131 }) 132 }) 133 }) 134 }) 135 136 // xrpc apis 137 r.Mount("/xrpc", h.XrpcRouter()) 138 139 // Create a new repository. 140 r.Route("/repo", func(r chi.Router) { 141 r.Use(h.VerifySignature) 142 r.Put("/new", h.NewRepo) 143 r.Delete("/", h.RemoveRepo) 144 r.Route("/fork", func(r chi.Router) { 145 r.Post("/", h.RepoFork) 146 r.Post("/sync/*", h.RepoForkSync) 147 r.Get("/sync/*", h.RepoForkAheadBehind) 148 }) 149 }) 150 151 r.Route("/member", func(r chi.Router) { 152 r.Use(h.VerifySignature) 153 r.Put("/add", h.AddMember) 154 }) 155 156 // Socket that streams git oplogs 157 r.Get("/events", h.Events) 158 159 // Initialize the knot with an owner and public key. 160 r.With(h.VerifySignature).Post("/init", h.Init) 161 162 // Health check. Used for two-way verification with appview. 163 r.With(h.VerifySignature).Get("/health", h.Health) 164 165 // All public keys on the knot. 166 r.Get("/keys", h.Keys) 167 168 return r, nil 169} 170 171func (h *Handle) XrpcRouter() http.Handler { 172 logger := tlog.New("knots") 173 174 serviceAuth := serviceauth.NewServiceAuth(h.l, h.resolver, h.c.Server.Did().String()) 175 176 xrpc := &xrpc.Xrpc{ 177 Config: h.c, 178 Db: h.db, 179 Ingester: h.jc, 180 Enforcer: h.e, 181 Logger: logger, 182 Notifier: h.n, 183 Resolver: h.resolver, 184 ServiceAuth: serviceAuth, 185 } 186 return xrpc.Router() 187} 188 189// version is set during build time. 190var version string 191 192func (h *Handle) Version(w http.ResponseWriter, r *http.Request) { 193 if version == "" { 194 info, ok := debug.ReadBuildInfo() 195 if !ok { 196 http.Error(w, "failed to read build info", http.StatusInternalServerError) 197 return 198 } 199 200 var modVer string 201 for _, mod := range info.Deps { 202 if mod.Path == "tangled.sh/tangled.sh/knotserver" { 203 version = mod.Version 204 break 205 } 206 } 207 208 if modVer == "" { 209 version = "unknown" 210 } 211 } 212 213 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 214 fmt.Fprintf(w, "knotserver/%s", version) 215}