forked from
tangled.org/core
fork
Configure Feed
Select the types of activity you want to include in your feed.
this repo has no description
fork
Configure Feed
Select the types of activity you want to include in your feed.
1package middleware
2
3import (
4 "context"
5 "fmt"
6 "log"
7 "net/http"
8 "net/url"
9 "slices"
10 "strconv"
11 "strings"
12
13 "github.com/bluesky-social/indigo/atproto/identity"
14 "github.com/go-chi/chi/v5"
15 "tangled.org/core/appview/db"
16 "tangled.org/core/appview/oauth"
17 "tangled.org/core/appview/pages"
18 "tangled.org/core/appview/pagination"
19 "tangled.org/core/appview/reporesolver"
20 "tangled.org/core/idresolver"
21 "tangled.org/core/orm"
22 "tangled.org/core/rbac"
23)
24
25type Middleware struct {
26 oauth *oauth.OAuth
27 db *db.DB
28 enforcer *rbac.Enforcer
29 repoResolver *reporesolver.RepoResolver
30 idResolver *idresolver.Resolver
31 pages *pages.Pages
32}
33
34func New(oauth *oauth.OAuth, db *db.DB, enforcer *rbac.Enforcer, repoResolver *reporesolver.RepoResolver, idResolver *idresolver.Resolver, pages *pages.Pages) Middleware {
35 return Middleware{
36 oauth: oauth,
37 db: db,
38 enforcer: enforcer,
39 repoResolver: repoResolver,
40 idResolver: idResolver,
41 pages: pages,
42 }
43}
44
45type middlewareFunc func(http.Handler) http.Handler
46
47func AuthMiddleware(o *oauth.OAuth) middlewareFunc {
48 return func(next http.Handler) http.Handler {
49 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
50 returnURL := "/"
51 if u, err := url.Parse(r.Header.Get("Referer")); err == nil {
52 returnURL = u.RequestURI()
53 }
54
55 loginURL := fmt.Sprintf("/login?return_url=%s", url.QueryEscape(returnURL))
56
57 redirectFunc := func(w http.ResponseWriter, r *http.Request) {
58 http.Redirect(w, r, loginURL, http.StatusTemporaryRedirect)
59 }
60 if r.Header.Get("HX-Request") == "true" {
61 redirectFunc = func(w http.ResponseWriter, _ *http.Request) {
62 w.Header().Set("HX-Redirect", loginURL)
63 w.WriteHeader(http.StatusOK)
64 }
65 }
66
67 sess, err := o.ResumeSession(r)
68 if err != nil {
69 log.Println("failed to resume session, redirecting...", "err", err, "url", r.URL.String())
70 redirectFunc(w, r)
71 return
72 }
73
74 if sess == nil {
75 log.Printf("session is nil, redirecting...")
76 redirectFunc(w, r)
77 return
78 }
79
80 next.ServeHTTP(w, r)
81 })
82 }
83}
84
85func Paginate(next http.Handler) http.Handler {
86 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
87 page := pagination.FirstPage()
88
89 offsetVal := r.URL.Query().Get("offset")
90 if offsetVal != "" {
91 offset, err := strconv.Atoi(offsetVal)
92 if err != nil {
93 log.Println("invalid offset")
94 } else {
95 page.Offset = offset
96 }
97 }
98
99 limitVal := r.URL.Query().Get("limit")
100 if limitVal != "" {
101 limit, err := strconv.Atoi(limitVal)
102 if err != nil {
103 log.Println("invalid limit")
104 } else {
105 page.Limit = limit
106 }
107 }
108
109 ctx := pagination.IntoContext(r.Context(), page)
110 next.ServeHTTP(w, r.WithContext(ctx))
111 })
112}
113
114func (mw Middleware) knotRoleMiddleware(group string) middlewareFunc {
115 return func(next http.Handler) http.Handler {
116 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
117 // requires auth also
118 actor := mw.oauth.GetUser(r)
119 if actor == nil {
120 // we need a logged in user
121 log.Printf("not logged in, redirecting")
122 http.Error(w, "Forbiden", http.StatusUnauthorized)
123 return
124 }
125 domain := chi.URLParam(r, "domain")
126 if domain == "" {
127 http.Error(w, "malformed url", http.StatusBadRequest)
128 return
129 }
130
131 ok, err := mw.enforcer.E.HasGroupingPolicy(actor.Did, group, domain)
132 if err != nil || !ok {
133 // we need a logged in user
134 log.Printf("%s does not have perms of a %s in domain %s", actor.Did, group, domain)
135 http.Error(w, "Forbiden", http.StatusUnauthorized)
136 return
137 }
138
139 next.ServeHTTP(w, r)
140 })
141 }
142}
143
144func (mw Middleware) KnotOwner() middlewareFunc {
145 return mw.knotRoleMiddleware("server:owner")
146}
147
148func (mw Middleware) RepoPermissionMiddleware(requiredPerm string) middlewareFunc {
149 return func(next http.Handler) http.Handler {
150 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
151 // requires auth also
152 actor := mw.oauth.GetUser(r)
153 if actor == nil {
154 // we need a logged in user
155 log.Printf("not logged in, redirecting")
156 http.Error(w, "Forbiden", http.StatusUnauthorized)
157 return
158 }
159 f, err := mw.repoResolver.Resolve(r)
160 if err != nil {
161 http.Error(w, "malformed url", http.StatusBadRequest)
162 return
163 }
164
165 ok, err := mw.enforcer.E.Enforce(actor.Did, f.Knot, f.DidSlashRepo(), requiredPerm)
166 if err != nil || !ok {
167 // we need a logged in user
168 log.Printf("%s does not have perms of a %s in repo %s", actor.Did, requiredPerm, f.DidSlashRepo())
169 http.Error(w, "Forbiden", http.StatusUnauthorized)
170 return
171 }
172
173 next.ServeHTTP(w, r)
174 })
175 }
176}
177
178func (mw Middleware) ResolveIdent() middlewareFunc {
179 excluded := []string{"favicon.ico"}
180
181 return func(next http.Handler) http.Handler {
182 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
183 didOrHandle := chi.URLParam(req, "user")
184 didOrHandle = strings.TrimPrefix(didOrHandle, "@")
185
186 if slices.Contains(excluded, didOrHandle) {
187 next.ServeHTTP(w, req)
188 return
189 }
190
191 id, err := mw.idResolver.ResolveIdent(req.Context(), didOrHandle)
192 if err != nil {
193 // invalid did or handle
194 log.Printf("failed to resolve did/handle '%s': %s\n", didOrHandle, err)
195 mw.pages.Error404(w)
196 return
197 }
198
199 ctx := context.WithValue(req.Context(), "resolvedId", *id)
200
201 next.ServeHTTP(w, req.WithContext(ctx))
202 })
203 }
204}
205
206func (mw Middleware) ResolveRepo() middlewareFunc {
207 return func(next http.Handler) http.Handler {
208 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
209 repoName := chi.URLParam(req, "repo")
210 repoName = strings.TrimSuffix(repoName, ".git")
211
212 id, ok := req.Context().Value("resolvedId").(identity.Identity)
213 if !ok {
214 log.Println("malformed middleware")
215 w.WriteHeader(http.StatusInternalServerError)
216 return
217 }
218
219 repo, err := db.GetRepo(
220 mw.db,
221 orm.FilterEq("did", id.DID.String()),
222 orm.FilterEq("name", repoName),
223 )
224 if err != nil {
225 log.Println("failed to resolve repo", "err", err)
226 mw.pages.ErrorKnot404(w)
227 return
228 }
229
230 ctx := context.WithValue(req.Context(), "repo", repo)
231 next.ServeHTTP(w, req.WithContext(ctx))
232 })
233 }
234}
235
236// middleware that is tacked on top of /{user}/{repo}/pulls/{pull}
237func (mw Middleware) ResolvePull() middlewareFunc {
238 return func(next http.Handler) http.Handler {
239 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
240 f, err := mw.repoResolver.Resolve(r)
241 if err != nil {
242 log.Println("failed to fully resolve repo", err)
243 mw.pages.ErrorKnot404(w)
244 return
245 }
246
247 prId := chi.URLParam(r, "pull")
248 prIdInt, err := strconv.Atoi(prId)
249 if err != nil {
250 log.Println("failed to parse pr id", err)
251 mw.pages.Error404(w)
252 return
253 }
254
255 pr, err := db.GetPull(mw.db, f.RepoAt(), prIdInt)
256 if err != nil {
257 log.Println("failed to get pull and comments", err)
258 mw.pages.Error404(w)
259 return
260 }
261
262 ctx := context.WithValue(r.Context(), "pull", pr)
263
264 if pr.IsStacked() {
265 stack, err := db.GetStack(mw.db, pr.StackId)
266 if err != nil {
267 log.Println("failed to get stack", err)
268 return
269 }
270 abandonedPulls, err := db.GetAbandonedPulls(mw.db, pr.StackId)
271 if err != nil {
272 log.Println("failed to get abandoned pulls", err)
273 return
274 }
275
276 ctx = context.WithValue(ctx, "stack", stack)
277 ctx = context.WithValue(ctx, "abandonedPulls", abandonedPulls)
278 }
279
280 next.ServeHTTP(w, r.WithContext(ctx))
281 })
282 }
283}
284
285// middleware that is tacked on top of /{user}/{repo}/issues/{issue}
286func (mw Middleware) ResolveIssue(next http.Handler) http.Handler {
287 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
288 f, err := mw.repoResolver.Resolve(r)
289 if err != nil {
290 log.Println("failed to fully resolve repo", err)
291 mw.pages.ErrorKnot404(w)
292 return
293 }
294
295 issueIdStr := chi.URLParam(r, "issue")
296 issueId, err := strconv.Atoi(issueIdStr)
297 if err != nil {
298 log.Println("failed to fully resolve issue ID", err)
299 mw.pages.Error404(w)
300 return
301 }
302
303 issue, err := db.GetIssue(mw.db, f.RepoAt(), issueId)
304 if err != nil {
305 log.Println("failed to get issues", "err", err)
306 mw.pages.Error404(w)
307 return
308 }
309
310 ctx := context.WithValue(r.Context(), "issue", issue)
311 next.ServeHTTP(w, r.WithContext(ctx))
312 })
313}
314
315// this should serve the go-import meta tag even if the path is technically
316// a 404 like tangled.sh/oppi.li/go-git/v5
317//
318// we're keeping the tangled.sh go-import tag too to maintain backward
319// compatiblity for modules that still point there. they will be redirected
320// to fetch source from tangled.org
321func (mw Middleware) GoImport() middlewareFunc {
322 return func(next http.Handler) http.Handler {
323 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
324 f, err := mw.repoResolver.Resolve(r)
325 if err != nil {
326 log.Println("failed to fully resolve repo", err)
327 mw.pages.ErrorKnot404(w)
328 return
329 }
330
331 fullName := reporesolver.GetBaseRepoPath(r, f)
332
333 if r.Header.Get("User-Agent") == "Go-http-client/1.1" {
334 if r.URL.Query().Get("go-get") == "1" {
335 html := fmt.Sprintf(
336 `<meta name="go-import" content="tangled.sh/%s git https://tangled.sh/%s"/>
337<meta name="go-import" content="tangled.org/%s git https://tangled.org/%s"/>`,
338 fullName, fullName,
339 fullName, fullName,
340 )
341 w.Header().Set("Content-Type", "text/html")
342 w.Write([]byte(html))
343 return
344 }
345 }
346
347 next.ServeHTTP(w, r)
348 })
349 }
350}