From a76ad830b6c69c54ec9d416b6ab6a6ce504756d2 Mon Sep 17 00:00:00 2001 From: Lewis Date: Tue, 3 Mar 2026 14:53:03 +0200 Subject: [PATCH] knotserver: DID-based repo resolution in internal, ingester, git, and router Signed-off-by: Lewis --- knotserver/git.go | 56 +++++++++---- knotserver/ingester.go | 152 +++++++++++++++++++++------------- knotserver/internal.go | 184 ++++++++++++++++++++++++----------------- knotserver/router.go | 53 +++--------- 4 files changed, 257 insertions(+), 188 deletions(-) diff --git a/knotserver/git.go b/knotserver/git.go index 227ddef4..707fb9d9 100644 --- a/knotserver/git.go +++ b/knotserver/git.go @@ -12,12 +12,34 @@ import ( "tangled.org/core/knotserver/git/service" ) -func (h *Knot) InfoRefs(w http.ResponseWriter, r *http.Request) { +func (h *Knot) resolveRepoPath(r *http.Request) (string, string, error) { + did := chi.URLParam(r, "did") name := chi.URLParam(r, "name") - repoPath, ok := repoPathFromcontext(r.Context()) - if !ok { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("Failed to find repository path")) + + if name == "" && strings.HasPrefix(did, "did:") { + repoPath, _, repoName, err := h.db.ResolveRepoDIDOnDisk(h.c.Repo.ScanPath, did) + if err != nil { + return "", "", fmt.Errorf("unknown repo DID: %w", err) + } + return repoPath, repoName, nil + } + + repoDid, err := h.db.GetRepoDid(did, name) + if err != nil { + return "", "", fmt.Errorf("repo not found: %w", err) + } + repoPath, _, _, err := h.db.ResolveRepoDIDOnDisk(h.c.Repo.ScanPath, repoDid) + if err != nil { + return "", "", fmt.Errorf("repo not found: %w", err) + } + return repoPath, name, nil +} + +func (h *Knot) InfoRefs(w http.ResponseWriter, r *http.Request) { + repoPath, name, err := h.resolveRepoPath(r) + if err != nil { + gitError(w, "repository not found", http.StatusNotFound) + h.l.Error("git: failed to resolve repo path", "handler", "InfoRefs", "error", err) return } @@ -48,10 +70,10 @@ func (h *Knot) InfoRefs(w http.ResponseWriter, r *http.Request) { } func (h *Knot) UploadArchive(w http.ResponseWriter, r *http.Request) { - repo, ok := repoPathFromcontext(r.Context()) - if !ok { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("Failed to find repository path")) + repo, _, err := h.resolveRepoPath(r) + if err != nil { + gitError(w, "repository not found", http.StatusNotFound) + h.l.Error("git: failed to resolve repo path", "handler", "UploadArchive", "error", err) return } @@ -93,10 +115,10 @@ func (h *Knot) UploadArchive(w http.ResponseWriter, r *http.Request) { } func (h *Knot) UploadPack(w http.ResponseWriter, r *http.Request) { - repo, ok := repoPathFromcontext(r.Context()) - if !ok { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("Failed to find repository path")) + repo, _, err := h.resolveRepoPath(r) + if err != nil { + gitError(w, "repository not found", http.StatusNotFound) + h.l.Error("git: failed to resolve repo path", "handler", "UploadPack", "error", err) return } @@ -140,7 +162,13 @@ func (h *Knot) UploadPack(w http.ResponseWriter, r *http.Request) { } func (h *Knot) ReceivePack(w http.ResponseWriter, r *http.Request) { - name := chi.URLParam(r, "name") + _, name, err := h.resolveRepoPath(r) + if err != nil { + gitError(w, "repository not found", http.StatusNotFound) + h.l.Error("git: failed to resolve repo path", "handler", "ReceivePack", "error", err) + return + } + h.RejectPush(w, r, name) } diff --git a/knotserver/ingester.go b/knotserver/ingester.go index 5d2cc6e7..ec4cccab 100644 --- a/knotserver/ingester.go +++ b/knotserver/ingester.go @@ -14,7 +14,6 @@ import ( "github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/xrpc" "github.com/bluesky-social/jetstream/pkg/models" - securejoin "github.com/cyphar/filepath-securejoin" "tangled.org/core/api/tangled" "tangled.org/core/knotserver/db" "tangled.org/core/knotserver/git" @@ -102,51 +101,70 @@ func (h *Knot) processPull(ctx context.Context, event *models.Event) error { return fmt.Errorf("ignoring pull record: target repo is nil") } - l = l.With("target_repo", record.Target.Repo) + l = l.With("target_repo", record.Target.Repo, "target_repo_did", record.Target.RepoDid) l = l.With("target_branch", record.Target.Branch) if record.Source == nil { return fmt.Errorf("ignoring pull record: not a branch-based pull request") } - if record.Source.Repo != nil { + if record.Source.Repo != nil || record.Source.RepoDid != nil { return fmt.Errorf("ignoring pull record: fork based pull") } - repoAt, err := syntax.ParseATURI(record.Target.Repo) - if err != nil { - return fmt.Errorf("failed to parse ATURI: %w", err) - } + var repoPath, ownerDid, repoName, repoDid string + switch { + case record.Target.RepoDid != nil && *record.Target.RepoDid != "": + repoDid = *record.Target.RepoDid + var lookupErr error + repoPath, ownerDid, repoName, lookupErr = h.db.ResolveRepoDIDOnDisk(h.c.Repo.ScanPath, repoDid) + if lookupErr != nil { + return fmt.Errorf("unknown target repo DID %s: %w", repoDid, lookupErr) + } - // resolve this aturi to extract the repo record - ident, err := h.resolver.ResolveIdent(ctx, repoAt.Authority().String()) - if err != nil || ident.Handle.IsInvalidHandle() { - return fmt.Errorf("failed to resolve handle: %w", err) - } + case record.Target.Repo != nil: + // TODO: get rid of this PDS fetch once all repos have DIDs + repoAt, parseErr := syntax.ParseATURI(*record.Target.Repo) + if parseErr != nil { + return fmt.Errorf("failed to parse ATURI: %w", parseErr) + } - xrpcc := xrpc.Client{ - Host: ident.PDSEndpoint(), - } + ident, resolveErr := h.resolver.ResolveIdent(ctx, repoAt.Authority().String()) + if resolveErr != nil || ident.Handle.IsInvalidHandle() { + return fmt.Errorf("failed to resolve handle: %w", resolveErr) + } - resp, err := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) - if err != nil { - return fmt.Errorf("failed to resolver repo: %w", err) - } + xrpcc := xrpc.Client{ + Host: ident.PDSEndpoint(), + } - repo := resp.Value.Val.(*tangled.Repo) + resp, getErr := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) + if getErr != nil { + return fmt.Errorf("failed to resolve repo: %w", getErr) + } - if repo.Knot != h.c.Server.Hostname { - return fmt.Errorf("rejected pull record: not this knot, %s != %s", repo.Knot, h.c.Server.Hostname) - } + repo := resp.Value.Val.(*tangled.Repo) - didSlashRepo, err := securejoin.SecureJoin(ident.DID.String(), repo.Name) - if err != nil { - return fmt.Errorf("failed to construct relative repo path: %w", err) - } + if repo.Knot != h.c.Server.Hostname { + return fmt.Errorf("rejected pull record: not this knot, %s != %s", repo.Knot, h.c.Server.Hostname) + } - repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, didSlashRepo) - if err != nil { - return fmt.Errorf("failed to construct absolute repo path: %w", err) + ownerDid = ident.DID.String() + repoName = repo.Name + + repoDid, didErr := h.db.GetRepoDid(ownerDid, repoName) + if didErr != nil { + return fmt.Errorf("failed to resolve repo DID for %s/%s: %w", ownerDid, repoName, didErr) + } + + var lookupErr error + repoPath, _, _, lookupErr = h.db.ResolveRepoDIDOnDisk(h.c.Repo.ScanPath, repoDid) + if lookupErr != nil { + return fmt.Errorf("failed to resolve repo on disk: %w", lookupErr) + } + + default: + return fmt.Errorf("ignoring pull record: target has neither repo nor repoDid") } gr, err := git.Open(repoPath, record.Source.Sha) @@ -189,9 +207,10 @@ func (h *Knot) processPull(ctx context.Context, event *models.Event) error { Kind: string(workflow.TriggerKindPullRequest), PullRequest: &trigger, Repo: &tangled.Pipeline_TriggerRepo{ - Did: ident.DID.String(), - Knot: repo.Knot, - Repo: repo.Name, + Did: ownerDid, + Knot: h.c.Server.Hostname, + Repo: &repoName, + RepoDid: &repoDid, }, }, } @@ -226,42 +245,61 @@ func (h *Knot) processCollaborator(ctx context.Context, event *models.Event) err return fmt.Errorf("failed to unmarshal record: %w", err) } - repoAt, err := syntax.ParseATURI(record.Repo) - if err != nil { - return err - } - subjectId, err := h.resolver.ResolveIdent(ctx, record.Subject) if err != nil || subjectId.Handle.IsInvalidHandle() { return err } - // TODO: fix this for good, we need to fetch the record here unfortunately - // resolve this aturi to extract the repo record - owner, err := h.resolver.ResolveIdent(ctx, repoAt.Authority().String()) - if err != nil || owner.Handle.IsInvalidHandle() { - return fmt.Errorf("failed to resolve handle: %w", err) - } + var rbacResource string + switch { + case record.RepoDid != nil && *record.RepoDid != "": + ownerDid, _, lookupErr := h.db.GetRepoKeyOwner(*record.RepoDid) + if lookupErr != nil { + return fmt.Errorf("unknown repo DID %s: %w", *record.RepoDid, lookupErr) + } + if ownerDid != did { + return fmt.Errorf("collaborator record author %s does not own repo %s", did, *record.RepoDid) + } + rbacResource = *record.RepoDid - xrpcc := xrpc.Client{ - Host: owner.PDSEndpoint(), - } + case record.Repo != nil: + // TODO: get rid of this PDS fetch once all repos have DIDs + repoAt, parseErr := syntax.ParseATURI(*record.Repo) + if parseErr != nil { + return parseErr + } - resp, err := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) - if err != nil { - return err - } + owner, resolveErr := h.resolver.ResolveIdent(ctx, repoAt.Authority().String()) + if resolveErr != nil || owner.Handle.IsInvalidHandle() { + return fmt.Errorf("failed to resolve handle: %w", resolveErr) + } + + xrpcc := xrpc.Client{ + Host: owner.PDSEndpoint(), + } - repo := resp.Value.Val.(*tangled.Repo) - didSlashRepo, _ := securejoin.SecureJoin(owner.DID.String(), repo.Name) + resp, getErr := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) + if getErr != nil { + return getErr + } + + repo := resp.Value.Val.(*tangled.Repo) + repoDid, didErr := h.db.GetRepoDid(owner.DID.String(), repo.Name) + if didErr != nil { + return fmt.Errorf("failed to resolve repo DID for %s/%s: %w", owner.DID.String(), repo.Name, didErr) + } + rbacResource = repoDid + + default: + return fmt.Errorf("collaborator record has neither repo nor repoDid") + } - // check perms for this user - ok, err := h.e.IsCollaboratorInviteAllowed(did, rbac.ThisServer, didSlashRepo) + ok, err := h.e.IsCollaboratorInviteAllowed(did, rbac.ThisServer, rbacResource) if err != nil { return fmt.Errorf("failed to check permissions: %w", err) } if !ok { - return fmt.Errorf("insufficient permissions: %s, %s, %s", did, "IsCollaboratorInviteAllowed", didSlashRepo) + return fmt.Errorf("insufficient permissions: %s, %s, %s", did, "IsCollaboratorInviteAllowed", rbacResource) } if err := h.db.AddDid(subjectId.DID.String()); err != nil { @@ -269,7 +307,7 @@ func (h *Knot) processCollaborator(ctx context.Context, event *models.Event) err } h.jc.AddDid(subjectId.DID.String()) - if err := h.e.AddCollaborator(subjectId.DID.String(), rbac.ThisServer, didSlashRepo); err != nil { + if err := h.e.AddCollaborator(subjectId.DID.String(), rbac.ThisServer, rbacResource); err != nil { return err } diff --git a/knotserver/internal.go b/knotserver/internal.go index b9bcaffa..f1dbc3e3 100644 --- a/knotserver/internal.go +++ b/knotserver/internal.go @@ -3,14 +3,12 @@ package knotserver import ( "context" "encoding/json" - "errors" "fmt" "log/slog" "net/http" "path/filepath" "strings" - securejoin "github.com/cyphar/filepath-securejoin" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-git/go-git/v5/plumbing" @@ -72,7 +70,7 @@ func (h *InternalHandle) InternalKeys(w http.ResponseWriter, r *http.Request) { // the body will be qualified repository path on success/push-denied // or an error message when process failed func (h *InternalHandle) Guard(w http.ResponseWriter, r *http.Request) { - l := h.l.With("handler", "PostReceiveHook") + l := h.l.With("handler", "Guard") var ( incomingUser = r.URL.Query().Get("user") @@ -87,36 +85,77 @@ func (h *InternalHandle) Guard(w http.ResponseWriter, r *http.Request) { return } - // did:foo/repo-name or - // handle/repo-name or - // any of the above with a leading slash (/) components := strings.Split(strings.TrimPrefix(strings.Trim(repo, "'"), "/"), "/") l.Info("command components", "components", components) - if len(components) != 2 { + var rbacResource string + var diskRelative string + + switch { + case len(components) == 1 && strings.HasPrefix(components[0], "did:"): + repoDid := components[0] + repoPath, _, _, lookupErr := h.db.ResolveRepoDIDOnDisk(h.c.Repo.ScanPath, repoDid) + if lookupErr != nil { + w.WriteHeader(http.StatusNotFound) + l.Error("repo DID not found", "repoDid", repoDid, "err", lookupErr) + fmt.Fprintln(w, "repo not found") + return + } + rbacResource = repoDid + rel, relErr := filepath.Rel(h.c.Repo.ScanPath, repoPath) + if relErr != nil { + w.WriteHeader(http.StatusInternalServerError) + l.Error("failed to compute relative path", "repoPath", repoPath, "err", relErr) + fmt.Fprintln(w, "internal error") + return + } + diskRelative = rel + + case len(components) == 2: + repoOwner := components[0] + resolver := idresolver.DefaultResolver(h.c.Server.PlcUrl) + repoOwnerIdent, resolveErr := resolver.ResolveIdent(r.Context(), repoOwner) + if resolveErr != nil || repoOwnerIdent.Handle.IsInvalidHandle() { + l.Error("Error resolving handle", "handle", repoOwner, "err", resolveErr) + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "error resolving handle: invalid handle\n") + return + } + ownerDid := repoOwnerIdent.DID.String() + repoName := components[1] + repoDid, didErr := h.db.GetRepoDid(ownerDid, repoName) + if didErr != nil { + w.WriteHeader(http.StatusNotFound) + l.Error("repo DID not found", "owner", ownerDid, "name", repoName, "err", didErr) + fmt.Fprintln(w, "repo not found") + return + } + repoPath, _, _, lookupErr := h.db.ResolveRepoDIDOnDisk(h.c.Repo.ScanPath, repoDid) + if lookupErr != nil { + w.WriteHeader(http.StatusNotFound) + l.Error("repo not found on disk", "repoDid", repoDid, "err", lookupErr) + fmt.Fprintln(w, "repo not found") + return + } + rbacResource = repoDid + rel, relErr := filepath.Rel(h.c.Repo.ScanPath, repoPath) + if relErr != nil { + w.WriteHeader(http.StatusInternalServerError) + l.Error("failed to compute relative path", "repoPath", repoPath, "err", relErr) + fmt.Fprintln(w, "internal error") + return + } + diskRelative = rel + + default: w.WriteHeader(http.StatusBadRequest) l.Error("invalid repo format", "components", components) - fmt.Fprintln(w, "invalid repo format, needs / or //") + fmt.Fprintln(w, "invalid repo format, needs /, //, or ") return } - repoOwner := components[0] - repoName := components[1] - - resolver := idresolver.DefaultResolver(h.c.Server.PlcUrl) - - repoOwnerIdent, err := resolver.ResolveIdent(r.Context(), repoOwner) - if err != nil || repoOwnerIdent.Handle.IsInvalidHandle() { - l.Error("Error resolving handle", "handle", repoOwner, "err", err) - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, "error resolving handle: invalid handle\n") - return - } - repoOwnerDid := repoOwnerIdent.DID.String() - - qualifiedRepo, _ := securejoin.SecureJoin(repoOwnerDid, repoName) if gitCommand == "git-receive-pack" { - ok, err := h.e.IsPushAllowed(incomingUser, rbac.ThisServer, qualifiedRepo) + ok, err := h.e.IsPushAllowed(incomingUser, rbac.ThisServer, rbacResource) if err != nil || !ok { w.WriteHeader(http.StatusForbidden) fmt.Fprint(w, repo) @@ -125,7 +164,7 @@ func (h *InternalHandle) Guard(w http.ResponseWriter, r *http.Request) { } w.WriteHeader(http.StatusOK) - fmt.Fprint(w, qualifiedRepo) + fmt.Fprint(w, diskRelative) } type PushOptions struct { @@ -140,16 +179,23 @@ func (h *InternalHandle) PostReceiveHook(w http.ResponseWriter, r *http.Request) gitRelativeDir, err := filepath.Rel(h.c.Repo.ScanPath, gitAbsoluteDir) if err != nil { l.Error("failed to calculate relative git dir", "scanPath", h.c.Repo.ScanPath, "gitAbsoluteDir", gitAbsoluteDir) + w.WriteHeader(http.StatusInternalServerError) return } - parts := strings.SplitN(gitRelativeDir, "/", 2) - if len(parts) != 2 { - l.Error("invalid git dir", "gitRelativeDir", gitRelativeDir) + repoDid := gitRelativeDir + if !strings.HasPrefix(repoDid, "did:") { + l.Error("invalid git dir, expected repo DID", "gitRelativeDir", gitRelativeDir) + w.WriteHeader(http.StatusBadRequest) + return + } + + ownerDid, repoName, err := h.db.GetRepoKeyOwner(repoDid) + if err != nil { + l.Error("failed to resolve repo DID from git dir", "repoDid", repoDid, "err", err) + w.WriteHeader(http.StatusBadRequest) return } - repoDid := parts[0] - repoName := parts[1] gitUserDid := r.Header.Get("X-Git-User-Did") @@ -176,37 +222,29 @@ func (h *InternalHandle) PostReceiveHook(w http.ResponseWriter, r *http.Request) } for _, line := range lines { - err := h.insertRefUpdate(line, gitUserDid, repoDid, repoName) + err := h.insertRefUpdate(line, gitUserDid, ownerDid, repoName, repoDid) if err != nil { l.Error("failed to insert op", "err", err, "line", line, "did", gitUserDid, "repo", gitRelativeDir) - // non-fatal } - err = h.emitCompareLink(&resp.Messages, line, repoDid, repoName) + err = h.emitCompareLink(&resp.Messages, line, ownerDid, repoName, repoDid) if err != nil { l.Error("failed to reply with compare link", "err", err, "line", line, "did", gitUserDid, "repo", gitRelativeDir) - // non-fatal } - err = h.triggerPipeline(&resp.Messages, line, gitUserDid, repoDid, repoName, pushOptions) + err = h.triggerPipeline(&resp.Messages, line, gitUserDid, ownerDid, repoName, repoDid, pushOptions) if err != nil { l.Error("failed to trigger pipeline", "err", err, "line", line, "did", gitUserDid, "repo", gitRelativeDir) - // non-fatal } } writeJSON(w, resp) } -func (h *InternalHandle) insertRefUpdate(line git.PostReceiveLine, gitUserDid, repoDid, repoName string) error { - didSlashRepo, err := securejoin.SecureJoin(repoDid, repoName) - if err != nil { - return err - } - - repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, didSlashRepo) - if err != nil { - return err +func (h *InternalHandle) insertRefUpdate(line git.PostReceiveLine, gitUserDid, ownerDid, repoName, repoDid string) error { + repoPath, _, _, resolveErr := h.db.ResolveRepoDIDOnDisk(h.c.Repo.ScanPath, repoDid) + if resolveErr != nil { + return fmt.Errorf("failed to resolve repo on disk: %w", resolveErr) } gr, err := git.Open(repoPath, line.Ref) @@ -214,9 +252,10 @@ func (h *InternalHandle) insertRefUpdate(line git.PostReceiveLine, gitUserDid, r return fmt.Errorf("failed to open git repo at ref %s: %w", line.Ref, err) } - var errs error meta, err := gr.RefUpdateMeta(line) - errors.Join(errs, err) + if err != nil { + return fmt.Errorf("failed to get ref update metadata: %w", err) + } metaRecord := meta.AsRecord() @@ -225,10 +264,12 @@ func (h *InternalHandle) insertRefUpdate(line git.PostReceiveLine, gitUserDid, r NewSha: line.NewSha.String(), Ref: line.Ref, CommitterDid: gitUserDid, - RepoDid: repoDid, + OwnerDid: &ownerDid, RepoName: repoName, + RepoDid: &repoDid, Meta: &metaRecord, } + eventJson, err := json.Marshal(refUpdate) if err != nil { return err @@ -240,29 +281,25 @@ func (h *InternalHandle) insertRefUpdate(line git.PostReceiveLine, gitUserDid, r EventJson: string(eventJson), } - return errors.Join(errs, h.db.InsertEvent(event, h.n)) + return h.db.InsertEvent(event, h.n) } func (h *InternalHandle) triggerPipeline( clientMsgs *[]string, line git.PostReceiveLine, gitUserDid string, - repoDid string, + ownerDid string, repoName string, + repoDid string, pushOptions PushOptions, ) error { if pushOptions.skipCi { return nil } - didSlashRepo, err := securejoin.SecureJoin(repoDid, repoName) - if err != nil { - return err - } - - repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, didSlashRepo) - if err != nil { - return err + repoPath, _, _, resolveErr := h.db.ResolveRepoDIDOnDisk(h.c.Repo.ScanPath, repoDid) + if resolveErr != nil { + return fmt.Errorf("failed to resolve repo on disk: %w", resolveErr) } gr, err := git.Open(repoPath, line.Ref) @@ -299,15 +336,18 @@ func (h *InternalHandle) triggerPipeline( NewSha: line.NewSha.String(), } + triggerRepo := &tangled.Pipeline_TriggerRepo{ + Did: ownerDid, + Knot: h.c.Server.Hostname, + Repo: &repoName, + RepoDid: &repoDid, + } + compiler := workflow.Compiler{ Trigger: tangled.Pipeline_TriggerMetadata{ Kind: string(workflow.TriggerKindPush), Push: &trigger, - Repo: &tangled.Pipeline_TriggerRepo{ - Did: repoDid, - Knot: h.c.Server.Hostname, - Repo: repoName, - }, + Repo: triggerRepo, }, } @@ -348,8 +388,9 @@ func (h *InternalHandle) triggerPipeline( func (h *InternalHandle) emitCompareLink( clientMsgs *[]string, line git.PostReceiveLine, - repoDid string, + ownerDid string, repoName string, + repoDid string, ) error { // this is a second push to a branch, don't reply with the link again if !line.OldSha.IsZero() { @@ -365,20 +406,15 @@ func (h *InternalHandle) emitCompareLink( pushedRef := plumbing.ReferenceName(line.Ref) - userIdent, err := h.res.ResolveIdent(context.Background(), repoDid) - user := repoDid + userIdent, err := h.res.ResolveIdent(context.Background(), ownerDid) + user := ownerDid if err == nil { user = userIdent.Handle.String() } - didSlashRepo, err := securejoin.SecureJoin(repoDid, repoName) - if err != nil { - return err - } - - repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, didSlashRepo) - if err != nil { - return err + repoPath, _, _, resolveErr := h.db.ResolveRepoDIDOnDisk(h.c.Repo.ScanPath, repoDid) + if resolveErr != nil { + return fmt.Errorf("failed to resolve repo on disk: %w", resolveErr) } gr, err := git.PlainOpen(repoPath) diff --git a/knotserver/router.go b/knotserver/router.go index 40ceff3e..bd1eb9bd 100644 --- a/knotserver/router.go +++ b/knotserver/router.go @@ -5,10 +5,8 @@ import ( "fmt" "log/slog" "net/http" - "path/filepath" "strings" - securejoin "github.com/cyphar/filepath-securejoin" "github.com/go-chi/chi/v5" "tangled.org/core/idresolver" "tangled.org/core/jetstream" @@ -83,10 +81,13 @@ func (h *Knot) Router() http.Handler { r.Route("/{did}", func(r chi.Router) { r.Use(h.resolveDidRedirect) - r.Route("/{name}", func(r chi.Router) { - r.Use(h.resolveRepo) - // routes for git operations + r.Get("/info/refs", h.InfoRefs) + r.Post("/git-upload-archive", h.UploadArchive) + r.Post("/git-upload-pack", h.UploadPack) + r.Post("/git-receive-pack", h.ReceivePack) + + r.Route("/{name}", func(r chi.Router) { r.Get("/info/refs", h.InfoRefs) r.Post("/git-upload-archive", h.UploadArchive) r.Post("/git-upload-pack", h.UploadPack) @@ -140,45 +141,11 @@ func (h *Knot) resolveDidRedirect(next http.Handler) http.Handler { } suffix := strings.TrimPrefix(r.URL.Path, "/"+didOrHandle) - newPath := fmt.Sprintf("/%s/%s?%s", id.DID.String(), suffix, r.URL.RawQuery) - http.Redirect(w, r, newPath, http.StatusTemporaryRedirect) - }) -} - -type ctxRepoPathKey struct{} - -func repoPathFromcontext(ctx context.Context) (string, bool) { - v, ok := ctx.Value(ctxRepoPathKey{}).(string) - return v, ok -} - -// resolveRepo is a http middleware that constructs git repo path from given did & name pair. -// It will reject the requests to unknown repos (when dir doesn't exist) -func (h *Knot) resolveRepo(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - did := chi.URLParam(r, "did") - name := chi.URLParam(r, "name") - repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, filepath.Join(did, name)) - if err != nil { - w.WriteHeader(http.StatusNotFound) - w.Write([]byte("Repository not found")) - return - } - - exist, err := isDir(repoPath) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("Failed to check repository path")) - return - } - if !exist { - w.WriteHeader(http.StatusNotFound) - w.Write([]byte("Repository not found")) - return + newPath := "/" + id.DID.String() + suffix + if r.URL.RawQuery != "" { + newPath += "?" + r.URL.RawQuery } - - ctx := context.WithValue(r.Context(), ctxRepoPathKey{}, repoPath) - next.ServeHTTP(w, r.WithContext(ctx)) + http.Redirect(w, r, newPath, http.StatusTemporaryRedirect) }) } -- 2.51.2