forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
at master 9.7 kB view raw
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 w.WriteHeader(http.StatusNotFound) 227 mw.pages.ErrorKnot404(w) 228 return 229 } 230 231 ctx := context.WithValue(req.Context(), "repo", repo) 232 next.ServeHTTP(w, req.WithContext(ctx)) 233 }) 234 } 235} 236 237// middleware that is tacked on top of /{user}/{repo}/pulls/{pull} 238func (mw Middleware) ResolvePull() middlewareFunc { 239 return func(next http.Handler) http.Handler { 240 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 241 f, err := mw.repoResolver.Resolve(r) 242 if err != nil { 243 log.Println("failed to fully resolve repo", err) 244 w.WriteHeader(http.StatusNotFound) 245 mw.pages.ErrorKnot404(w) 246 return 247 } 248 249 prId := chi.URLParam(r, "pull") 250 prIdInt, err := strconv.Atoi(prId) 251 if err != nil { 252 log.Println("failed to parse pr id", err) 253 mw.pages.Error404(w) 254 return 255 } 256 257 pr, err := db.GetPull(mw.db, f.RepoAt(), prIdInt) 258 if err != nil { 259 log.Println("failed to get pull and comments", err) 260 mw.pages.Error404(w) 261 return 262 } 263 264 ctx := context.WithValue(r.Context(), "pull", pr) 265 266 if pr.IsStacked() { 267 stack, err := db.GetStack(mw.db, pr.StackId) 268 if err != nil { 269 log.Println("failed to get stack", err) 270 return 271 } 272 abandonedPulls, err := db.GetAbandonedPulls(mw.db, pr.StackId) 273 if err != nil { 274 log.Println("failed to get abandoned pulls", err) 275 return 276 } 277 278 ctx = context.WithValue(ctx, "stack", stack) 279 ctx = context.WithValue(ctx, "abandonedPulls", abandonedPulls) 280 } 281 282 next.ServeHTTP(w, r.WithContext(ctx)) 283 }) 284 } 285} 286 287// middleware that is tacked on top of /{user}/{repo}/issues/{issue} 288func (mw Middleware) ResolveIssue(next http.Handler) http.Handler { 289 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 290 f, err := mw.repoResolver.Resolve(r) 291 if err != nil { 292 log.Println("failed to fully resolve repo", err) 293 w.WriteHeader(http.StatusNotFound) 294 mw.pages.ErrorKnot404(w) 295 return 296 } 297 298 issueIdStr := chi.URLParam(r, "issue") 299 issueId, err := strconv.Atoi(issueIdStr) 300 if err != nil { 301 log.Println("failed to fully resolve issue ID", err) 302 mw.pages.Error404(w) 303 return 304 } 305 306 issue, err := db.GetIssue(mw.db, f.RepoAt(), issueId) 307 if err != nil { 308 log.Println("failed to get issues", "err", err) 309 mw.pages.Error404(w) 310 return 311 } 312 313 ctx := context.WithValue(r.Context(), "issue", issue) 314 next.ServeHTTP(w, r.WithContext(ctx)) 315 }) 316} 317 318// this should serve the go-import meta tag even if the path is technically 319// a 404 like tangled.sh/oppi.li/go-git/v5 320// 321// we're keeping the tangled.sh go-import tag too to maintain backward 322// compatiblity for modules that still point there. they will be redirected 323// to fetch source from tangled.org 324func (mw Middleware) GoImport() middlewareFunc { 325 return func(next http.Handler) http.Handler { 326 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 327 f, err := mw.repoResolver.Resolve(r) 328 if err != nil { 329 log.Println("failed to fully resolve repo", err) 330 w.WriteHeader(http.StatusNotFound) 331 mw.pages.ErrorKnot404(w) 332 return 333 } 334 335 fullName := reporesolver.GetBaseRepoPath(r, f) 336 337 if r.Header.Get("User-Agent") == "Go-http-client/1.1" { 338 if r.URL.Query().Get("go-get") == "1" { 339 html := fmt.Sprintf( 340 `<meta name="go-import" content="tangled.sh/%s git https://tangled.sh/%s"/> 341<meta name="go-import" content="tangled.org/%s git https://tangled.org/%s"/>`, 342 fullName, fullName, 343 fullName, fullName, 344 ) 345 w.Header().Set("Content-Type", "text/html") 346 w.Write([]byte(html)) 347 return 348 } 349 } 350 351 next.ServeHTTP(w, r) 352 }) 353 } 354}