- internal/{address,identity,protocol,tce,transport,verify} -> pkg/ so
external Go projects can import the verified core; invariant tests
updated for the new paths
- Config.TrustProxy: key rate limiting by X-Forwarded-For when the relay
sits behind a reverse proxy (off by default, header never trusted
otherwise)
- examples/service + examples/approve: complete passwordless login round
trip (mint request -> wallet approves -> local verify), run live in CI
- docs/SERVICE-GUIDE.md: the integration recipe
91 lines
2.1 KiB
Go
91 lines
2.1 KiB
Go
package server
|
|
|
|
import (
|
|
"net"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// ipLimiter is a fixed-window per-client rate limiter keyed by remote IP. It is
|
|
// deliberately minimal: no bursting, no warm-up, just a hard cap per window to
|
|
// keep an open relay from being flooded.
|
|
type ipLimiter struct {
|
|
mu sync.Mutex
|
|
limit int
|
|
window time.Duration
|
|
hits map[string]int
|
|
reset map[string]time.Time
|
|
}
|
|
|
|
func newIPLimiter(limit int, window time.Duration) *ipLimiter {
|
|
return &ipLimiter{
|
|
limit: limit,
|
|
window: window,
|
|
hits: make(map[string]int),
|
|
reset: make(map[string]time.Time),
|
|
}
|
|
}
|
|
|
|
// allow reports whether the client may proceed, incrementing its windowed count.
|
|
func (l *ipLimiter) allow(ip string) bool {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
now := time.Now()
|
|
until, ok := l.reset[ip]
|
|
if !ok || now.After(until) {
|
|
l.hits[ip] = 0
|
|
l.reset[ip] = now.Add(l.window)
|
|
} else if now.Sub(until) > l.window*2 {
|
|
// Lazy cleanup of long-idle entries.
|
|
delete(l.hits, ip)
|
|
delete(l.reset, ip)
|
|
l.hits[ip] = 0
|
|
l.reset[ip] = now.Add(l.window)
|
|
}
|
|
if l.hits[ip] >= l.limit {
|
|
return false
|
|
}
|
|
l.hits[ip]++
|
|
return true
|
|
}
|
|
|
|
// clientIP returns the request's remote IP, stripping a port if present.
|
|
// With trustProxy set (Config.TrustProxy) the left-most X-Forwarded-For
|
|
// entry wins: the relay sits behind a reverse proxy and every socket would
|
|
// otherwise share the proxy's address. The header is attacker-controlled,
|
|
// which is exactly why trusting it is an explicit operator choice.
|
|
func clientIP(r *http.Request, trustProxy bool) string {
|
|
if trustProxy {
|
|
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
|
if i := indexByte(xff, ','); i >= 0 {
|
|
xff = xff[:i]
|
|
}
|
|
return trimSpace(xff)
|
|
}
|
|
}
|
|
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
if err != nil {
|
|
return r.RemoteAddr
|
|
}
|
|
return host
|
|
}
|
|
|
|
func indexByte(s string, b byte) int {
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i] == b {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func trimSpace(s string) string {
|
|
for len(s) > 0 && (s[0] == ' ' || s[0] == '\t') {
|
|
s = s[1:]
|
|
}
|
|
for len(s) > 0 && (s[len(s)-1] == ' ' || s[len(s)-1] == '\t') {
|
|
s = s[:len(s)-1]
|
|
}
|
|
return s
|
|
}
|