Forex/camp review hardening: bound CoinGecko body, escape URL params, fail-open camp on DB error

- forex_crypto.go: cap the CoinGecko response with io.LimitReader(64KiB);
  build the simple/price query via url.Values instead of bare Sprintf.
- dnd_expedition_camp.go: a combat-session lookup error now fails open
  (allow camp) + logs a warning, instead of blocking standard camp with a
  misleading empty 'not cleared' rejection.
This commit is contained in:
prosolis
2026-05-22 07:25:08 -07:00
parent 56f896b941
commit 95e0995c7f
2 changed files with 16 additions and 4 deletions

View File

@@ -2,6 +2,7 @@ package plugin
import ( import (
"fmt" "fmt"
"log/slog"
"math/rand/v2" "math/rand/v2"
"strings" "strings"
"time" "time"
@@ -369,7 +370,13 @@ func campLocationCheck(exp *Expedition) (cleared bool, problem string) {
encID := encounterIDForRoom(run.CurrentRoom) encID := encounterIDForRoom(run.CurrentRoom)
sess, err := getCombatSessionForEncounter(run.RunID, encID) sess, err := getCombatSessionForEncounter(run.RunID, encID)
if err != nil { if err != nil {
return false, "" // Can't read the encounter's combat state. Fail open like the
// run-lookup path above rather than blocking standard camp with an
// empty (misleading "not cleared") rejection — a DB hiccup shouldn't
// strand a player who only wants to rest.
slog.Warn("camp: combat session lookup failed; allowing camp",
"run", run.RunID, "encounter", encID, "err", err)
return true, ""
} }
if sess != nil && sess.Status == CombatStatusActive { if sess != nil && sess.Status == CombatStatusActive {
return false, "You can't camp mid-fight — finish the encounter first." return false, "You can't camp mid-fight — finish the encounter first."

View File

@@ -4,7 +4,9 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net/http" "net/http"
"net/url"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -82,8 +84,11 @@ func (p *ForexPlugin) cgSpotUSD(sym string) (float64, error) {
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
defer cancel() defer cancel()
url := fmt.Sprintf("%s/simple/price?ids=%s&vs_currencies=usd", coinGeckoBaseURL, info.CoinGeckoID) q := url.Values{}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil) q.Set("ids", info.CoinGeckoID)
q.Set("vs_currencies", "usd")
reqURL := coinGeckoBaseURL + "/simple/price?" + q.Encode()
req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -99,7 +104,7 @@ func (p *ForexPlugin) cgSpotUSD(sym string) (float64, error) {
} }
var data map[string]map[string]float64 var data map[string]map[string]float64
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { if err := json.NewDecoder(io.LimitReader(resp.Body, 64<<10)).Decode(&data); err != nil {
return 0, fmt.Errorf("coingecko decode error: %w", err) return 0, fmt.Errorf("coingecko decode error: %w", err)
} }
entry, ok := data[info.CoinGeckoID] entry, ok := data[info.CoinGeckoID]