Files
dchain/node/api_common.go
vsecoder 49ad09efe7 fix(node): proper CORS middleware + preflight handling
The desktop Electron renderer runs at http://127.0.0.1:5173 (dev) or
file:// (prod); the node HTTP API is at a different origin by design.
Browsers enforce CORS, and our per-handler `Access-Control-Allow-Origin: *`
header only covered the happy path — preflight OPTIONS requests, which
browsers send before any POST with a JSON body or Authorization header,
fell through to the 404 handler without CORS headers and the subsequent
real request was blocked.

Added node/cors.go — a single middleware that:
  * Sets Access-Control-Allow-Origin / -Methods / -Headers /
    -Expose-Headers / -Max-Age on every response.
  * Short-circuits OPTIONS with 204, never invoking the mux.

Wired into stats.go:ListenAndServe so the wrapping is unconditional
(the node's security model gates writes by token + Ed25519 signature,
not by origin, so wide CORS is the correct default).

Cleaned up the now-redundant per-jsonOK/jsonErr Allow-Origin setters in
api_common.go — the middleware sets a single consistent header instead
of two collisions from handlers that both write one.

Symptom before: `net::ERR_FAILED` / "CORS policy blocked" errors in
the Electron devtools console when hitting /api/* or /relay/*.
Symptom after: clean GET/POST, preflight answers in ~1ms.
2026-04-22 17:22:39 +03:00

194 lines
4.4 KiB
Go

package node
import (
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"go-blockchain/blockchain"
"go-blockchain/identity"
)
func jsonOK(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(v)
}
func jsonErr(w http.ResponseWriter, err error, code int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
}
func queryInt(r *http.Request, key string, def int) int {
s := r.URL.Query().Get(key)
if s == "" {
return def
}
n, err := strconv.Atoi(s)
if err != nil || n <= 0 {
return def
}
return n
}
// queryInt64 reads a non-negative int64 query param — typically a unix
// timestamp cursor for pagination. Returns def when missing or invalid.
func queryInt64(r *http.Request, key string, def int64) int64 {
s := r.URL.Query().Get(key)
if s == "" {
return def
}
n, err := strconv.ParseInt(s, 10, 64)
if err != nil || n < 0 {
return def
}
return n
}
// queryIntMin0 parses a query param as a non-negative integer; returns 0 if absent or invalid.
func queryIntMin0(r *http.Request, key string) int {
s := r.URL.Query().Get(key)
if s == "" {
return 0
}
n, err := strconv.Atoi(s)
if err != nil || n < 0 {
return 0
}
return n
}
func queryUint64Optional(r *http.Request, key string) (*uint64, error) {
raw := strings.TrimSpace(r.URL.Query().Get(key))
if raw == "" {
return nil, nil
}
n, err := strconv.ParseUint(raw, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid %s: %s", key, raw)
}
return &n, nil
}
func resolveAccountID(q ExplorerQuery, accountID string) (string, error) {
if accountID == "" {
return "", fmt.Errorf("account id required")
}
if strings.HasPrefix(accountID, "DC") {
pubKey, err := q.AddressToPubKey(accountID)
if err != nil {
return "", err
}
if pubKey == "" {
return "", fmt.Errorf("account not found")
}
return pubKey, nil
}
return accountID, nil
}
func verifyTransactionSignature(tx *blockchain.Transaction) error {
if tx == nil {
return fmt.Errorf("transaction is nil")
}
return identity.VerifyTx(tx)
}
func decodeTransactionEnvelope(raw string) (*blockchain.Transaction, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil, fmt.Errorf("empty transaction envelope")
}
tryDecodeJSON := func(data []byte) (*blockchain.Transaction, error) {
var tx blockchain.Transaction
if err := json.Unmarshal(data, &tx); err != nil {
return nil, err
}
if tx.ID == "" || tx.From == "" || tx.Type == "" {
return nil, fmt.Errorf("invalid tx payload")
}
return &tx, nil
}
if strings.HasPrefix(raw, "{") {
return tryDecodeJSON([]byte(raw))
}
base64Decoders := []*base64.Encoding{
base64.StdEncoding,
base64.RawStdEncoding,
base64.URLEncoding,
base64.RawURLEncoding,
}
for _, enc := range base64Decoders {
if b, err := enc.DecodeString(raw); err == nil {
if tx, txErr := tryDecodeJSON(b); txErr == nil {
return tx, nil
}
}
}
if b, err := hex.DecodeString(raw); err == nil {
if tx, txErr := tryDecodeJSON(b); txErr == nil {
return tx, nil
}
}
return nil, fmt.Errorf("failed to decode transaction envelope")
}
func txMemo(tx *blockchain.Transaction) string {
if tx == nil {
return ""
}
if memo := strings.TrimSpace(tx.Memo); memo != "" {
return memo
}
switch tx.Type {
case blockchain.EventTransfer:
var p blockchain.TransferPayload
if err := json.Unmarshal(tx.Payload, &p); err == nil {
return strings.TrimSpace(p.Memo)
}
case blockchain.EventBlockReward:
return blockRewardReason(tx.Payload)
case blockchain.EventRelayProof:
return "Relay delivery fee"
case blockchain.EventHeartbeat:
return "Liveness heartbeat"
case blockchain.EventRegisterRelay:
return "Register relay service"
case blockchain.EventBindWallet:
return "Bind payout wallet"
}
return ""
}
func blockRewardReason(payload []byte) string {
var p blockchain.BlockRewardPayload
if err := json.Unmarshal(payload, &p); err != nil {
return "Block fees"
}
if p.FeeReward == 0 && p.TotalReward > 0 {
return "Genesis allocation"
}
return "Block fees collected"
}
func decodeTxPayload(payload []byte) (any, string) {
if len(payload) == 0 {
return nil, ""
}
var decoded any
if err := json.Unmarshal(payload, &decoded); err == nil {
return decoded, ""
}
return nil, hex.EncodeToString(payload)
}