package state import ( "fmt" "io" "maps" "net/http" "strings" "github.com/bluesky-social/indigo/atproto/identity" indigoxrpc "github.com/bluesky-social/indigo/xrpc" "github.com/go-chi/chi/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/hashicorp/go-version" "tangled.org/core/api/tangled" "tangled.org/core/appview/models" xrpcclient "tangled.org/core/appview/xrpcclient" ) func (s *State) InfoRefs(w http.ResponseWriter, r *http.Request) { user := r.Context().Value("resolvedId").(identity.Identity) repo := r.Context().Value("repo").(*models.Repo) scheme := "https" if s.config.Core.Dev { scheme = "http" } targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) s.proxyRequest(w, r, targetURL) } func (s *State) UploadArchive(w http.ResponseWriter, r *http.Request) { user, ok := r.Context().Value("resolvedId").(identity.Identity) if !ok { http.Error(w, "failed to resolve user", http.StatusInternalServerError) return } repo := r.Context().Value("repo").(*models.Repo) scheme := "https" if s.config.Core.Dev { scheme = "http" } targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-archive?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) s.proxyRequest(w, r, targetURL) } func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) { user, ok := r.Context().Value("resolvedId").(identity.Identity) if !ok { http.Error(w, "failed to resolve user", http.StatusInternalServerError) return } repo := r.Context().Value("repo").(*models.Repo) scheme := "https" if s.config.Core.Dev { scheme = "http" } targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) s.proxyRequest(w, r, targetURL) } func (s *State) ReceivePack(w http.ResponseWriter, r *http.Request) { user, ok := r.Context().Value("resolvedId").(identity.Identity) if !ok { http.Error(w, "failed to resolve user", http.StatusInternalServerError) return } repo := r.Context().Value("repo").(*models.Repo) scheme := "https" if s.config.Core.Dev { scheme = "http" } targetURL := fmt.Sprintf("%s://%s/%s/%s/git-receive-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) s.proxyRequest(w, r, targetURL) } var knotVersionDownloadArchiveConstraint = version.MustConstraints(version.NewConstraint(">= 1.12.0-alpha")) func (s *State) DownloadArchive(w http.ResponseWriter, r *http.Request) { l := s.logger.With("handler", "DownloadArchive") ref := chi.URLParam(r, "ref") user, ok := r.Context().Value("resolvedId").(identity.Identity) if !ok { l.Error("failed to resolve user") http.Error(w, "failed to resolve user", http.StatusInternalServerError) return } repo := r.Context().Value("repo").(*models.Repo) scheme := "https" if s.config.Core.Dev { scheme = "http" } host := fmt.Sprintf("%s://%s", scheme, repo.Knot) xrpcc := &indigoxrpc.Client{ Host: host, } l = l.With("knot", repo.Knot) isCompatible := func() bool { out, err := tangled.KnotVersion(r.Context(), xrpcc) if err != nil { l.Warn("failed to get knot version", "err", err) return false } v, err := version.NewVersion(out.Version) if err != nil { l.Warn("failed to parse knot version", "version", out.Version, "err", err) return false } if !knotVersionDownloadArchiveConstraint.Check(v) { l.Warn("knot version incompatible.", "version", v) return false } return true }() l.Debug("knot compatibility check", "isCompatible", isCompatible) if isCompatible { targetURL := fmt.Sprintf("%s://%s/%s/%s/archive/%s", scheme, repo.Knot, user.DID, repo.Name, ref) s.proxyRequest(w, r, targetURL) } else { l.Debug("requesting xrpc/sh.tangled.repo.archive") archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, repo.DidSlashRepo()) if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { l.Error("failed to call XRPC repo.archive", "err", xrpcerr) s.pages.Error503(w) return } safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-") filename := fmt.Sprintf("%s-%s.tar.gz", repo.Name, safeRefFilename) w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) w.Header().Set("Content-Type", "application/gzip") w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes))) w.Write(archiveBytes) } } func (s *State) proxyRequest(w http.ResponseWriter, r *http.Request, targetURL string) { client := &http.Client{} // Create new request proxyReq, err := http.NewRequest(r.Method, targetURL, r.Body) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Copy original headers proxyReq.Header = r.Header repoOwnerHandle := chi.URLParam(r, "user") proxyReq.Header.Add("x-tangled-repo-owner-handle", repoOwnerHandle) // Execute request resp, err := client.Do(proxyReq) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } defer resp.Body.Close() // Copy response headers maps.Copy(w.Header(), resp.Header) // Set response status code w.WriteHeader(resp.StatusCode) // Copy response body if _, err := io.Copy(w, resp.Body); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }