Korzen
Bot Protection

Go net/http

Bot Protection in un server Go con il solo net/http: un client per processo, tutta la chiamata sotto un unico timeout, e l'IP del visitatore.

Questa guida usa solo la standard library: net/http e un unico http.Client per il processo, che tiene aperte le connessioni. Il suo Timeout limita l'intera chiamata, dalla connessione alla lettura del body.

Il tag

Metti il tag nel <head> del layout che renderizza le pagine di registrazione, login e checkout:

<script src="https://korzen.dev/v1/tag.js" data-site-key="YOUR_SITE_KEY" async></script>

Nella pagina, prima di inviare il form, chiama await window.korzen?.ready(): riporta ciò che il visitatore ha fatto dall'ultimo rapporto del tag e si risolve entro pochi secondi qualunque cosa accada. Il tag

La chiamata

Il client è un solo file con la sola libreria standard, korzen.go: chiede a /v1/decide, rinuncia allo scadere del tuo timeout e lascia passare il visitatore a ogni errore. Scaricalo in un package del tuo modulo e leggilo prima del commit:

mkdir -p internal/korzen
curl -o internal/korzen/korzen.go https://korzen.dev/bot-protection/sdk/korzen.go

Un solo Client per il processo: tiene aperte le connessioni, e il suo Timeout limita l'intera chiamata. Protect chiede prima dell'handler che crea l'account e mette la risposta nel context della richiesta:

var bot = &korzen.Client{APIKey: os.Getenv("KORZEN_API_KEY"), URL: "https://korzen.dev", Timeout: 200 * time.Millisecond}

mux.Handle("POST /signup", bot.Protect(http.HandlerFunc(signup), korzen.Options{
	IP: korzen.RemoteIP, // see "The visitor's IP"
	OnBlock: func(w http.ResponseWriter, r *http.Request, d korzen.Decision) bool {
		http.Error(w, "We could not create your account.", http.StatusBadRequest)
		return true
	},
	// Your own check for challenge runs in signup: return false to go on to it. Without
	// OnChallenge, a challenged post is refused (403).
	OnChallenge: func(http.ResponseWriter, *http.Request, korzen.Decision) bool { return false },
}))

func signup(w http.ResponseWriter, r *http.Request) {
	d, _ := korzen.FromContext(r.Context()) // no answer: the zero Decision
	if d.Action == korzen.Challenge {
		// your CAPTCHA, or an email or phone check, before going on
	}
	user, err := createAccount(r) // your own code
	// ...
	saveRequestID(user.ID, d.RequestID) // see "Keep the request_id"
}

Per chiedere dentro un handler: d, err := bot.Decide(r.Context(), korzen.FromHTTP(r, korzen.RemoteIP(r))). A ogni errore d.Action è allow, e err dice perché, per il tuo log. Una risposta il cui action non è allow, challenge o block conta anch'essa come errore; il *korzen.Problem di una risposta di errore è in err (errors.As). Per registrare ogni chiamata, imposta OnAnswer del client: riceve lo status, Korzen-Request-Id e gli header RateLimit.

L'IP del visitatore

r.RemoteAddr è l'indirizzo della connessione: quello del tuo proxy, se ne hai uno. korzen.RemoteIP lo legge: usalo quando davanti al tuo server non c'è nulla. korzen.CloudflareIP legge CF-Connecting-IP, che è giusto solo se il tuo server riceve traffico soltanto da Cloudflare; altrimenti un visitatore potrebbe scriverlo. Dietro il tuo load balancer, korzen.LastForwardedIP prende l'ultima voce di X-Forwarded-For, quella che ha aggiunto lui.

Applica l'azione

Leggi solo action. Su block, rispondi con lo stesso errore che l'azione mostra quando fallisce per qualsiasi altro motivo, così il bot non impara niente. Su challenge, usa la verifica che hai già (un CAPTCHA, un codice via email o telefono); se non ne hai, prosegui. L'account parte in monitor mode, dove action è sempre allow: per i visitatori non cambia niente finché non attivi il block mode. Oppure manda il visitatore alla nostra pagina di challenge, in challenge.url: vedi la pagina di challenge.

Senza OnChallenge, il middleware non lascia passare nessuna challenge: il caricamento di una pagina (GET) va alla nostra pagina di challenge, e l'invio di un modulo o una chiamata API viene rifiutato con 403. Proteggi anche la pagina che mostra il modulo: lì una persona incontra la challenge, e il suo pass lascia passare l'invio. Con una verifica tua, passa OnChallenge ed eseguila lì. Il middleware rimanda la persona all'indirizzo che ha chiesto, letto da X-Forwarded-Host e X-Forwarded-Proto quando il tuo proxy li imposta; dietro un proxy che inoltra Host: localhost e nessuno dei due (nginx di default), dai PublicURL: "https://shop.example".

Conserva il request_id

Ogni risposta ha un request_id. Conservalo insieme a ciò che l'azione ha creato, in una colonna dell'account o dell'ordine, o in una riga di log con il loro id. Identifica quella decisione quando ci chiedi informazioni, e ti permette di scoprire in seguito quale risposta ha fatto entrare un account falso. Quando l'account si rivela buono o cattivo, comunicalo: bot.Feedback(ctx, []korzen.Feedback{{RequestID: id, Outcome: "chargeback"}}, korzen.Batch{}) (vedi feedback).

Provalo sulla tua macchina

Avvia go run . e apri il tuo server su localhost. Le pagine su localhost accettano sempre il tag e non compaiono mai tra i domini. In locale l'IP è 127.0.0.1 o ::1, e va bene così. In Docker sulla tua macchina, l'app vede invece l'indirizzo del bridge, come 172.17.0.1: la pagina di challenge ti riporta comunque su localhost, perché il tag ha visto lì la tua pagina.

  1. Imposta KORZEN_API_KEY nell'ambiente e apri la pagina di registrazione. Negli strumenti per sviluppatori del browser, il cookie _kz compare dopo qualche secondo.
  2. Registrati. La decisione compare nella pagina Decisioni della dashboard, sotto il path che hai inviato.
  3. Verifica il fail-open: imposta una chiave sbagliata e registrati di nuovo. La chiamata risponde 401, e la registrazione va a buon fine.

Errori comuni

  • Un HTTPClient con un nuovo http.Transport a ogni chiamata, o quello predefinito della libreria standard, che tiene aperte due connessioni. Ognuna nuova è una nuova connessione TLS: lascia vuoto HTTPClient, o dai al tuo korzen.NewTransport(), una volta per il processo.
  • Leggere CF-Connecting-IP su un server che non sta dietro Cloudflare. Un bot scrive l'header e si sceglie il proprio IP.
  • Chiamare /v1/decide dal browser. L'API key arriverebbe a ogni visitatore; la chiamata va fatta dal tuo server.
  • Nessun timeout, o un errore che blocca la registrazione. Qualsiasi risposta diversa da 200, o nessuna risposta in tempo, significa: lascia passare il visitatore.
  • Una riga nel tuo log che inizia con korzen: the visitor's IP sent to Bot Protection. Il client la scrive una volta, alla prima decisione il cui IP è della tua rete (di un load balancer, di un proxy, di un container), che Bot Protection giudica come senza IP: imposta Options.IP sull'indirizzo del visitatore (l'IP del visitatore). In Docker sulla tua macchina è l'indirizzo del bridge, e lì non fa danni.