package forge import ( "bytes" "net/http" "net/http/httptest" "net/url" "os" "os/exec" "path/filepath" "strconv" "strings" "testing" "time" ) func pagesGet(h http.Handler, host, path string) *httptest.ResponseRecorder { r := httptest.NewRequest("GET", path, nil) r.Host = host r.RemoteAddr = "192.0.2.1:1000" w := httptest.NewRecorder() h.ServeHTTP(w, r) return w } func addEntry(t *testing.T, g Git, branch, mode, name, content string) string { t.Helper() parent, _ := g.Resolve("refs/heads/" + branch) blob, err := g.run([]byte(content), "hash-object", "-w", "--stdin") if err != nil { t.Fatal(err) } index := filepath.Join(t.TempDir(), "index") env := []string{"GIT_INDEX_FILE=" + index} if parent != "" { g.runEnv(env, nil, "read-tree", parent) } if _, err := g.runEnv(env, nil, "update-index", "--add", "--cacheinfo", mode+","+strings.TrimSpace(string(blob))+","+name); err != nil { t.Fatal(err) } tree, _ := g.runEnv(env, nil, "write-tree") args := []string{"commit-tree", strings.TrimSpace(string(tree))} if parent != "" { args = append(args, "-p", parent) } out, err := g.runEnv(ident("T", "t@example.org", timeNow()), []byte("add "+name), args...) if err != nil { t.Fatal(err) } hash := strings.TrimSpace(string(out)) g.run(nil, "update-ref", "refs/heads/"+branch, hash) return hash } func newRepo(t *testing.T, s *Server, owner User, name string, vis int) (Repo, Git) { t.Helper() repo, err := s.store.CreateRepo(owner, name, "", vis, "") if err != nil { t.Fatal(err) } g := Git{Dir: s.repoDir(repo)} if err := initRepo(g.Dir); err != nil { t.Fatal(err) } return repo, g } func age(t *testing.T, s *Server, u User) { t.Helper() if _, err := s.store.db.Exec(`update users set created = ? where id = ?`, time.Now().Add(-48*time.Hour).Unix(), u.ID); err != nil { t.Fatal(err) } } func TestPages(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git not installed") } s, h := testServer(t) alice, _ := s.store.CreateUser("alice", "correct horse") age(t, s, alice) _, g := newRepo(t, s, alice, "pages", Public) main := g.Head() addEntry(t, g, main, "100644", "index.html", "

home

") addEntry(t, g, main, "100644", "about.html", "about me") addEntry(t, g, main, "100644", "style.css", "body{}") addEntry(t, g, main, "100644", "docs/index.html", "docs") addEntry(t, g, main, "120000", "link", "/etc/passwd") addEntry(t, g, main, "100644", "404.html", "missing") host := "alice.example.org" cases := []struct { path, body, ctype string code int }{ {"/", "

home

", "text/html", 200}, {"/index.html", "

home

", "text/html", 200}, {"/about", "about me", "text/html", 200}, {"/style.css", "body{}", "text/css", 200}, {"/docs/", "docs", "text/html", 200}, {"/link", "missing", "text/html", 404}, {"/nope", "missing", "text/html", 404}, {"/../../../etc/passwd", "", "", 404}, {"/%2e%2e/%2e%2e/etc/passwd", "", "", 404}, } for _, c := range cases { w := pagesGet(h, host, c.path) if w.Code != c.code || (c.body != "" && w.Body.String() != c.body) || !strings.HasPrefix(w.Header().Get("Content-Type"), c.ctype) { t.Errorf("%s: %d %q %s", c.path, w.Code, w.Body.String(), w.Header().Get("Content-Type")) } if len(w.Result().Cookies()) > 0 || w.Header().Get("X-Content-Type-Options") != "nosniff" { t.Errorf("%s: cookies or missing nosniff", c.path) } } if w := pagesGet(h, host, "/docs"); w.Code != http.StatusMovedPermanently || w.Header().Get("Location") != "/docs/" { t.Errorf("directory without slash: %d %s", w.Code, w.Header().Get("Location")) } r := httptest.NewRequest("POST", "/", nil) r.Host = host w := httptest.NewRecorder() h.ServeHTTP(w, r) if w.Code != http.StatusMethodNotAllowed { t.Errorf("post to pages: %d", w.Code) } _, pg := newRepo(t, s, alice, "proj", Public) addEntry(t, pg, pg.Head(), "100644", "README.md", "readme") addEntry(t, pg, "pages", "100644", "index.html", "project site") if w := pagesGet(h, host, "/proj/"); w.Body.String() != "project site" { t.Errorf("project site: %d %q", w.Code, w.Body.String()) } if w := pagesGet(h, host, "/proj"); w.Code != http.StatusMovedPermanently { t.Errorf("project root redirect: %d", w.Code) } _, secret := newRepo(t, s, alice, "secret", Private) addEntry(t, secret, secret.Head(), "100644", "source.txt", "private source") if w := pagesGet(h, host, "/secret/source.txt"); strings.Contains(w.Body.String(), "private source") { t.Error("private repository without a pages branch served") } addEntry(t, secret, "pages", "100644", "index.html", "private site") if w := pagesGet(h, host, "/secret/"); w.Body.String() != "private site" { t.Errorf("private pages branch: %d %q", w.Code, w.Body.String()) } if w := pagesGet(h, host, "/secret/source.txt"); strings.Contains(w.Body.String(), "private source") { t.Error("private default branch leaked through pages") } for _, host := range []string{"www.example.org", "a.b.example.org", "x_y.example.org"} { if w := pagesGet(h, host, "/"); w.Code != 404 { t.Errorf("%s: %d", host, w.Code) } } if w := pagesGet(h, "alice.example.org.", "/"); w.Body.String() != "

home

" { t.Error("trailing dot host not served as pages") } if w := pagesGet(h, "example.org", "/"); w.Header().Get("Content-Security-Policy") == "" { t.Error("main site lost its headers") } carol, _ := s.store.CreateUser("carol", "correct horse") age(t, s, carol) _, cg := newRepo(t, s, carol, "pages", Private) addEntry(t, cg, cg.Head(), "100644", "index.html", "draft") if w := pagesGet(h, "carol.example.org", "/"); w.Code != 404 { t.Errorf("private pages repository served its default branch: %d", w.Code) } addEntry(t, cg, "pages", "100644", "index.html", "published") if w := pagesGet(h, "carol.example.org", "/"); w.Body.String() != "published" { t.Errorf("private pages branch: %d %q", w.Code, w.Body.String()) } bob, _ := s.store.CreateUser("bob", "correct horse") _, bg := newRepo(t, s, bob, "pages", Public) addEntry(t, bg, bg.Head(), "100644", "index.html", "fresh") if w := pagesGet(h, "bob.example.org", "/"); w.Code != 404 { t.Errorf("new account served pages: %d", w.Code) } age(t, s, bob) if w := pagesGet(h, "bob.example.org", "/"); w.Code != 200 { t.Errorf("aged account not served: %d", w.Code) } s.store.SetPages("bob", false) if w := pagesGet(h, "bob.example.org", "/"); w.Code != 404 { t.Errorf("disabled pages served: %d", w.Code) } s.store.SetPages("bob", true) s.store.SetBanned("bob", true) if w := pagesGet(h, "bob.example.org", "/"); w.Code != 404 { t.Errorf("banned account served pages: %d", w.Code) } } func TestReservedNames(t *testing.T) { s, _ := testServer(t) for _, name := range []string{"www", "mail", "pages"} { if _, err := s.store.CreateUser(name, "correct horse"); err == nil { t.Errorf("registered reserved name %s", name) } } } type pushRig struct { t *testing.T s *Server url string work string } func (p pushRig) git(args ...string) (string, error) { cmd := exec.Command("git", append([]string{"-C", p.work}, args...)...) cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_AUTHOR_NAME=B", "GIT_AUTHOR_EMAIL=b@example.org", "GIT_COMMITTER_NAME=B", "GIT_COMMITTER_EMAIL=b@example.org") out, err := cmd.CombinedOutput() return string(out), err } func (p pushRig) must(args ...string) string { p.t.Helper() out, err := p.git(args...) if err != nil { p.t.Fatalf("git %v: %v\n%s", args, err, out) } return out } func (p pushRig) commit(name, content, message string) { p.t.Helper() os.WriteFile(filepath.Join(p.work, name), []byte(content), 0o644) p.must("add", name) p.must("commit", "-q", "-m", message) } func remote(base, user, repo string) string { u, _ := url.Parse(base) u.User = url.UserPassword(user, "correct horse") u.Path = "/~" + repo return u.String() } func TestAgit(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git not installed") } if testing.Short() { t.Skip("builds the binary") } s, h := testServer(t) bin := filepath.Join(t.TempDir(), "gitpocket") if out, err := exec.Command("go", "build", "-o", bin, "..").CombinedOutput(); err != nil { t.Fatalf("build: %v\n%s", err, out) } for _, kind := range []string{"pre-receive", "proc-receive"} { if err := writeHook(filepath.Join(hooksDir(s.root), kind), hookScript(s.root, "example.org", bin, kind)); err != nil { t.Fatal(err) } } srv := httptest.NewServer(h) defer srv.Close() alice, _ := s.store.CreateUser("alice", "correct horse") bob, _ := s.store.CreateUser("bob", "correct horse") repo, g := newRepo(t, s, alice, "proj", Public) base := commitFile(t, g, g.Head(), "", "a.txt", "one\n") p := pushRig{t: t, s: s, work: t.TempDir()} p.must("init", "-q", "-b", "main") p.must("fetch", "-q", remote(srv.URL, "bob", "alice/proj"), "main") p.must("reset", "-q", "--hard", "FETCH_HEAD") p.commit("b.txt", "two\n", "Add b\n\nBody text") bobURL := remote(srv.URL, "bob", "alice/proj") for _, ref := range []string{"HEAD:refs/heads/main", "HEAD:refs/heads/evil", "HEAD:refs/tags/v1", "HEAD:refs/pull/9/head"} { if out, err := p.git("push", bobURL, ref); err == nil { t.Errorf("non-writer push to %s accepted\n%s", ref, out) } } if head, _ := g.Resolve("refs/heads/main"); head != base { t.Fatal("main moved after refused pushes") } if out, err := p.git("push", "-o", "title=Hello there", bobURL, "HEAD:refs/for/main"); err != nil || !strings.Contains(out, "pulls/1") { t.Fatalf("agit push failed: %v\n%s", err, out) } pull, err := s.store.Pull(repo, 1) if err != nil || pull.Agit != "main" || pull.Author.ID != bob.ID || pull.Title != "Hello there" || pull.TargetBranch != "main" { t.Fatalf("pull request not created as expected: %+v %v", pull, err) } first := strings.TrimSpace(p.must("rev-parse", "HEAD")) if got, _ := g.Resolve(pull.Ref()); got != first { t.Fatalf("pull ref %s, want %s", got, first) } p.commit("b.txt", "three\n", "Add b, again") if out, err := p.git("push", bobURL, "HEAD:refs/for/main"); err != nil || !strings.Contains(out, "updated pull request #1") { t.Fatalf("agit update failed: %v\n%s", err, out) } second := strings.TrimSpace(p.must("rev-parse", "HEAD")) if got, _ := g.Resolve(pull.Ref()); got != second { t.Error("pull ref not updated") } if n := len(g.RecordRevision(pull)); n != 2 { t.Errorf("revisions %d, want 2", n) } if out, err := p.git("push", bobURL, "HEAD:refs/for/main/other"); err != nil { t.Fatalf("topic push failed: %v\n%s", err, out) } if pr, err := s.store.Pull(repo, 2); err != nil || pr.Agit != "other" || pr.Title != "Add b, again" { t.Errorf("topic pull request: %+v %v", pr, err) } if _, err := p.git("push", bobURL, "HEAD:refs/for/nosuch"); err == nil { t.Error("push for a missing branch accepted") } if _, err := p.git("push", bobURL, base+":refs/for/main/stale"); err == nil { t.Error("push without new commits accepted") } aliceURL := remote(srv.URL, "alice", "alice/proj") if _, err := p.git("push", "-f", aliceURL, base+":refs/pull/1/head"); err == nil { t.Error("writer overwrote a pull ref directly") } if _, err := p.git("push", aliceURL, "HEAD:refs/heads/main"); err != nil { t.Error("writer push refused") } own, _ := s.store.CreateSession(alice.ID) if body := get(h, "/~alice/proj/pulls/1", own).Body.String(); !strings.Contains(body, "~bob:main") || strings.Contains(body, "Update from source") { t.Error("agit pull page does not show the topic") } s.store.SetBlock(alice.ID, bob.ID, true) p.commit("c.txt", "c\n", "Add c") if _, err := p.git("push", bobURL, "HEAD:refs/for/main/blocked"); err == nil { t.Error("blocked user opened a pull request") } s.store.SetBlock(alice.ID, bob.ID, false) secret, _ := newRepo(t, s, alice, "secret", Private) commitFile(t, Git{Dir: s.repoDir(secret)}, "main", "", "a.txt", "x\n") if _, err := p.git("push", remote(srv.URL, "bob", "alice/secret"), "HEAD:refs/for/main"); err == nil { t.Error("agit push into a private repository accepted") } s.store.AddMirror(repo.ID, "pull", "https://example.com/x.git", "oauth2", nil, time.Hour) if _, err := p.git("push", aliceURL, "HEAD:refs/heads/mirrored"); err == nil { t.Error("push into a pull mirror accepted") } s.store.db.Exec(`delete from mirrors`) os.Remove(filepath.Join(hooksDir(s.root), "proc-receive")) if _, err := p.git("push", aliceURL, "HEAD:refs/heads/nohooks"); err == nil { t.Error("push accepted without hooks installed") } } func TestProcReceiveParsing(t *testing.T) { var out bytes.Buffer for _, in := range []string{"", "0003", "ffff", "zzzz", "0008abc", "000eversion=2\n0000"} { out.Reset() if procReceive(t.TempDir(), strings.NewReader(in), &out, &out) == 0 { t.Errorf("accepted %q", in) } } var buf bytes.Buffer writePkt(&buf, "version=1\x00push-options") writePkt(&buf, "") writePkt(&buf, zeroOID+" "+strings.Repeat("a", 40)+" refs/for/main") writePkt(&buf, "") writePkt(&buf, "title=x") writePkt(&buf, "") out.Reset() if procReceive(t.TempDir(), &buf, &out, &out) != 0 || !strings.Contains(out.String(), "ng refs/for/main") { t.Errorf("command without gitpocket context not refused: %q", out.String()) } } func TestSeal(t *testing.T) { root := t.TempDir() box, err := seal(root, "hunter2") if err != nil || bytes.Contains(box, []byte("hunter2")) { t.Fatalf("seal: %v", err) } if got, err := unseal(root, box); err != nil || got != "hunter2" { t.Errorf("unseal: %q %v", got, err) } box[len(box)-1] ^= 1 if _, err := unseal(root, box); err == nil { t.Error("tampered credential accepted") } if fi, _ := os.Stat(filepath.Join(root, "mirror.key")); fi.Mode().Perm() != 0o600 { t.Errorf("key mode %v", fi.Mode().Perm()) } } func TestMirrorSettings(t *testing.T) { s, h := testServer(t) alice, _ := s.store.CreateUser("alice", "correct horse") bob, _ := s.store.CreateUser("bob", "correct horse") repo, g := newRepo(t, s, alice, "proj", Public) commitFile(t, g, g.Head(), "", "a.txt", "one\n") own, _ := s.store.CreateSession(alice.ID) other, _ := s.store.CreateSession(bob.ID) for _, bad := range []string{"http://example.com/x.git", "https://127.0.0.1/x.git", "https://user:pw@example.com/x.git", "file:///etc", "https://localhost/x"} { if w := post(h, "/~alice/proj/mirrors", url.Values{"action": {"add"}, "url": {bad}}, own); !strings.Contains(w.Header().Get("Location"), "error") { t.Errorf("mirror to %s accepted", bad) } } if w := post(h, "/~alice/proj/mirrors", url.Values{"action": {"add"}, "url": {"https://example.com/x.git"}, "token": {"tok"}}, other); w.Code != 403 { t.Errorf("non-owner added a mirror: %d", w.Code) } if len(s.store.Mirrors(repo.ID)) != 0 { t.Fatal("mirror stored after refusals") } if w := post(h, "/~alice/proj/mirrors", url.Values{"action": {"delete"}, "id": {"1"}}, other); w.Code != 403 { t.Errorf("non-owner touched mirrors: %d", w.Code) } if w := post(h, "/~alice/proj/mirrors", url.Values{"action": {"add"}, "url": {"https://example.com/x.git"}, "login": {"bad:name"}}, own); !strings.Contains(w.Header().Get("Location"), "error") { t.Error("login with a colon accepted") } box, _ := seal(s.root, "s3cr3t-token-value") s.store.AddMirror(repo.ID, "push", "https://example.com/x.git", "oauth2", box, 0) if body := get(h, "/~alice/proj/settings", own).Body.String(); strings.Contains(body, "s3cr3t") || !strings.Contains(body, "https://example.com/x.git") { t.Error("settings page leaks a token") } if s.store.IsPullMirror(repo.ID) { t.Error("push mirror counted as pull mirror") } s.store.AddMirror(repo.ID, "pull", "https://example.com/y.git", "oauth2", nil, time.Hour) if err := s.store.AddMirror(repo.ID, "pull", "https://example.com/z.git", "oauth2", nil, time.Hour); err == nil { t.Error("second pull mirror accepted") } if body := get(h, "/~alice/proj/new/"+g.Head()+"/x.txt", own); body.Code != 403 { t.Errorf("web edit on a pull mirror: %d", body.Code) } } func TestModeration(t *testing.T) { s, h := testServer(t) alice, _ := s.store.CreateUser("alice", "correct horse") bob, _ := s.store.CreateUser("bob", "correct horse") boss, _ := s.store.CreateUser("boss", "correct horse") s.store.SetAdmin("boss", true) s.store.CreateRepo(alice, "proj", "", Public, "") s.store.CreateRepo(alice, "hidden", "", Private, "") a, _ := s.store.CreateSession(alice.ID) b, _ := s.store.CreateSession(bob.ID) admin, _ := s.store.CreateSession(boss.ID) if w := post(h, "/~alice/proj/issues/new", url.Values{"title": {"spam"}, "body": {"x"}}, b); w.Code != http.StatusSeeOther { t.Fatalf("issue: %d", w.Code) } if w := post(h, "/report", url.Values{"kind": {"issue"}, "target": {"alice/proj#1"}, "reason": {"spam"}}, a); w.Code != 200 { t.Fatalf("report: %d %s", w.Code, w.Body.String()) } if w := post(h, "/report", url.Values{"kind": {"issue"}, "target": {"alice/proj#1"}, "reason": {"spam"}}, a); w.Code != 400 { t.Error("duplicate report accepted") } if w := post(h, "/report", url.Values{"kind": {"repo"}, "target": {"alice/hidden"}, "reason": {"x"}}, b); w.Code != 404 { t.Error("report reveals a private repository") } if w := get(h, "/admin?s=reports", admin); !strings.Contains(w.Body.String(), "alice/proj#1") { t.Error("report missing from admin") } if w := get(h, "/admin?s=reports", b); w.Code == 200 { t.Error("non-admin sees reports") } rep := s.store.Reports("open")[0] if w := post(h, "/admin", url.Values{"s": {"reports"}, "action": {"moderate"}, "report": {itoa(rep.ID)}, "do": {"ban"}}, admin); strings.Contains(w.Header().Get("Location"), "error") { t.Fatalf("ban: %s", w.Header().Get("Location")) } if !s.store.IsBanned(bob.ID) || s.store.IsBanned(alice.ID) { t.Error("ban hit the wrong account") } s.store.SetBanned("bob", false) b, _ = s.store.CreateSession(bob.ID) if len(s.store.Reports("open")) != 0 { t.Error("report still open") } if w := post(h, "/block", url.Values{"name": {"bob"}}, a); w.Code != http.StatusSeeOther { t.Fatalf("block: %d", w.Code) } if w := post(h, "/~alice/proj/issues/new", url.Values{"title": {"again"}}, b); w.Code != 403 { t.Errorf("blocked user opened an issue: %d", w.Code) } if w := post(h, "/~alice/proj/issues/1/comment", url.Values{"body": {"hi"}}, b); w.Code != 403 { t.Errorf("blocked user commented: %d", w.Code) } if w := post(h, "/~alice/proj/star", nil, b); w.Code != 403 { t.Errorf("blocked user starred: %d", w.Code) } if w := post(h, "/~alice/follow", nil, b); w.Code != 403 { t.Errorf("blocked user followed: %d", w.Code) } if w := post(h, "/~alice/proj/fork", nil, b); w.Code != 403 { t.Errorf("blocked user forked: %d", w.Code) } if !strings.Contains(get(h, "/settings", a).Body.String(), "~bob") { t.Error("blocked list missing from settings") } post(h, "/block", url.Values{"name": {"bob"}, "action": {"unblock"}}, a) if s.store.Blocked(alice.ID, bob.ID) { t.Error("unblock failed") } if w := post(h, "/block", url.Values{"name": {"bob"}}, b); w.Code != 400 { t.Error("blocked yourself") } for i := 0; i < threadsPerHour; i++ { s.store.CreateIssue(Repo{ID: 1}, bob, "t", "") } if w := post(h, "/~alice/proj/issues/new", url.Values{"title": {"flood"}}, b); w.Code != 429 { t.Errorf("issue flood not limited: %d", w.Code) } } func itoa(n int64) string { return strconv.FormatInt(n, 10) } func TestDomains(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git not installed") } s, h := testServer(t) for _, bad := range []string{"", "localhost", "127.0.0.1", "example.org", "x.example.org", "a..b", "-a.com", "a_b.com", strings.Repeat("a", 64) + ".com"} { if _, err := s.cleanDomain(bad); err == nil { t.Errorf("accepted %q", bad) } } if d, err := s.cleanDomain(" HTTPS://Blog.Example.COM. "); err != nil || d != "blog.example.com" { t.Errorf("clean: %q %v", d, err) } alice, _ := s.store.CreateUser("alice", "correct horse") bob, _ := s.store.CreateUser("bob", "correct horse") age(t, s, alice) _, g := newRepo(t, s, alice, "pages", Public) addEntry(t, g, g.Head(), "100644", "index.html", "alice blog") if err := s.store.ClaimDomain(alice.ID, "blog.example.com"); err != nil { t.Fatal(err) } if err := s.store.ClaimDomain(bob.ID, "blog.example.com"); err == nil { t.Error("two accounts claimed one domain") } if w := pagesGet(h, "blog.example.com", "/"); strings.Contains(w.Body.String(), "alice blog") { t.Error("unverified domain served") } own, _ := s.store.CreateSession(alice.ID) if body := get(h, "/settings", own).Body.String(); !strings.Contains(body, "_gitpocket-pages.blog.example.com") { t.Error("settings do not show the verification record") } s.store.MarkDomain(alice.ID, true) if w := pagesGet(h, "Blog.Example.com.", "/"); w.Body.String() != "alice blog" || len(w.Result().Cookies()) > 0 { t.Errorf("verified domain: %d %q", w.Code, w.Body.String()) } if w := post(h, "/settings/pages", url.Values{"action": {"claim"}, "domain": {"alice.example.org"}}, own); !strings.Contains(w.Header().Get("Location"), "error") { t.Error("claimed a subdomain of the host") } post(h, "/settings/pages", url.Values{"action": {"remove"}}, own) if _, ok := s.store.DomainOwner("blog.example.com"); ok { t.Error("removed domain still served") } } func TestCommitAccount(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git not installed") } s, h := testServer(t) tee, _ := s.store.CreateUser("tee", "correct horse") _, g := newRepo(t, s, tee, "proj", Public) hash := addEntry(t, g, g.Head(), "100644", "README.md", "hello") link := `~tee` paths := []string{"/~tee/proj/log", "/~tee/proj/commit/" + hash, "/~tee/proj/blame/" + g.Head() + "/README.md"} for _, p := range paths { if w := pagesGet(h, "example.org", p); w.Code != 200 || strings.Contains(w.Body.String(), link) { t.Errorf("%s before verification: %d", p, w.Code) } } s.store.MarkVerified(tee.ID, "t@example.org") for _, p := range paths { if w := pagesGet(h, "example.org", p); w.Code != 200 || !strings.Contains(w.Body.String(), link) { t.Errorf("%s after verification: %d", p, w.Code) } } }