1package state
2
3import (
4 "context"
5 "fmt"
6 "log"
7 "net/http"
8 "strconv"
9 "strings"
10 "time"
11
12 "slices"
13
14 "github.com/bluesky-social/indigo/atproto/identity"
15 "github.com/go-chi/chi/v5"
16 "tangled.sh/tangled.sh/core/appview/db"
17 "tangled.sh/tangled.sh/core/appview/middleware"
18)
19
20func knotRoleMiddleware(s *State, group string) middleware.Middleware {
21 return func(next http.Handler) http.Handler {
22 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
23 // requires auth also
24 actor := s.oauth.GetUser(r)
25 if actor == nil {
26 // we need a logged in user
27 log.Printf("not logged in, redirecting")
28 http.Error(w, "Forbiden", http.StatusUnauthorized)
29 return
30 }
31 domain := chi.URLParam(r, "domain")
32 if domain == "" {
33 http.Error(w, "malformed url", http.StatusBadRequest)
34 return
35 }
36
37 ok, err := s.enforcer.E.HasGroupingPolicy(actor.Did, group, domain)
38 if err != nil || !ok {
39 // we need a logged in user
40 log.Printf("%s does not have perms of a %s in domain %s", actor.Did, group, domain)
41 http.Error(w, "Forbiden", http.StatusUnauthorized)
42 return
43 }
44
45 next.ServeHTTP(w, r)
46 })
47 }
48}
49
50func KnotOwner(s *State) middleware.Middleware {
51 return knotRoleMiddleware(s, "server:owner")
52}
53
54func RepoPermissionMiddleware(s *State, requiredPerm string) middleware.Middleware {
55 return func(next http.Handler) http.Handler {
56 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
57 // requires auth also
58 actor := s.oauth.GetUser(r)
59 if actor == nil {
60 // we need a logged in user
61 log.Printf("not logged in, redirecting")
62 http.Error(w, "Forbiden", http.StatusUnauthorized)
63 return
64 }
65 f, err := s.fullyResolvedRepo(r)
66 if err != nil {
67 http.Error(w, "malformed url", http.StatusBadRequest)
68 return
69 }
70
71 ok, err := s.enforcer.E.Enforce(actor.Did, f.Knot, f.DidSlashRepo(), requiredPerm)
72 if err != nil || !ok {
73 // we need a logged in user
74 log.Printf("%s does not have perms of a %s in repo %s", actor.Did, requiredPerm, f.OwnerSlashRepo())
75 http.Error(w, "Forbiden", http.StatusUnauthorized)
76 return
77 }
78
79 next.ServeHTTP(w, r)
80 })
81 }
82}
83
84func StripLeadingAt(next http.Handler) http.Handler {
85 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
86 path := req.URL.EscapedPath()
87 if strings.HasPrefix(path, "/@") {
88 req.URL.RawPath = "/" + strings.TrimPrefix(path, "/@")
89 }
90 next.ServeHTTP(w, req)
91 })
92}
93
94func ResolveIdent(s *State) middleware.Middleware {
95 excluded := []string{"favicon.ico"}
96
97 return func(next http.Handler) http.Handler {
98 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
99 didOrHandle := chi.URLParam(req, "user")
100 if slices.Contains(excluded, didOrHandle) {
101 next.ServeHTTP(w, req)
102 return
103 }
104
105 id, err := s.resolver.ResolveIdent(req.Context(), didOrHandle)
106 if err != nil {
107 // invalid did or handle
108 log.Println("failed to resolve did/handle:", err)
109 w.WriteHeader(http.StatusNotFound)
110 return
111 }
112
113 ctx := context.WithValue(req.Context(), "resolvedId", *id)
114
115 next.ServeHTTP(w, req.WithContext(ctx))
116 })
117 }
118}
119
120func ResolveRepo(s *State) middleware.Middleware {
121 return func(next http.Handler) http.Handler {
122 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
123 repoName := chi.URLParam(req, "repo")
124 id, ok := req.Context().Value("resolvedId").(identity.Identity)
125 if !ok {
126 log.Println("malformed middleware")
127 w.WriteHeader(http.StatusInternalServerError)
128 return
129 }
130
131 repo, err := db.GetRepo(s.db, id.DID.String(), repoName)
132 if err != nil {
133 // invalid did or handle
134 log.Println("failed to resolve repo")
135 s.pages.Error404(w)
136 return
137 }
138
139 ctx := context.WithValue(req.Context(), "knot", repo.Knot)
140 ctx = context.WithValue(ctx, "repoAt", repo.AtUri)
141 ctx = context.WithValue(ctx, "repoDescription", repo.Description)
142 ctx = context.WithValue(ctx, "repoAddedAt", repo.Created.Format(time.RFC3339))
143 next.ServeHTTP(w, req.WithContext(ctx))
144 })
145 }
146}
147
148// middleware that is tacked on top of /{user}/{repo}/pulls/{pull}
149func ResolvePull(s *State) middleware.Middleware {
150 return func(next http.Handler) http.Handler {
151 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
152 f, err := s.fullyResolvedRepo(r)
153 if err != nil {
154 log.Println("failed to fully resolve repo", err)
155 http.Error(w, "invalid repo url", http.StatusNotFound)
156 return
157 }
158
159 prId := chi.URLParam(r, "pull")
160 prIdInt, err := strconv.Atoi(prId)
161 if err != nil {
162 http.Error(w, "bad pr id", http.StatusBadRequest)
163 log.Println("failed to parse pr id", err)
164 return
165 }
166
167 pr, err := db.GetPull(s.db, f.RepoAt, prIdInt)
168 if err != nil {
169 log.Println("failed to get pull and comments", err)
170 return
171 }
172
173 ctx := context.WithValue(r.Context(), "pull", pr)
174
175 if pr.IsStacked() {
176 stack, err := db.GetStack(s.db, pr.StackId)
177 if err != nil {
178 log.Println("failed to get stack", err)
179 return
180 }
181 abandonedPulls, err := db.GetAbandonedPulls(s.db, pr.StackId)
182 if err != nil {
183 log.Println("failed to get abandoned pulls", err)
184 return
185 }
186
187 ctx = context.WithValue(ctx, "stack", stack)
188 ctx = context.WithValue(ctx, "abandonedPulls", abandonedPulls)
189 }
190
191 next.ServeHTTP(w, r.WithContext(ctx))
192 })
193 }
194}
195
196// this should serve the go-import meta tag even if the path is technically
197// a 404 like tangled.sh/oppi.li/go-git/v5
198func GoImport(s *State) middleware.Middleware {
199 return func(next http.Handler) http.Handler {
200 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
201 f, err := s.fullyResolvedRepo(r)
202 if err != nil {
203 log.Println("failed to fully resolve repo", err)
204 http.Error(w, "invalid repo url", http.StatusNotFound)
205 return
206 }
207
208 fullName := f.OwnerHandle() + "/" + f.RepoName
209
210 if r.Header.Get("User-Agent") == "Go-http-client/1.1" {
211 if r.URL.Query().Get("go-get") == "1" {
212 html := fmt.Sprintf(
213 `<meta name="go-import" content="tangled.sh/%s git https://tangled.sh/%s"/>`,
214 fullName,
215 fullName,
216 )
217 w.Header().Set("Content-Type", "text/html")
218 w.Write([]byte(html))
219 return
220 }
221 }
222
223 next.ServeHTTP(w, r)
224 })
225 }
226}