Monorepo for Tangled tangled.org
814
fork

Configure Feed

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

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