A vibe coded tangled fork which supports pijul.

knotserver/git: reject requests to unknown repos

Signed-off-by: Seongmin Lee <git@boltless.me>

+65 -34
+24 -34
knotserver/git.go
··· 5 5 "fmt" 6 6 "io" 7 7 "net/http" 8 - "path/filepath" 8 + "os" 9 9 "strings" 10 10 11 - securejoin "github.com/cyphar/filepath-securejoin" 12 11 "github.com/go-chi/chi/v5" 13 12 "tangled.org/core/knotserver/git/service" 14 13 ) 15 14 16 15 func (h *Knot) InfoRefs(w http.ResponseWriter, r *http.Request) { 17 - did := chi.URLParam(r, "did") 18 16 name := chi.URLParam(r, "name") 19 - repoName, err := securejoin.SecureJoin(did, name) 20 - if err != nil { 21 - gitError(w, "repository not found", http.StatusNotFound) 22 - h.l.Error("git: failed to secure join repo path", "handler", "InfoRefs", "error", err) 23 - return 24 - } 25 - 26 - repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, repoName) 27 - if err != nil { 28 - gitError(w, "repository not found", http.StatusNotFound) 29 - h.l.Error("git: failed to secure join repo path", "handler", "InfoRefs", "error", err) 17 + repoPath, ok := repoPathFromcontext(r.Context()) 18 + if !ok { 19 + w.WriteHeader(http.StatusInternalServerError) 20 + w.Write([]byte("Failed to find repository path")) 30 21 return 31 22 } 32 23 ··· 57 48 } 58 49 59 50 func (h *Knot) UploadArchive(w http.ResponseWriter, r *http.Request) { 60 - did := chi.URLParam(r, "did") 61 - name := chi.URLParam(r, "name") 62 - repo, err := securejoin.SecureJoin(h.c.Repo.ScanPath, filepath.Join(did, name)) 63 - if err != nil { 64 - gitError(w, err.Error(), http.StatusInternalServerError) 65 - h.l.Error("git: failed to secure join repo path", "handler", "UploadPack", "error", err) 51 + repo, ok := repoPathFromcontext(r.Context()) 52 + if !ok { 53 + w.WriteHeader(http.StatusInternalServerError) 54 + w.Write([]byte("Failed to find repository path")) 66 55 return 67 56 } 68 57 ··· 104 93 } 105 94 106 95 func (h *Knot) UploadPack(w http.ResponseWriter, r *http.Request) { 107 - did := chi.URLParam(r, "did") 108 - name := chi.URLParam(r, "name") 109 - repo, err := securejoin.SecureJoin(h.c.Repo.ScanPath, filepath.Join(did, name)) 110 - if err != nil { 111 - gitError(w, err.Error(), http.StatusInternalServerError) 112 - h.l.Error("git: failed to secure join repo path", "handler", "UploadPack", "error", err) 96 + repo, ok := repoPathFromcontext(r.Context()) 97 + if !ok { 98 + w.WriteHeader(http.StatusInternalServerError) 99 + w.Write([]byte("Failed to find repository path")) 113 100 return 114 101 } 115 102 ··· 153 140 } 154 141 155 142 func (h *Knot) ReceivePack(w http.ResponseWriter, r *http.Request) { 156 - did := chi.URLParam(r, "did") 157 143 name := chi.URLParam(r, "name") 158 - _, err := securejoin.SecureJoin(h.c.Repo.ScanPath, filepath.Join(did, name)) 159 - if err != nil { 160 - gitError(w, err.Error(), http.StatusForbidden) 161 - h.l.Error("git: failed to secure join repo path", "handler", "ReceivePack", "error", err) 162 - return 163 - } 164 - 165 144 h.RejectPush(w, r, name) 166 145 } 167 146 ··· 190 169 fmt.Fprintf(w, " Try:\ngit remote set-url --push origin git@%s:%s/%s\n\n... and push again.", hostname, ownerHandle, unqualifiedRepoName) 191 170 } 192 171 fmt.Fprintf(w, "\n\n") 172 + } 173 + 174 + func isDir(path string) (bool, error) { 175 + info, err := os.Stat(path) 176 + if err == nil && info.IsDir() { 177 + return true, nil 178 + } 179 + if os.IsNotExist(err) { 180 + return false, nil 181 + } 182 + return false, err 193 183 } 194 184 195 185 func gitError(w http.ResponseWriter, msg string, status int) {
+41
knotserver/router.go
··· 5 5 "fmt" 6 6 "log/slog" 7 7 "net/http" 8 + "path/filepath" 8 9 "strings" 9 10 11 + securejoin "github.com/cyphar/filepath-securejoin" 10 12 "github.com/go-chi/chi/v5" 11 13 "tangled.org/core/idresolver" 12 14 "tangled.org/core/jetstream" ··· 81 83 82 84 r.Route("/{did}", func(r chi.Router) { 83 85 r.Use(h.resolveDidRedirect) 86 + r.Use(h.resolveRepo) 84 87 r.Route("/{name}", func(r chi.Router) { 85 88 // routes for git operations 86 89 r.Get("/info/refs", h.InfoRefs) ··· 138 141 suffix := strings.TrimPrefix(r.URL.Path, "/"+didOrHandle) 139 142 newPath := fmt.Sprintf("/%s/%s?%s", id.DID.String(), suffix, r.URL.RawQuery) 140 143 http.Redirect(w, r, newPath, http.StatusTemporaryRedirect) 144 + }) 145 + } 146 + 147 + type ctxRepoPathKey struct{} 148 + 149 + func repoPathFromcontext(ctx context.Context) (string, bool) { 150 + v, ok := ctx.Value(ctxRepoPathKey{}).(string) 151 + return v, ok 152 + } 153 + 154 + // resolveRepo is a http middleware that constructs git repo path from given did & name pair. 155 + // It will reject the requests to unknown repos (when dir doesn't exist) 156 + func (h *Knot) resolveRepo(next http.Handler) http.Handler { 157 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 158 + did := chi.URLParam(r, "did") 159 + name := chi.URLParam(r, "name") 160 + // TODO: resolve repository, get repoPath path, ensure repository 161 + repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, filepath.Join(did, name)) 162 + if err != nil { 163 + w.WriteHeader(http.StatusNotFound) 164 + w.Write([]byte("Repository not found")) 165 + return 166 + } 167 + 168 + exist, err := isDir(repoPath) 169 + if err != nil { 170 + w.WriteHeader(http.StatusInternalServerError) 171 + w.Write([]byte("Failed to check repository path")) 172 + return 173 + } 174 + if !exist { 175 + w.WriteHeader(http.StatusNotFound) 176 + w.Write([]byte("Repository not found")) 177 + return 178 + } 179 + 180 + ctx := context.WithValue(r.Context(), "repoPath", repoPath) 181 + next.ServeHTTP(w, r.WithContext(ctx)) 141 182 }) 142 183 } 143 184