this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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