package handlers import ( "bytes" "compress/gzip" "encoding/base64" "io" "strings" "polar/internal/git" "polar/internal/repo" "polar/internal/user" "github.com/gofiber/fiber/v2" ) func (h *Handler) authenticateGitBasic(c *fiber.Ctx) *user.User { authHeader := c.Get(fiber.HeaderAuthorization) if !strings.HasPrefix(authHeader, "Basic ") { return nil } decoded, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(authHeader, "Basic ")) if err != nil { return nil } parts := strings.SplitN(string(decoded), ":", 2) if len(parts) != 2 { return nil } username := strings.TrimSpace(parts[0]) secret := strings.TrimSpace(parts[1]) if secret == "" { return nil } // 1. Personal Access Token if strings.HasPrefix(secret, "polar_") { userID, err := h.authSvc.VerifyToken(c.UserContext(), secret) if err != nil { return nil } u, err := h.userSvc.GetByID(c.UserContext(), userID) if err != nil { return nil } if username != "" && !strings.EqualFold(u.Username, username) && !strings.EqualFold(u.Email, username) { return nil } return u } // 2. Fallback to username + password if provided if username != "" { if u, err := h.userSvc.Authenticate(c.UserContext(), username, secret); err == nil { return u } } return nil } func requestBasicAuth(c *fiber.Ctx) error { c.Set(fiber.HeaderWWWAuthenticate, `Basic realm="Polar Git"`) return c.Status(fiber.StatusUnauthorized).SendString("Authentication required") } func (h *Handler) authorizeGitRequest(c *fiber.Ctx, service string) (*repo.Repository, string, error) { owner := c.Params("owner") repoName := strings.TrimSuffix(c.Params("repo"), ".git") r, err := h.repoSvc.GetByOwnerAndName(c.UserContext(), owner, repoName) if err != nil { // If anonymous request to non-existent or private repo, prompt Basic Auth first so git clients send credentials if c.Get(fiber.HeaderAuthorization) == "" { return nil, "", requestBasicAuth(c) } return nil, "", c.Status(fiber.StatusNotFound).SendString("Repository not found") } authedUser := h.authenticateGitBasic(c) switch service { case "git-upload-pack": if r.IsPrivate { if authedUser == nil { return nil, "", requestBasicAuth(c) } if !h.repoSvc.CanRead(c.UserContext(), r, authedUser) { return nil, "", c.Status(fiber.StatusForbidden).SendString("Forbidden") } } case "git-receive-pack": if authedUser == nil { return nil, "", requestBasicAuth(c) } if !h.repoSvc.CanWrite(c.UserContext(), r, authedUser) { return nil, "", c.Status(fiber.StatusForbidden).SendString("Forbidden") } default: return nil, "", c.Status(fiber.StatusBadRequest).SendString("Unsupported git service") } diskPath, err := h.repoSvc.DiskPath(r.OwnerUsername, r.Name) if err != nil { return nil, "", c.Status(fiber.StatusBadRequest).SendString("Invalid repository path") } return r, diskPath, nil } func (h *Handler) GitInfoRefs(c *fiber.Ctx) error { service := c.Query("service") if service != "git-upload-pack" && service != "git-receive-pack" { return c.Status(fiber.StatusBadRequest).SendString("Smart HTTP service parameter required") } _, diskPath, err := h.authorizeGitRequest(c, service) if err != nil || diskPath == "" { return err } var buf bytes.Buffer if err := git.AdvertiseRefs(c.UserContext(), diskPath, service, &buf); err != nil { h.logger.Error("git advertise-refs failed", "service", service, "err", err) return c.Status(fiber.StatusInternalServerError).SendString("Git advertise-refs error") } c.Set(fiber.HeaderContentType, "application/x-"+service+"-advertisement") c.Set(fiber.HeaderCacheControl, "no-cache, max-age=0, must-revalidate") c.Set("Pragma", "no-cache") return c.Status(fiber.StatusOK).Send(buf.Bytes()) } func (h *Handler) GitUploadPack(c *fiber.Ctx) error { return h.handleGitServiceRPC(c, "git-upload-pack") } func (h *Handler) GitReceivePack(c *fiber.Ctx) error { return h.handleGitServiceRPC(c, "git-receive-pack") } func (h *Handler) handleGitServiceRPC(c *fiber.Ctx, service string) error { r, diskPath, err := h.authorizeGitRequest(c, service) if err != nil || diskPath == "" { return err } var bodyReader io.Reader = bytes.NewReader(c.Body()) if strings.EqualFold(c.Get(fiber.HeaderContentEncoding), "gzip") { gz, err := gzip.NewReader(bodyReader) if err != nil { return c.Status(fiber.StatusBadRequest).SendString("Invalid gzip body") } defer gz.Close() bodyReader = gz } var out bytes.Buffer if err := git.ServiceRPC(c.UserContext(), diskPath, service, bodyReader, &out); err != nil { h.logger.Error("git service rpc failed", "service", service, "err", err) return c.Status(fiber.StatusInternalServerError).SendString("Git RPC error") } if service == "git-receive-pack" { h.repoSvc.TouchUpdatedAt(c.UserContext(), r.ID) } c.Set(fiber.HeaderContentType, "application/x-"+service+"-result") c.Set(fiber.HeaderCacheControl, "no-cache, max-age=0, must-revalidate") return c.Status(fiber.StatusOK).Send(out.Bytes()) }