Commit 1a274bec3b
Verified · cmc
e2e/snippetweb_test.go +15
| @@ -138,6 +138,15 @@ func TestSnippetsWeb(t *testing.T) { | ||
| 138 | 138 | if status, _ := browserPost(t, alice, page+"/file", url.Values{"name": {"b.txt"}, "content": {"b\n"}}); status != 200 { |
| 139 | 139 | t.Fatal("file add failed") |
| 140 | 140 | } |
| 141 | // Removing a file needs its name typed; a bare post is refused and | |
| 142 | // the file stays. | |
| 143 | _, body = browserPost(t, alice, page+"/file/remove", url.Values{"name": {"b.txt"}}) | |
| 144 | if !strings.Contains(body, "type b.txt to confirm") { | |
| 145 | t.Fatalf("unconfirmed file remove was not refused:\n%s", body) | |
| 146 | } | |
| 147 | if _, _, code := inst.ssh(t, aliceKey, "", "snippet", "file", "get", created, "b.txt"); code != 0 { | |
| 148 | t.Fatalf("b.txt removed without confirmation: exit %d", code) | |
| 149 | } | |
| 141 | 150 | if status, _ := browserPost(t, alice, page+"/file/remove", url.Values{"name": {"b.txt"}, "confirm": {"b.txt"}}); status != 200 { |
| 142 | 151 | t.Fatal("file remove failed") |
| 143 | 152 | } |
| @@ -169,6 +178,12 @@ func TestSnippetsWeb(t *testing.T) { | ||
| 169 | 178 | if status, _ := browserGet(t, bob, inst.base()+"/alice/-/snippets/"+unlisted); status != 200 { |
| 170 | 179 | t.Fatalf("stranger on an unlisted page: %d", status) |
| 171 | 180 | } |
| 181 | // An unconfirmed delete on a private snippet is still 404 for a | |
| 182 | // stranger: the snippet is resolved, and refused, before the | |
| 183 | // confirmation is even checked. | |
| 184 | if status, _ := browserPost(t, bob, inst.base()+"/alice/-/snippets/"+private+"/delete", nil); status != 404 { | |
| 185 | t.Fatalf("stranger's unconfirmed delete on a private snippet: %d", status) | |
| 186 | } | |
| 172 | 187 | if status, _ := browserPost(t, bob, inst.base()+"/alice/-/snippets/"+public+"/edit", url.Values{"description": {"x"}, "visibility": {"public"}}); status != 403 { |
| 173 | 188 | t.Fatalf("bob editing alice's snippet: %d", status) |
| 174 | 189 | } |
internal/httpd/account.go +4 −16
| @@ -37,10 +37,7 @@ func (s *Server) accountForm(w http.ResponseWriter, r *http.Request, u store.Use | ||
| 37 | 37 | var keys []accountKey |
| 38 | 38 | if list, err := s.st.ListSSHKeys(u.ID); err == nil { |
| 39 | 39 | for _, k := range list { |
| 40 | confirm := strings.TrimPrefix(k.Fingerprint, "SHA256:") | |
| 41 | if len(confirm) > 8 { | |
| 42 | confirm = confirm[:8] | |
| 43 | } | |
| 40 | confirm := prefix8(strings.TrimPrefix(k.Fingerprint, "SHA256:")) | |
| 44 | 41 | keys = append(keys, accountKey{Fingerprint: k.Fingerprint, Algo: k.Algo, Scope: k.Scope, Label: k.Label, Confirm: confirm}) |
| 45 | 42 | } |
| 46 | 43 | } |
| @@ -49,10 +46,7 @@ func (s *Server) accountForm(w http.ResponseWriter, r *http.Request, u store.Use | ||
| 49 | 46 | for _, k := range list { |
| 50 | 47 | var uids []string |
| 51 | 48 | json.Unmarshal([]byte(k.UIDsJSON), &uids) |
| 52 | confirm := k.Fingerprint | |
| 53 | if len(confirm) > 8 { | |
| 54 | confirm = confirm[:8] | |
| 55 | } | |
| 49 | confirm := prefix8(k.Fingerprint) | |
| 56 | 50 | pgp = append(pgp, accountPGP{ |
| 57 | 51 | Fingerprint: k.Fingerprint, UIDs: uids, |
| 58 | 52 | Expired: k.ExpiresAt != nil, Revoked: k.RevokedAt != nil, Confirm: confirm, |
| @@ -163,10 +157,7 @@ func (s *Server) accountSubmit(w http.ResponseWriter, r *http.Request, u store.U | ||
| 163 | 157 | } |
| 164 | 158 | back("", "key registered") |
| 165 | 159 | case "key-remove": |
| 166 | want := strings.TrimPrefix(r.FormValue("fingerprint"), "SHA256:") | |
| 167 | if len(want) > 8 { | |
| 168 | want = want[:8] | |
| 169 | } | |
| 160 | want := prefix8(strings.TrimPrefix(r.FormValue("fingerprint"), "SHA256:")) | |
| 170 | 161 | if ok, msg := confirmed(r, want); !ok { |
| 171 | 162 | back(msg, "") |
| 172 | 163 | return |
| @@ -189,10 +180,7 @@ func (s *Server) accountSubmit(w http.ResponseWriter, r *http.Request, u store.U | ||
| 189 | 180 | back("", "PGP key registered") |
| 190 | 181 | case "pgp-remove": |
| 191 | 182 | fp := r.FormValue("fingerprint") |
| 192 | want := fp | |
| 193 | if len(want) > 8 { | |
| 194 | want = want[:8] | |
| 195 | } | |
| 183 | want := prefix8(fp) | |
| 196 | 184 | if ok, msg := confirmed(r, want); !ok { |
| 197 | 185 | back(msg, "") |
| 198 | 186 | return |
internal/httpd/confirm.go +9
| @@ -15,3 +15,12 @@ func confirmed(r *http.Request, want string) (bool, string) { | ||
| 15 | 15 | } |
| 16 | 16 | return false, "type " + want + " to confirm" |
| 17 | 17 | } |
| 18 | ||
| 19 | // prefix8 returns s truncated to its first 8 characters, or s unchanged | |
| 20 | // if it is shorter. | |
| 21 | func prefix8(s string) string { | |
| 22 | if len(s) > 8 { | |
| 23 | return s[:8] | |
| 24 | } | |
| 25 | return s | |
| 26 | } | |
internal/httpd/labels.go +1 −1
| @@ -44,7 +44,7 @@ func (s *Server) labelSubmit(w http.ResponseWriter, r *http.Request, u store.Use | ||
| 44 | 44 | name := strings.TrimSpace(r.FormValue("name")) |
| 45 | 45 | back := func(w http.ResponseWriter, r *http.Request, msg string) { s.backTo(w, r, "labels", msg) } |
| 46 | 46 | if name == "" { |
| 47 | s.backTo(w, r, "labels", "name the label") | |
| 47 | back(w, r, "name the label") | |
| 48 | 48 | return |
| 49 | 49 | } |
| 50 | 50 | argv := []string{"label", "set", repo, name, "--color", strings.TrimSpace(r.FormValue("color"))} |
internal/httpd/snippets.go +30 −17
| @@ -178,14 +178,12 @@ func (s *Server) snippetNewSubmit(w http.ResponseWriter, r *http.Request, u stor | ||
| 178 | 178 | http.Redirect(w, r, "/"+u.Username+"/-/snippets/"+out.ID, http.StatusSeeOther) |
| 179 | 179 | } |
| 180 | 180 | |
| 181 | // snippetAction runs a write on the snippet in the URL and returns to its | |
| 182 | // page with the message, or to dest (the list, for a delete) on success. | |
| 183 | // A snippet the viewer may not read is the 404 page, as on every read. | |
| 184 | func (s *Server) snippetAction(w http.ResponseWriter, r *http.Request, u store.User, argv []string, stdin string, dest string) { | |
| 185 | sn, _, ok := s.snippetScope(w, r) | |
| 186 | if !ok { | |
| 187 | return | |
| 188 | } | |
| 181 | // snippetAction runs a write on an already-resolved snippet and returns to | |
| 182 | // its page with the message, or to dest (the list, for a delete) on | |
| 183 | // success. Callers resolve the snippet with snippetScope first, so a | |
| 184 | // snippet the viewer may not read is the 404 page before any confirmation | |
| 185 | // or write is considered. | |
| 186 | func (s *Server) snippetAction(w http.ResponseWriter, r *http.Request, u store.User, sn store.Snippet, argv []string, stdin string, dest string) { | |
| 189 | 187 | page := "/" + sn.OwnerName + "/-/snippets/" + sn.PublicID |
| 190 | 188 | if dest == "" { |
| 191 | 189 | dest = page |
| @@ -207,35 +205,50 @@ func (s *Server) snippetAction(w http.ResponseWriter, r *http.Request, u store.U | ||
| 207 | 205 | } |
| 208 | 206 | |
| 209 | 207 | func (s *Server) snippetEditSubmit(w http.ResponseWriter, r *http.Request, u store.User) { |
| 210 | s.snippetAction(w, r, u, []string{"snippet", "edit", r.PathValue("id"), | |
| 208 | sn, _, ok := s.snippetScope(w, r) | |
| 209 | if !ok { | |
| 210 | return | |
| 211 | } | |
| 212 | s.snippetAction(w, r, u, sn, []string{"snippet", "edit", r.PathValue("id"), | |
| 211 | 213 | "--description", strings.TrimSpace(r.FormValue("description")), |
| 212 | 214 | "--visibility", r.FormValue("visibility")}, "", "") |
| 213 | 215 | } |
| 214 | 216 | |
| 215 | 217 | func (s *Server) snippetDeleteSubmit(w http.ResponseWriter, r *http.Request, u store.User) { |
| 216 | id := r.PathValue("id") | |
| 217 | if ok, msg := confirmed(r, id); !ok { | |
| 218 | sn, _, ok := s.snippetScope(w, r) | |
| 219 | if !ok { | |
| 220 | return | |
| 221 | } | |
| 222 | if ok, msg := confirmed(r, sn.PublicID); !ok { | |
| 218 | 223 | s.setFlash(w, msg) |
| 219 | http.Redirect(w, r, "/"+r.PathValue("owner")+"/-/snippets/"+id, http.StatusSeeOther) | |
| 224 | http.Redirect(w, r, "/"+sn.OwnerName+"/-/snippets/"+sn.PublicID, http.StatusSeeOther) | |
| 220 | 225 | return |
| 221 | 226 | } |
| 222 | s.snippetAction(w, r, u, []string{"snippet", "delete", id}, "", | |
| 223 | "/"+r.PathValue("owner")+"/-/snippets") | |
| 227 | s.snippetAction(w, r, u, sn, []string{"snippet", "delete", sn.PublicID}, "", | |
| 228 | "/"+sn.OwnerName+"/-/snippets") | |
| 224 | 229 | } |
| 225 | 230 | |
| 226 | 231 | // An empty textarea reaches the command as empty stdin, which it refuses; |
| 227 | 232 | // the message lands on the page like any other. |
| 228 | 233 | func (s *Server) snippetFileSubmit(w http.ResponseWriter, r *http.Request, u store.User) { |
| 229 | s.snippetAction(w, r, u, []string{"snippet", "file", "set", r.PathValue("id"), strings.TrimSpace(r.FormValue("name"))}, | |
| 234 | sn, _, ok := s.snippetScope(w, r) | |
| 235 | if !ok { | |
| 236 | return | |
| 237 | } | |
| 238 | s.snippetAction(w, r, u, sn, []string{"snippet", "file", "set", r.PathValue("id"), strings.TrimSpace(r.FormValue("name"))}, | |
| 230 | 239 | r.FormValue("content"), "") |
| 231 | 240 | } |
| 232 | 241 | |
| 233 | 242 | func (s *Server) snippetFileRemoveSubmit(w http.ResponseWriter, r *http.Request, u store.User) { |
| 243 | sn, _, ok := s.snippetScope(w, r) | |
| 244 | if !ok { | |
| 245 | return | |
| 246 | } | |
| 234 | 247 | name := strings.TrimSpace(r.FormValue("name")) |
| 235 | 248 | if ok, msg := confirmed(r, name); !ok { |
| 236 | 249 | s.setFlash(w, msg) |
| 237 | http.Redirect(w, r, "/"+r.PathValue("owner")+"/-/snippets/"+r.PathValue("id"), http.StatusSeeOther) | |
| 250 | http.Redirect(w, r, "/"+sn.OwnerName+"/-/snippets/"+sn.PublicID, http.StatusSeeOther) | |
| 238 | 251 | return |
| 239 | 252 | } |
| 240 | s.snippetAction(w, r, u, []string{"snippet", "file", "remove", r.PathValue("id"), name}, "", "") | |
| 253 | s.snippetAction(w, r, u, sn, []string{"snippet", "file", "remove", r.PathValue("id"), name}, "", "") | |
| 241 | 254 | } |