rin
Публичный Open-source lightweight git hosting.
git_http.go · 170 строк · 4.9 KB
Raw
1 package handlers
2
3 import (
4 "bytes"
5 "compress/gzip"
6 "encoding/base64"
7 "io"
8 "strings"
9
10 "polar/internal/git"
11 "polar/internal/repo"
12 "polar/internal/user"
13
14 "github.com/gofiber/fiber/v2"
15 )
16
17 func (h *Handler) authenticateGitBasic(c *fiber.Ctx) *user.User {
18 authHeader := c.Get(fiber.HeaderAuthorization)
19 if !strings.HasPrefix(authHeader, "Basic ") {
20 return nil
21 }
22 decoded, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(authHeader, "Basic "))
23 if err != nil {
24 return nil
25 }
26 parts := strings.SplitN(string(decoded), ":", 2)
27 if len(parts) != 2 {
28 return nil
29 }
30 username := strings.TrimSpace(parts[0])
31 secret := strings.TrimSpace(parts[1])
32 if secret == "" {
33 return nil
34 }
35
36 // 1. Personal Access Token
37 if strings.HasPrefix(secret, "polar_") {
38 userID, err := h.authSvc.VerifyToken(c.UserContext(), secret)
39 if err != nil {
40 return nil
41 }
42 u, err := h.userSvc.GetByID(c.UserContext(), userID)
43 if err != nil {
44 return nil
45 }
46 if username != "" && !strings.EqualFold(u.Username, username) && !strings.EqualFold(u.Email, username) {
47 return nil
48 }
49 return u
50 }
51
52 // 2. Fallback to username + password if provided
53 if username != "" {
54 if u, err := h.userSvc.Authenticate(c.UserContext(), username, secret); err == nil {
55 return u
56 }
57 }
58 return nil
59 }
60
61 func requestBasicAuth(c *fiber.Ctx) error {
62 c.Set(fiber.HeaderWWWAuthenticate, `Basic realm="Polar Git"`)
63 return c.Status(fiber.StatusUnauthorized).SendString("Authentication required")
64 }
65
66 func (h *Handler) authorizeGitRequest(c *fiber.Ctx, service string) (*repo.Repository, string, error) {
67 owner := c.Params("owner")
68 repoName := strings.TrimSuffix(c.Params("repo"), ".git")
69
70 r, err := h.repoSvc.GetByOwnerAndName(c.UserContext(), owner, repoName)
71 if err != nil {
72 // If anonymous request to non-existent or private repo, prompt Basic Auth first so git clients send credentials
73 if c.Get(fiber.HeaderAuthorization) == "" {
74 return nil, "", requestBasicAuth(c)
75 }
76 return nil, "", c.Status(fiber.StatusNotFound).SendString("Repository not found")
77 }
78
79 authedUser := h.authenticateGitBasic(c)
80
81 switch service {
82 case "git-upload-pack":
83 if r.IsPrivate {
84 if authedUser == nil {
85 return nil, "", requestBasicAuth(c)
86 }
87 if !h.repoSvc.CanRead(c.UserContext(), r, authedUser) {
88 return nil, "", c.Status(fiber.StatusForbidden).SendString("Forbidden")
89 }
90 }
91 case "git-receive-pack":
92 if authedUser == nil {
93 return nil, "", requestBasicAuth(c)
94 }
95 if !h.repoSvc.CanWrite(c.UserContext(), r, authedUser) {
96 return nil, "", c.Status(fiber.StatusForbidden).SendString("Forbidden")
97 }
98 default:
99 return nil, "", c.Status(fiber.StatusBadRequest).SendString("Unsupported git service")
100 }
101
102 diskPath, err := h.repoSvc.DiskPath(r.OwnerUsername, r.Name)
103 if err != nil {
104 return nil, "", c.Status(fiber.StatusBadRequest).SendString("Invalid repository path")
105 }
106
107 return r, diskPath, nil
108 }
109
110 func (h *Handler) GitInfoRefs(c *fiber.Ctx) error {
111 service := c.Query("service")
112 if service != "git-upload-pack" && service != "git-receive-pack" {
113 return c.Status(fiber.StatusBadRequest).SendString("Smart HTTP service parameter required")
114 }
115
116 _, diskPath, err := h.authorizeGitRequest(c, service)
117 if err != nil || diskPath == "" {
118 return err
119 }
120
121 var buf bytes.Buffer
122 if err := git.AdvertiseRefs(c.UserContext(), diskPath, service, &buf); err != nil {
123 h.logger.Error("git advertise-refs failed", "service", service, "err", err)
124 return c.Status(fiber.StatusInternalServerError).SendString("Git advertise-refs error")
125 }
126
127 c.Set(fiber.HeaderContentType, "application/x-"+service+"-advertisement")
128 c.Set(fiber.HeaderCacheControl, "no-cache, max-age=0, must-revalidate")
129 c.Set("Pragma", "no-cache")
130 return c.Status(fiber.StatusOK).Send(buf.Bytes())
131 }
132
133 func (h *Handler) GitUploadPack(c *fiber.Ctx) error {
134 return h.handleGitServiceRPC(c, "git-upload-pack")
135 }
136
137 func (h *Handler) GitReceivePack(c *fiber.Ctx) error {
138 return h.handleGitServiceRPC(c, "git-receive-pack")
139 }
140
141 func (h *Handler) handleGitServiceRPC(c *fiber.Ctx, service string) error {
142 r, diskPath, err := h.authorizeGitRequest(c, service)
143 if err != nil || diskPath == "" {
144 return err
145 }
146
147 var bodyReader io.Reader = bytes.NewReader(c.Body())
148 if strings.EqualFold(c.Get(fiber.HeaderContentEncoding), "gzip") {
149 gz, err := gzip.NewReader(bodyReader)
150 if err != nil {
151 return c.Status(fiber.StatusBadRequest).SendString("Invalid gzip body")
152 }
153 defer gz.Close()
154 bodyReader = gz
155 }
156
157 var out bytes.Buffer
158 if err := git.ServiceRPC(c.UserContext(), diskPath, service, bodyReader, &out); err != nil {
159 h.logger.Error("git service rpc failed", "service", service, "err", err)
160 return c.Status(fiber.StatusInternalServerError).SendString("Git RPC error")
161 }
162
163 if service == "git-receive-pack" {
164 h.repoSvc.TouchUpdatedAt(c.UserContext(), r.ID)
165 }
166
167 c.Set(fiber.HeaderContentType, "application/x-"+service+"-result")
168 c.Set(fiber.HeaderCacheControl, "no-cache, max-age=0, must-revalidate")
169 return c.Status(fiber.StatusOK).Send(out.Bytes())
170 }
171