forked from tangled.org/core
this repo has no description
1package middleware 2 3import ( 4 "context" 5 "fmt" 6 "log" 7 "net/http" 8 "slices" 9 "strconv" 10 "strings" 11 "time" 12 13 "github.com/bluesky-social/indigo/atproto/identity" 14 "github.com/go-chi/chi/v5" 15 "tangled.sh/tangled.sh/core/appview/db" 16 "tangled.sh/tangled.sh/core/appview/idresolver" 17 "tangled.sh/tangled.sh/core/appview/oauth" 18 "tangled.sh/tangled.sh/core/appview/pages" 19 "tangled.sh/tangled.sh/core/appview/pagination" 20 "tangled.sh/tangled.sh/core/appview/reporesolver" 21 "tangled.sh/tangled.sh/core/rbac" 22) 23 24type Middleware struct { 25 oauth *oauth.OAuth 26 db *db.DB 27 enforcer *rbac.Enforcer 28 repoResolver *reporesolver.RepoResolver 29 idResolver *idresolver.Resolver 30 pages *pages.Pages 31} 32 33func New(oauth *oauth.OAuth, db *db.DB, enforcer *rbac.Enforcer, repoResolver *reporesolver.RepoResolver, idResolver *idresolver.Resolver, pages *pages.Pages) Middleware { 34 return Middleware{ 35 oauth: oauth, 36 db: db, 37 enforcer: enforcer, 38 repoResolver: repoResolver, 39 idResolver: idResolver, 40 pages: pages, 41 } 42} 43 44type middlewareFunc func(http.Handler) http.Handler 45 46func AuthMiddleware(a *oauth.OAuth) middlewareFunc { 47 return func(next http.Handler) http.Handler { 48 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 49 redirectFunc := func(w http.ResponseWriter, r *http.Request) { 50 http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) 51 } 52 if r.Header.Get("HX-Request") == "true" { 53 redirectFunc = func(w http.ResponseWriter, _ *http.Request) { 54 w.Header().Set("HX-Redirect", "/login") 55 w.WriteHeader(http.StatusOK) 56 } 57 } 58 59 _, auth, err := a.GetSession(r) 60 if err != nil { 61 log.Println("not logged in, redirecting", "err", err) 62 redirectFunc(w, r) 63 return 64 } 65 66 if !auth { 67 log.Printf("not logged in, redirecting") 68 redirectFunc(w, r) 69 return 70 } 71 72 next.ServeHTTP(w, r) 73 }) 74 } 75} 76 77func Paginate(next http.Handler) http.Handler { 78 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 79 page := pagination.FirstPage() 80 81 offsetVal := r.URL.Query().Get("offset") 82 if offsetVal != "" { 83 offset, err := strconv.Atoi(offsetVal) 84 if err != nil { 85 log.Println("invalid offset") 86 } else { 87 page.Offset = offset 88 } 89 } 90 91 limitVal := r.URL.Query().Get("limit") 92 if limitVal != "" { 93 limit, err := strconv.Atoi(limitVal) 94 if err != nil { 95 log.Println("invalid limit") 96 } else { 97 page.Limit = limit 98 } 99 } 100 101 ctx := context.WithValue(r.Context(), "page", page) 102 next.ServeHTTP(w, r.WithContext(ctx)) 103 }) 104} 105 106func (mw Middleware) knotRoleMiddleware(group string) middlewareFunc { 107 return func(next http.Handler) http.Handler { 108 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 109 // requires auth also 110 actor := mw.oauth.GetUser(r) 111 if actor == nil { 112 // we need a logged in user 113 log.Printf("not logged in, redirecting") 114 http.Error(w, "Forbiden", http.StatusUnauthorized) 115 return 116 } 117 domain := chi.URLParam(r, "domain") 118 if domain == "" { 119 http.Error(w, "malformed url", http.StatusBadRequest) 120 return 121 } 122 123 ok, err := mw.enforcer.E.HasGroupingPolicy(actor.Did, group, domain) 124 if err != nil || !ok { 125 // we need a logged in user 126 log.Printf("%s does not have perms of a %s in domain %s", actor.Did, group, domain) 127 http.Error(w, "Forbiden", http.StatusUnauthorized) 128 return 129 } 130 131 next.ServeHTTP(w, r) 132 }) 133 } 134} 135 136func (mw Middleware) KnotOwner() middlewareFunc { 137 return mw.knotRoleMiddleware("server:owner") 138} 139 140func (mw Middleware) RepoPermissionMiddleware(requiredPerm string) middlewareFunc { 141 return func(next http.Handler) http.Handler { 142 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 143 // requires auth also 144 actor := mw.oauth.GetUser(r) 145 if actor == nil { 146 // we need a logged in user 147 log.Printf("not logged in, redirecting") 148 http.Error(w, "Forbiden", http.StatusUnauthorized) 149 return 150 } 151 f, err := mw.repoResolver.Resolve(r) 152 if err != nil { 153 http.Error(w, "malformed url", http.StatusBadRequest) 154 return 155 } 156 157 ok, err := mw.enforcer.E.Enforce(actor.Did, f.Knot, f.DidSlashRepo(), requiredPerm) 158 if err != nil || !ok { 159 // we need a logged in user 160 log.Printf("%s does not have perms of a %s in repo %s", actor.Did, requiredPerm, f.OwnerSlashRepo()) 161 http.Error(w, "Forbiden", http.StatusUnauthorized) 162 return 163 } 164 165 next.ServeHTTP(w, r) 166 }) 167 } 168} 169 170func StripLeadingAt(next http.Handler) http.Handler { 171 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 172 path := req.URL.EscapedPath() 173 if strings.HasPrefix(path, "/@") { 174 req.URL.RawPath = "/" + strings.TrimPrefix(path, "/@") 175 } 176 next.ServeHTTP(w, req) 177 }) 178} 179 180func (mw Middleware) ResolveIdent() middlewareFunc { 181 excluded := []string{"favicon.ico"} 182 183 return func(next http.Handler) http.Handler { 184 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 185 didOrHandle := chi.URLParam(req, "user") 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.Println("failed to resolve did/handle:", err) 195 w.WriteHeader(http.StatusNotFound) 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 id, ok := req.Context().Value("resolvedId").(identity.Identity) 211 if !ok { 212 log.Println("malformed middleware") 213 w.WriteHeader(http.StatusInternalServerError) 214 return 215 } 216 217 repo, err := db.GetRepo(mw.db, id.DID.String(), repoName) 218 if err != nil { 219 // invalid did or handle 220 log.Println("failed to resolve repo") 221 mw.pages.Error404(w) 222 return 223 } 224 225 ctx := context.WithValue(req.Context(), "knot", repo.Knot) 226 ctx = context.WithValue(ctx, "repoAt", repo.AtUri) 227 ctx = context.WithValue(ctx, "repoDescription", repo.Description) 228 ctx = context.WithValue(ctx, "repoAddedAt", repo.Created.Format(time.RFC3339)) 229 next.ServeHTTP(w, req.WithContext(ctx)) 230 }) 231 } 232} 233 234// middleware that is tacked on top of /{user}/{repo}/pulls/{pull} 235func (mw Middleware) ResolvePull() middlewareFunc { 236 return func(next http.Handler) http.Handler { 237 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 238 f, err := mw.repoResolver.Resolve(r) 239 if err != nil { 240 log.Println("failed to fully resolve repo", err) 241 http.Error(w, "invalid repo url", http.StatusNotFound) 242 return 243 } 244 245 prId := chi.URLParam(r, "pull") 246 prIdInt, err := strconv.Atoi(prId) 247 if err != nil { 248 http.Error(w, "bad pr id", http.StatusBadRequest) 249 log.Println("failed to parse pr id", err) 250 return 251 } 252 253 pr, err := db.GetPull(mw.db, f.RepoAt, prIdInt) 254 if err != nil { 255 log.Println("failed to get pull and comments", err) 256 return 257 } 258 259 ctx := context.WithValue(r.Context(), "pull", pr) 260 261 if pr.IsStacked() { 262 stack, err := db.GetStack(mw.db, pr.StackId) 263 if err != nil { 264 log.Println("failed to get stack", err) 265 return 266 } 267 abandonedPulls, err := db.GetAbandonedPulls(mw.db, pr.StackId) 268 if err != nil { 269 log.Println("failed to get abandoned pulls", err) 270 return 271 } 272 273 ctx = context.WithValue(ctx, "stack", stack) 274 ctx = context.WithValue(ctx, "abandonedPulls", abandonedPulls) 275 } 276 277 next.ServeHTTP(w, r.WithContext(ctx)) 278 }) 279 } 280} 281 282// this should serve the go-import meta tag even if the path is technically 283// a 404 like tangled.sh/oppi.li/go-git/v5 284func (mw Middleware) GoImport() middlewareFunc { 285 return func(next http.Handler) http.Handler { 286 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 287 f, err := mw.repoResolver.Resolve(r) 288 if err != nil { 289 log.Println("failed to fully resolve repo", err) 290 http.Error(w, "invalid repo url", http.StatusNotFound) 291 return 292 } 293 294 fullName := f.OwnerHandle() + "/" + f.RepoName 295 296 if r.Header.Get("User-Agent") == "Go-http-client/1.1" { 297 if r.URL.Query().Get("go-get") == "1" { 298 html := fmt.Sprintf( 299 `<meta name="go-import" content="tangled.sh/%s git https://tangled.sh/%s"/>`, 300 fullName, 301 fullName, 302 ) 303 w.Header().Set("Content-Type", "text/html") 304 w.Write([]byte(html)) 305 return 306 } 307 } 308 309 next.ServeHTTP(w, r) 310 }) 311 } 312}