mirror of
https://github.com/tiennm99/ghglance.git
synced 2026-10-11 03:13:20 +00:00
The web UI is for a quick look at a profile, not for hosting embeddable images. Cards are inlined into /u/<user> as data: images, the per-card route is gone, and the page no longer offers copy links or Markdown snippets. The page itself stays shareable. Visitors can again paste their own token, with a button that opens GitHub's new-token page with the needed scopes ticked. A pasted token takes precedence over sign-in, follows the same ownership, privacy and cooldown rules, is never stored or logged, and is not revoked. The server still uses no token of its own.
183 lines
5.9 KiB
Go
183 lines
5.9 KiB
Go
package web
|
|
|
|
import (
|
|
"bytes"
|
|
"io/fs"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/tiennm99/ghglance/internal/github"
|
|
)
|
|
|
|
// assertTokenNowhere fails when token reached a file under the data dir or
|
|
// the log.
|
|
func assertTokenNowhere(t *testing.T, s *Server, logs *lockedBuffer, token string) {
|
|
t.Helper()
|
|
filepath.WalkDir(s.store.dir, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil || !d.Type().IsRegular() {
|
|
return err
|
|
}
|
|
raw, _ := os.ReadFile(path)
|
|
if bytes.Contains(raw, []byte(token)) {
|
|
t.Errorf("token written to %s", path)
|
|
}
|
|
return nil
|
|
})
|
|
if strings.Contains(logs.String(), token) {
|
|
t.Errorf("log contains the token:\n%s", logs.String())
|
|
}
|
|
}
|
|
|
|
func TestPastedTokenTakesPrecedence(t *testing.T) {
|
|
logs := captureLog(t)
|
|
g := newFakeGitHub(t)
|
|
f := &fakeFetcher{tokens: map[string]github.TokenInfo{testToken: {Login: "octocat", CanReadPrivate: true}}}
|
|
s := newOAuthTestServer(t, f, g)
|
|
h := s.Handler()
|
|
|
|
publishTest(t, s.store, "octocat") // in cooldown: the owner's token skips it
|
|
// The form posts the same way whichever button is pressed, Enter in
|
|
// the token field included: a non-empty token wins over signing in.
|
|
rec := startSignIn(h, url.Values{
|
|
"user": {"OctoCat"}, "include_private": {"1"}, "token": {" " + testToken + " "},
|
|
}, "203.0.113.60")
|
|
if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/u/octocat" {
|
|
t.Fatalf("pasted token post = %d %q: %s", rec.Code, rec.Header().Get("Location"), rec.Body.String())
|
|
}
|
|
if c := rec.Result().Cookies(); len(c) != 0 {
|
|
t.Errorf("pasted token started a sign-in: cookies %v", c)
|
|
}
|
|
if st := waitIdle(t, s.queue, "octocat"); st.State != stateDone {
|
|
t.Fatalf("job = %+v", st)
|
|
}
|
|
call := f.lastCall()
|
|
if call.token != testToken || !call.cfg.Options.IncludePrivate || !call.cfg.Strict {
|
|
t.Errorf("fetch = %+v", call)
|
|
}
|
|
if m, err := s.store.Meta("octocat"); err != nil || m.Scope != "private" {
|
|
t.Errorf("meta = %+v, %v", m, err)
|
|
}
|
|
if got := g.revokedTokens(); len(got) != 0 {
|
|
t.Errorf("pasted token revoked: %q", got)
|
|
}
|
|
if g.exchangeCount() != 0 {
|
|
t.Error("pasted token talked to the OAuth token endpoint")
|
|
}
|
|
s.logins.mu.Lock()
|
|
pending := len(s.logins.entries)
|
|
s.logins.mu.Unlock()
|
|
if pending != 0 {
|
|
t.Errorf("%d sign-ins parked for a pasted token", pending)
|
|
}
|
|
s.queue.mu.Lock()
|
|
leftover := s.queue.jobs["octocat"].token
|
|
s.queue.mu.Unlock()
|
|
if leftover != "" {
|
|
t.Error("pasted token kept in memory after the job ended")
|
|
}
|
|
assertTokenNowhere(t, s, logs, testToken)
|
|
}
|
|
|
|
func TestPastedTokenForeignAccount(t *testing.T) {
|
|
logs := captureLog(t)
|
|
const publicToken = "ghp_publicONLYvalue0123456789abcdef"
|
|
g := newFakeGitHub(t)
|
|
f := &fakeFetcher{tokens: map[string]github.TokenInfo{
|
|
testToken: {Login: "alice", CanReadPrivate: true},
|
|
publicToken: {Login: "alice"},
|
|
}}
|
|
s := newOAuthTestServer(t, f, g)
|
|
h := s.Handler()
|
|
post := func(token, ip string) {
|
|
t.Helper()
|
|
rec := startSignIn(h, url.Values{
|
|
"user": {"xavier"}, "include_private": {"1"}, "include_org_repos": {"1"}, "token": {token},
|
|
}, ip)
|
|
if rec.Code != http.StatusSeeOther {
|
|
t.Fatalf("post = %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
// A token that can read private repos is only good for its owner.
|
|
post(testToken, "203.0.113.70")
|
|
if st := waitIdle(t, s.queue, "xavier"); st.State != stateFailed || !strings.Contains(st.Error, "your token belongs to alice") {
|
|
t.Fatalf("private-capable foreign token = %+v", st)
|
|
}
|
|
if f.callCount() != 0 {
|
|
t.Fatal("fetched another user with a private-capable token")
|
|
}
|
|
|
|
// A public-only token for another account renders public data...
|
|
post(publicToken, "203.0.113.71")
|
|
if st := waitIdle(t, s.queue, "xavier"); st.State != stateDone {
|
|
t.Fatalf("public foreign token = %+v", st)
|
|
}
|
|
if o := f.lastCall().cfg.Options; o.IncludePrivate || o.IncludeOrgRepos {
|
|
t.Errorf("foreign token kept private scope: %+v", o)
|
|
}
|
|
if m, err := s.store.Meta("xavier"); err != nil || m.Scope != "public" || m.Options.IncludePrivate {
|
|
t.Errorf("meta = %+v, %v", m, err)
|
|
}
|
|
|
|
// ...under the cooldown.
|
|
post(publicToken, "203.0.113.72")
|
|
if st := waitIdle(t, s.queue, "xavier"); st.State != stateFailed || st.Error != errFreshPasted.Error() {
|
|
t.Fatalf("foreign token during cooldown = %+v", st)
|
|
}
|
|
if n := f.callCount(); n != 1 {
|
|
t.Errorf("fetched %d times, want 1", n)
|
|
}
|
|
if got := g.revokedTokens(); len(got) != 0 {
|
|
t.Errorf("pasted tokens revoked: %q", got)
|
|
}
|
|
assertTokenNowhere(t, s, logs, testToken)
|
|
assertTokenNowhere(t, s, logs, publicToken)
|
|
}
|
|
|
|
func TestPastedTokenNeverEchoed(t *testing.T) {
|
|
s := newTestServer(t, &fakeFetcher{})
|
|
h := s.Handler()
|
|
|
|
for name, v := range map[string]url.Values{
|
|
"invalid user": {"user": {"--bad"}, "token": {testToken}},
|
|
"invalid tz": {"user": {"octocat"}, "tz": {"Mars/Olympus"}, "token": {testToken}},
|
|
"invalid token": {"user": {"octocat"}, "token": {testToken + "\r\nX-Evil: 1"}},
|
|
} {
|
|
rec := startSignIn(h, v, "203.0.113.80")
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Errorf("%s = %d, want 400", name, rec.Code)
|
|
}
|
|
if body := rec.Body.String(); strings.Contains(body, testToken) {
|
|
t.Errorf("%s: token echoed back into the page", name)
|
|
}
|
|
}
|
|
|
|
// The same rate limit covers pasted tokens.
|
|
var last int
|
|
for range submitBurst + 1 {
|
|
last = startSignIn(h, url.Values{"user": {"--bad"}, "token": {testToken}}, "203.0.113.81").Code
|
|
}
|
|
if last != http.StatusTooManyRequests {
|
|
t.Errorf("pasted-token post past burst = %d, want 429", last)
|
|
}
|
|
}
|
|
|
|
func TestPastedTokenNotRevokedOnShutdown(t *testing.T) {
|
|
g := newFakeGitHub(t)
|
|
f := &fakeFetcher{gate: make(chan struct{})}
|
|
s := newOAuthTestServer(t, f, g)
|
|
for _, login := range []string{"a1", "a2", "a3"} {
|
|
if _, err := s.queue.Submit(submission{Login: login, Token: testToken, Pasted: true}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
s.queue.Stop() // two running, one still queued
|
|
if got := g.revokedTokens(); len(got) != 0 {
|
|
t.Errorf("revoked %d pasted tokens on shutdown, want 0", len(got))
|
|
}
|
|
}
|