refactor: move hashcash stamp from X-Hashcash header to JSON request body
All checks were successful
check / check (push) Successful in 4s
All checks were successful
check / check (push) Successful in 4s
Move the hashcash proof-of-work stamp from the X-Hashcash HTTP header into the JSON request body as a 'hashcash' field on POST /api/v1/session. Updated server handler, CLI client, SPA client, and documentation.
This commit is contained in:
28
README.md
28
README.md
@@ -989,23 +989,18 @@ Create a new user session. This is the entry point for all clients.
|
|||||||
|
|
||||||
If the server requires hashcash proof-of-work (see
|
If the server requires hashcash proof-of-work (see
|
||||||
[Hashcash Proof-of-Work](#hashcash-proof-of-work)), the client must include a
|
[Hashcash Proof-of-Work](#hashcash-proof-of-work)), the client must include a
|
||||||
valid stamp in the `X-Hashcash` request header. The required difficulty is
|
valid stamp in the `hashcash` field of the JSON request body. The required
|
||||||
advertised via `GET /api/v1/server` in the `hashcash_bits` field.
|
difficulty is advertised via `GET /api/v1/server` in the `hashcash_bits` field.
|
||||||
|
|
||||||
**Request Headers:**
|
|
||||||
|
|
||||||
| Header | Required | Description |
|
|
||||||
|--------------|----------|-------------|
|
|
||||||
| `X-Hashcash` | Conditional | Hashcash stamp (required when server has `hashcash_bits` > 0) |
|
|
||||||
|
|
||||||
**Request Body:**
|
**Request Body:**
|
||||||
```json
|
```json
|
||||||
{"nick": "alice"}
|
{"nick": "alice", "hashcash": "1:20:260310:neoirc::3a2f1"}
|
||||||
```
|
```
|
||||||
|
|
||||||
| Field | Type | Required | Constraints |
|
| Field | Type | Required | Constraints |
|
||||||
|--------|--------|----------|-------------|
|
|------------|--------|-------------|-------------|
|
||||||
| `nick` | string | Yes | 1–32 characters, must be unique on the server |
|
| `nick` | string | Yes | 1–32 characters, must be unique on the server |
|
||||||
|
| `hashcash` | string | Conditional | Hashcash stamp (required when server has `hashcash_bits` > 0) |
|
||||||
|
|
||||||
**Response:** `201 Created`
|
**Response:** `201 Created`
|
||||||
```json
|
```json
|
||||||
@@ -1027,7 +1022,7 @@ advertised via `GET /api/v1/server` in the `hashcash_bits` field.
|
|||||||
| Status | Error | When |
|
| Status | Error | When |
|
||||||
|--------|-------|------|
|
|--------|-------|------|
|
||||||
| 400 | `nick must be 1-32 characters` | Empty or too-long nick |
|
| 400 | `nick must be 1-32 characters` | Empty or too-long nick |
|
||||||
| 402 | `hashcash proof-of-work required` | Missing `X-Hashcash` header when hashcash is enabled |
|
| 402 | `hashcash proof-of-work required` | Missing `hashcash` field in request body when hashcash is enabled |
|
||||||
| 402 | `invalid hashcash stamp: ...` | Stamp fails validation (wrong bits, expired, reused, etc.) |
|
| 402 | `invalid hashcash stamp: ...` | Stamp fails validation (wrong bits, expired, reused, etc.) |
|
||||||
| 409 | `nick already taken` | Another active session holds this nick |
|
| 409 | `nick already taken` | Another active session holds this nick |
|
||||||
|
|
||||||
@@ -1035,8 +1030,7 @@ advertised via `GET /api/v1/server` in the `hashcash_bits` field.
|
|||||||
```bash
|
```bash
|
||||||
TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/session \
|
TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/session \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-H 'X-Hashcash: 1:20:260310:neoirc::3a2f1' \
|
-d '{"nick":"alice","hashcash":"1:20:260310:neoirc::3a2f1"}' | jq -r .token)
|
||||||
-d '{"nick":"alice"}' | jq -r .token)
|
|
||||||
echo $TOKEN
|
echo $TOKEN
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -2138,7 +2132,7 @@ account registration, no IP-based rate limits that punish shared networks.
|
|||||||
2. Client computes a hashcash stamp: find a counter value such that the
|
2. Client computes a hashcash stamp: find a counter value such that the
|
||||||
SHA-256 hash of the stamp string has the required number of leading zero
|
SHA-256 hash of the stamp string has the required number of leading zero
|
||||||
bits.
|
bits.
|
||||||
3. Client includes the stamp in the `X-Hashcash` request header when creating
|
3. Client includes the stamp in the `hashcash` field of the JSON request body when creating
|
||||||
a session: `POST /api/v1/session`.
|
a session: `POST /api/v1/session`.
|
||||||
4. Server validates the stamp:
|
4. Server validates the stamp:
|
||||||
- Version is `1`
|
- Version is `1`
|
||||||
@@ -2195,7 +2189,7 @@ Both the embedded web SPA and the CLI client automatically handle hashcash:
|
|||||||
|
|
||||||
1. Fetch `GET /api/v1/server` to read `hashcash_bits`
|
1. Fetch `GET /api/v1/server` to read `hashcash_bits`
|
||||||
2. If `hashcash_bits > 0`, compute a valid stamp
|
2. If `hashcash_bits > 0`, compute a valid stamp
|
||||||
3. Include the stamp in the `X-Hashcash` header on `POST /api/v1/session`
|
3. Include the stamp in the `hashcash` field of the JSON body on `POST /api/v1/session`
|
||||||
|
|
||||||
The web SPA uses the Web Crypto API (`crypto.subtle.digest`) for SHA-256
|
The web SPA uses the Web Crypto API (`crypto.subtle.digest`) for SHA-256
|
||||||
computation with batched parallelism. The CLI client uses Go's `crypto/sha256`.
|
computation with batched parallelism. The CLI client uses Go's `crypto/sha256`.
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ func (client *Client) CreateSession(
|
|||||||
// Fetch server info to check for hashcash requirement.
|
// Fetch server info to check for hashcash requirement.
|
||||||
info, err := client.GetServerInfo()
|
info, err := client.GetServerInfo()
|
||||||
|
|
||||||
var headers map[string]string
|
var hashcashStamp string
|
||||||
|
|
||||||
if err == nil && info.HashcashBits > 0 {
|
if err == nil && info.HashcashBits > 0 {
|
||||||
resource := info.Name
|
resource := info.Name
|
||||||
@@ -60,17 +60,13 @@ func (client *Client) CreateSession(
|
|||||||
resource = "neoirc"
|
resource = "neoirc"
|
||||||
}
|
}
|
||||||
|
|
||||||
stamp := MintHashcash(info.HashcashBits, resource)
|
hashcashStamp = MintHashcash(info.HashcashBits, resource)
|
||||||
headers = map[string]string{
|
|
||||||
"X-Hashcash": stamp,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := client.doWithHeaders(
|
data, err := client.do(
|
||||||
http.MethodPost,
|
http.MethodPost,
|
||||||
"/api/v1/session",
|
"/api/v1/session",
|
||||||
&SessionRequest{Nick: nick},
|
&SessionRequest{Nick: nick, Hashcash: hashcashStamp},
|
||||||
headers,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import "time"
|
|||||||
|
|
||||||
// SessionRequest is the body for POST /api/v1/session.
|
// SessionRequest is the body for POST /api/v1/session.
|
||||||
type SessionRequest struct {
|
type SessionRequest struct {
|
||||||
Nick string `json:"nick"`
|
Nick string `json:"nick"`
|
||||||
|
Hashcash string `json:"hashcash,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SessionResponse is the response from session creation.
|
// SessionResponse is the response from session creation.
|
||||||
|
|||||||
@@ -144,35 +144,9 @@ func (hdlr *Handlers) handleCreateSession(
|
|||||||
writer http.ResponseWriter,
|
writer http.ResponseWriter,
|
||||||
request *http.Request,
|
request *http.Request,
|
||||||
) {
|
) {
|
||||||
// Validate hashcash proof-of-work if configured.
|
|
||||||
if hdlr.params.Config.HashcashBits > 0 {
|
|
||||||
stamp := request.Header.Get("X-Hashcash")
|
|
||||||
if stamp == "" {
|
|
||||||
hdlr.respondError(
|
|
||||||
writer, request,
|
|
||||||
"hashcash proof-of-work required",
|
|
||||||
http.StatusPaymentRequired,
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := hdlr.hashcashVal.Validate(
|
|
||||||
stamp, hdlr.params.Config.HashcashBits,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
hdlr.respondError(
|
|
||||||
writer, request,
|
|
||||||
"invalid hashcash stamp: "+err.Error(),
|
|
||||||
http.StatusPaymentRequired,
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type createRequest struct {
|
type createRequest struct {
|
||||||
Nick string `json:"nick"`
|
Nick string `json:"nick"`
|
||||||
|
Hashcash string `json:"hashcash,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var payload createRequest
|
var payload createRequest
|
||||||
@@ -188,6 +162,32 @@ func (hdlr *Handlers) handleCreateSession(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate hashcash proof-of-work if configured.
|
||||||
|
if hdlr.params.Config.HashcashBits > 0 {
|
||||||
|
if payload.Hashcash == "" {
|
||||||
|
hdlr.respondError(
|
||||||
|
writer, request,
|
||||||
|
"hashcash proof-of-work required",
|
||||||
|
http.StatusPaymentRequired,
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = hdlr.hashcashVal.Validate(
|
||||||
|
payload.Hashcash, hdlr.params.Config.HashcashBits,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
hdlr.respondError(
|
||||||
|
writer, request,
|
||||||
|
"invalid hashcash stamp: "+err.Error(),
|
||||||
|
http.StatusPaymentRequired,
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
payload.Nick = strings.TrimSpace(payload.Nick)
|
payload.Nick = strings.TrimSpace(payload.Nick)
|
||||||
|
|
||||||
if !validNickRe.MatchString(payload.Nick) {
|
if !validNickRe.MatchString(payload.Nick) {
|
||||||
|
|||||||
@@ -135,20 +135,22 @@ function LoginScreen({ onLogin }) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
const extraHeaders = {};
|
let hashcashStamp = "";
|
||||||
if (hashcashBitsRef.current > 0) {
|
if (hashcashBitsRef.current > 0) {
|
||||||
setError("Computing proof-of-work...");
|
setError("Computing proof-of-work...");
|
||||||
const stamp = await mintHashcash(
|
hashcashStamp = await mintHashcash(
|
||||||
hashcashBitsRef.current,
|
hashcashBitsRef.current,
|
||||||
hashcashResourceRef.current,
|
hashcashResourceRef.current,
|
||||||
);
|
);
|
||||||
extraHeaders["X-Hashcash"] = stamp;
|
|
||||||
setError("");
|
setError("");
|
||||||
}
|
}
|
||||||
|
const reqBody = { nick: nick.trim() };
|
||||||
|
if (hashcashStamp) {
|
||||||
|
reqBody.hashcash = hashcashStamp;
|
||||||
|
}
|
||||||
const res = await api("/session", {
|
const res = await api("/session", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ nick: nick.trim() }),
|
body: JSON.stringify(reqBody),
|
||||||
headers: extraHeaders,
|
|
||||||
});
|
});
|
||||||
localStorage.setItem("neoirc_token", res.token);
|
localStorage.setItem("neoirc_token", res.token);
|
||||||
onLogin(res.nick);
|
onLogin(res.nick);
|
||||||
|
|||||||
Reference in New Issue
Block a user