1package state
2
3import (
4 "context"
5 "fmt"
6 "log"
7 "net/http"
8 "strconv"
9 "strings"
10 "time"
11
12 "slices"
13
14 "github.com/bluesky-social/indigo/atproto/identity"
15 "github.com/go-chi/chi/v5"
16 "tangled.sh/tangled.sh/core/appview/db"
17 "tangled.sh/tangled.sh/core/appview/middleware"
18)
19
20func knotRoleMiddleware(s *State, group string) middleware.Middleware {
21 return func(next http.Handler) http.Handler {
22 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
23 // requires auth also
24 actor := s.oauth.GetUser(r)
25 if actor == nil {
26 // we need a logged in user
27 log.Printf("not logged in, redirecting")
28 http.Error(w, "Forbiden", http.StatusUnauthorized)
29 return
30 }
31 domain := chi.URLParam(r, "domain")
32 if domain == "" {
33 http.Error(w, "malformed url", http.StatusBadRequest)
34 return
35 }
36
37 ok, err := s.enforcer.E.HasGroupingPolicy(actor.Did, group, domain)
38 if err != nil || !ok {
39 // we need a logged in user
40 log.Printf("%s does not have perms of a %s in domain %s", actor.Did, group, domain)
41 http.Error(w, "Forbiden", http.StatusUnauthorized)
42 return
43 }
44
45 next.ServeHTTP(w, r)
46 })
47 }
48}
49
50func KnotOwner(s *State) middleware.Middleware {
51 return knotRoleMiddleware(s, "server:owner")
52}
53
54func RepoPermissionMiddleware(s *State, requiredPerm string) middleware.Middleware {
55 return func(next http.Handler) http.Handler {
56 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
57 // requires auth also
58 actor := s.oauth.GetUser(r)
59 if actor == nil {
60 // we need a logged in user
61 log.Printf("not logged in, redirecting")
62 http.Error(w, "Forbiden", http.StatusUnauthorized)
63 return
64 }
65 f, err := s.fullyResolvedRepo(r)
66 if err != nil {
67 http.Error(w, "malformed url", http.StatusBadRequest)
68 return
69 }
70
71 ok, err := s.enforcer.E.Enforce(actor.Did, f.Knot, f.DidSlashRepo(), requiredPerm)
72 if err != nil || !ok {
73 // we need a logged in user
74 log.Printf("%s does not have perms of a %s in repo %s", actor.Did, requiredPerm, f.OwnerSlashRepo())
75 http.Error(w, "Forbiden", http.StatusUnauthorized)
76 return
77 }
78
79 next.ServeHTTP(w, r)
80 })
81 }
82}
83
84func StripLeadingAt(next http.Handler) http.Handler {
85 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
86 path := req.URL.EscapedPath()
87 if strings.HasPrefix(path, "/@") {
88 req.URL.RawPath = "/" + strings.TrimPrefix(path, "/@")
89 }
90 next.ServeHTTP(w, req)
91 })
92}
93
94func ResolveIdent(s *State) middleware.Middleware {
95 excluded := []string{"favicon.ico"}
96
97 return func(next http.Handler) http.Handler {
98 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
99 didOrHandle := chi.URLParam(req, "user")
100 if slices.Contains(excluded, didOrHandle) {
101 next.ServeHTTP(w, req)
102 return
103 }
104
105 id, err := s.resolver.ResolveIdent(req.Context(), didOrHandle)
106 if err != nil {
107 // invalid did or handle
108 log.Println("failed to resolve did/handle:", err)
109 w.WriteHeader(http.StatusNotFound)
110 return
111 }
112
113 ctx := context.WithValue(req.Context(), "resolvedId", *id)
114
115 next.ServeHTTP(w, req.WithContext(ctx))
116 })
117 }
118}
119
120func ResolveRepo(s *State) middleware.Middleware {
121 return func(next http.Handler) http.Handler {
122 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
123 repoName := chi.URLParam(req, "repo")
124 id, ok := req.Context().Value("resolvedId").(identity.Identity)
125 if !ok {
126 log.Println("malformed middleware")
127 w.WriteHeader(http.StatusInternalServerError)
128 return
129 }
130
131 repo, err := db.GetRepo(s.db, id.DID.String(), repoName)
132 if err != nil {
133 // invalid did or handle
134 log.Println("failed to resolve repo")
135 s.pages.Error404(w)
136 return
137 }
138
139 ctx := context.WithValue(req.Context(), "knot", repo.Knot)
140 ctx = context.WithValue(ctx, "repoAt", repo.AtUri)
141 ctx = context.WithValue(ctx, "repoDescription", repo.Description)
142 ctx = context.WithValue(ctx, "repoAddedAt", repo.Created.Format(time.RFC3339))
143 next.ServeHTTP(w, req.WithContext(ctx))
144 })
145 }
146}
147
148// middleware that is tacked on top of /{user}/{repo}/pulls/{pull}
149func ResolvePull(s *State) middleware.Middleware {
150 return func(next http.Handler) http.Handler {
151 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
152 f, err := s.fullyResolvedRepo(r)
153 if err != nil {
154 log.Println("failed to fully resolve repo", err)
155 http.Error(w, "invalid repo url", http.StatusNotFound)
156 return
157 }
158
159 prId := chi.URLParam(r, "pull")
160 prIdInt, err := strconv.Atoi(prId)
161 if err != nil {
162 http.Error(w, "bad pr id", http.StatusBadRequest)
163 log.Println("failed to parse pr id", err)
164 return
165 }
166
167 pr, err := db.GetPull(s.db, f.RepoAt, prIdInt)
168 if err != nil {
169 log.Println("failed to get pull and comments", err)
170 return
171 }
172
173 ctx := context.WithValue(r.Context(), "pull", pr)
174
175 next.ServeHTTP(w, r.WithContext(ctx))
176 })
177 }
178}
179
180// this should serve the go-import meta tag even if the path is technically
181// a 404 like tangled.sh/oppi.li/go-git/v5
182func GoImport(s *State) middleware.Middleware {
183 return func(next http.Handler) http.Handler {
184 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
185 f, err := s.fullyResolvedRepo(r)
186 if err != nil {
187 log.Println("failed to fully resolve repo", err)
188 http.Error(w, "invalid repo url", http.StatusNotFound)
189 return
190 }
191
192 fullName := f.OwnerHandle() + "/" + f.RepoName
193
194 if r.Header.Get("User-Agent") == "Go-http-client/1.1" {
195 if r.URL.Query().Get("go-get") == "1" {
196 html := fmt.Sprintf(
197 `<meta name="go-import" content="tangled.sh/%s git https://tangled.sh/%s"/>`,
198 fullName,
199 fullName,
200 )
201 w.Header().Set("Content-Type", "text/html")
202 w.Write([]byte(html))
203 return
204 }
205 }
206
207 next.ServeHTTP(w, r)
208 })
209 }
210}