Korzen
Bot Protection

Go net/http

Bot Protection dans un serveur Go avec net/http seul : un client pour le processus, tout l'appel sous un timeout, et l'IP du visiteur.

Ce guide n'utilise que la bibliothèque standard : net/http et un http.Client pour le processus, qui garde ses connexions ouvertes. Son Timeout borne tout l'appel, de la connexion à la lecture du corps.

Le tag

Placez le tag dans le <head> du layout qui rend vos pages d'inscription, de connexion et de checkout :

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

Dans la page, avant d'envoyer le formulaire, appelez await window.korzen?.ready() : il signale ce que le visiteur a fait depuis le dernier signalement du tag, et se résout en quelques secondes quoi qu'il arrive. Le tag

L'appel

Le client tient en un seul fichier, avec la seule bibliothèque standard, korzen.go : il interroge /v1/decide, abandonne à votre timeout et laisse passer le visiteur à la moindre erreur. Téléchargez-le dans un package de votre module, et lisez-le avant de le committer :

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

Un seul Client pour le processus : il garde ses connexions ouvertes, et son Timeout borne l'appel entier. Protect interroge avant le handler qui crée le compte, et met la réponse dans le contexte de la requête :

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"
}

Pour interroger depuis un handler : d, err := bot.Decide(r.Context(), korzen.FromHTTP(r, korzen.RemoteIP(r))). À la moindre erreur, d.Action vaut allow, et err dit pourquoi, pour vos logs. Une réponse dont l'action n'est ni allow, ni challenge, ni block compte aussi comme une erreur ; le *korzen.Problem d'une réponse d'erreur est dans err (errors.As). Pour journaliser chaque appel, définissez le OnAnswer du client : il reçoit le statut, Korzen-Request-Id et les headers RateLimit.

L'IP du visiteur

r.RemoteAddr est l'adresse de la connexion : celle de votre proxy, si vous en avez un. korzen.RemoteIP la lit : utilisez-le quand rien ne se trouve devant votre serveur. korzen.CloudflareIP lit CF-Connecting-IP, ce qui n'est juste que si votre serveur ne reçoit du trafic que de Cloudflare ; sinon, un visiteur pourrait l'écrire. Derrière votre propre load balancer, korzen.LastForwardedIP prend la dernière entrée de X-Forwarded-For, celle qu'il a ajoutée.

Appliquer l'action

Ne lisez que action. Avec block, répondez avec la même erreur que l'action affiche quand elle échoue pour toute autre raison, pour qu'un bot n'apprenne rien. Avec challenge, utilisez la vérification que vous avez déjà (un CAPTCHA, un code par e-mail ou téléphone) ; sans aucune, continuez. Le compte commence en mode monitor, où action est toujours allow : rien ne change pour les visiteurs tant que vous n'activez pas le mode block. Ou envoyez le visiteur vers notre page de challenge, à challenge.url : voir la page de challenge.

Sans OnChallenge, le middleware ne laisse passer aucun challenge : un chargement de page (GET) va vers notre page de challenge, et l'envoi d'un formulaire ou un appel d'API est refusé avec 403. Protégez aussi la page qui affiche le formulaire : une personne y rencontre le challenge, et son pass laisse passer l'envoi. Avec une vérification à vous, passez OnChallenge et lancez-la là. Le middleware renvoie la personne à l'adresse demandée, lue dans X-Forwarded-Host et X-Forwarded-Proto quand votre proxy les pose ; derrière un proxy qui transmet Host: localhost et aucun des deux (nginx par défaut), donnez PublicURL: "https://shop.example".

Garder le request_id

Chaque réponse a un request_id. Gardez-le avec ce que l'action a créé, dans une colonne du compte ou de la commande, ou dans une ligne de log avec leur identifiant. Il désigne cette décision quand vous nous interrogez à son sujet, et vous permet de retrouver plus tard quelle réponse a laissé entrer un faux compte. Quand le compte se révèle bon ou mauvais, renvoyez-le : bot.Feedback(ctx, []korzen.Feedback{{RequestID: id, Outcome: "chargeback"}}, korzen.Batch{}) (voir le feedback).

Tester sur votre machine

Lancez go run . et ouvrez votre serveur sur localhost. Les pages sur localhost acceptent toujours le tag et n'apparaissent jamais comme domaine. En local, l'IP est 127.0.0.1 ou ::1, et c'est très bien. Dans Docker sur votre machine, l'app voit à la place l'adresse du bridge, comme 172.17.0.1 : la page de challenge vous ramène quand même sur localhost, puisque le tag y a vu votre page.

  1. Définissez KORZEN_API_KEY dans l'environnement et ouvrez la page d'inscription. Dans les outils de développement du navigateur, le cookie _kz apparaît après quelques secondes.
  2. Inscrivez-vous. La décision apparaît sur la page Décisions du tableau de bord, sous le chemin que vous avez envoyé.
  3. Vérifiez le fail-open : mettez une mauvaise clé et inscrivez-vous à nouveau. L'appel répond 401, et l'inscription passe.

Erreurs fréquentes

  • Un HTTPClient avec un nouveau http.Transport à chaque appel, ou celui par défaut de la bibliothèque standard, qui garde deux connexions ouvertes. Chaque nouvelle est une nouvelle connexion TLS : laissez HTTPClient vide, ou donnez au vôtre korzen.NewTransport(), une fois pour le processus.
  • Lire CF-Connecting-IP sur un serveur que Cloudflare ne précède pas. Un bot écrit l'en-tête et choisit sa propre IP.
  • Appeler /v1/decide depuis le navigateur. L'API key atteindrait chaque visiteur ; l'appel a sa place sur votre serveur.
  • Pas de timeout, ou une erreur qui bloque l'inscription. Toute réponse autre que 200, ou l'absence de réponse à temps, signifie : laissez passer le visiteur.
  • Une ligne de votre log qui commence par korzen: the visitor's IP sent to Bot Protection. Le client l'écrit une fois, à la première décision dont l'IP est celle de votre propre réseau (d'un load balancer, d'un proxy, d'un conteneur), que Bot Protection juge comme sans IP : donnez à Options.IP l'adresse du visiteur (l'IP du visiteur). Dans Docker sur votre machine, c'est l'adresse du bridge, et là elle est sans conséquence.