Spend a hashcash stamp only when the request succeeds (closes #158)
check / check (push) Waiting to run
check / check (push) Waiting to run
A session request refused with 400 or 409, or a +H channel message refused for membership, a ban or +m, used to spend its stamp, so a retry with the same stamp was refused as already used. hashcash.Validator.Validate is split into Check, which spends nothing, and Claim, which records the stamp; Release undoes a claim. Session creation checks the stamp first, claims it just before CreateSession and releases it if CreateSession fails. SendChannelMessage checks the stamp after the membership, ban and +m checks. Model: opus-5-5
This commit was merged in pull request #171.
This commit is contained in:
@@ -29,6 +29,7 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"modernc.org/sqlite"
|
||||
"sneak.berlin/go/neoirc/internal/broker"
|
||||
api "sneak.berlin/go/neoirc/internal/cli/api"
|
||||
"sneak.berlin/go/neoirc/internal/config"
|
||||
"sneak.berlin/go/neoirc/internal/db"
|
||||
"sneak.berlin/go/neoirc/internal/globals"
|
||||
@@ -4073,6 +4074,121 @@ func TestChannelHashcashMissingBitsArg(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// postSessionWithStamp posts a session request for nick and
|
||||
// username carrying stamp and returns the response status.
|
||||
// An empty username defaults to the nick.
|
||||
func postSessionWithStamp(
|
||||
t *testing.T,
|
||||
tserver *testServer,
|
||||
nick, username, stamp string,
|
||||
) int {
|
||||
t.Helper()
|
||||
|
||||
body, err := json.Marshal(map[string]string{
|
||||
nickKey: nick, "username": username, "pow_token": stamp,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
|
||||
resp, err := doRequest(
|
||||
t, http.MethodPost, tserver.url(apiSession),
|
||||
bytes.NewReader(body),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_ = resp.Body.Close()
|
||||
|
||||
return resp.StatusCode
|
||||
}
|
||||
|
||||
// sessionCount returns the number of sessions in tserver's
|
||||
// database.
|
||||
func sessionCount(t *testing.T, tserver *testServer) int64 {
|
||||
t.Helper()
|
||||
|
||||
count, err := tserver.database.GetUserCount(t.Context())
|
||||
if err != nil {
|
||||
t.Fatalf("count sessions: %v", err)
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
// TestRefusedSessionLeavesStampUnspent checks that a
|
||||
// session request is refused with 402 for an invalid stamp
|
||||
// before its nick is looked at, that a request refused with
|
||||
// 400 or 409 does not spend its stamp, so the same stamp
|
||||
// then creates a session, that the stamp stays spent after
|
||||
// that, that a spent stamp with an invalid nick is refused
|
||||
// for its nick, and that no refused request creates a
|
||||
// session.
|
||||
func TestRefusedSessionLeavesStampUnspent(t *testing.T) {
|
||||
const (
|
||||
bits = 2
|
||||
badNick = "bad nick"
|
||||
)
|
||||
|
||||
tserver := newTestServerWith(t, bits, "", "")
|
||||
|
||||
// A server with no SERVER_NAME takes stamps for "neoirc".
|
||||
status := postSessionWithStamp(
|
||||
t, tserver, "taken", "", api.MintHashcash(bits, "neoirc"),
|
||||
)
|
||||
if status != http.StatusCreated {
|
||||
t.Fatalf("create taken: want 201, got %d", status)
|
||||
}
|
||||
|
||||
stamp := api.MintHashcash(bits, "neoirc")
|
||||
otherServerStamp := api.MintHashcash(bits, "elsewhere")
|
||||
|
||||
steps := []struct {
|
||||
nick, username, stamp string
|
||||
want int
|
||||
}{
|
||||
{badNick, "", otherServerStamp, http.StatusPaymentRequired},
|
||||
{badNick, "", stamp, http.StatusBadRequest},
|
||||
{"fresh", "bad username", stamp, http.StatusBadRequest},
|
||||
{"taken", "", stamp, http.StatusConflict},
|
||||
{"fresh", "", stamp, http.StatusCreated},
|
||||
{"another", "", stamp, http.StatusPaymentRequired},
|
||||
// Refused as already spent, the stamp is not given back.
|
||||
{"later", "", stamp, http.StatusPaymentRequired},
|
||||
// Whether the stamp is spent is looked at after the nick.
|
||||
{badNick, "", stamp, http.StatusBadRequest},
|
||||
// The refused request for "another" left the nick free.
|
||||
{"another", "", api.MintHashcash(bits, "neoirc"), http.StatusCreated},
|
||||
}
|
||||
|
||||
sessions := sessionCount(t, tserver)
|
||||
|
||||
for _, step := range steps {
|
||||
status = postSessionWithStamp(
|
||||
t, tserver, step.nick, step.username, step.stamp,
|
||||
)
|
||||
if status != step.want {
|
||||
t.Fatalf(
|
||||
"nick %q, username %q: want %d, got %d",
|
||||
step.nick, step.username, step.want, status,
|
||||
)
|
||||
}
|
||||
|
||||
if status == http.StatusCreated {
|
||||
sessions++
|
||||
}
|
||||
|
||||
count := sessionCount(t, tserver)
|
||||
if count != sessions {
|
||||
t.Fatalf(
|
||||
"nick %q, username %q: want %d sessions, got %d",
|
||||
step.nick, step.username, sessions, count,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamesShowsHostmask(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user