85.1.16. pepsi-stage-anti-spam¶
gate a message behind a GNU Taler payment
- Section du manuel:
1
85.1.16.1.1. Nom¶
pepsi-stage-anti-spam - l’étape anti-spam à péage à l’envoi du pipeline Pepsi.
85.1.16.1.2. Synopsis¶
pepsi-stage-anti-spam [GLOBAL-OPTIONS] worker
85.1.16.1.3. Description¶
pepsi-stage-anti-spam est un programme d’étape exécuté par pepsi-dispatch(1). Il charge une ligne pepsi.workqueue (refusant d’agir sauf si son status est running), lit sa section [stage-<stage>], et décide quoi faire à partir du JSON state du message, exigeant un paiement GNU Taler pour un message par ailleurs non classifié avant qu’il puisse continuer dans le pipeline.
La décision est :
le message a l”expéditeur d’enveloppe nul (un rebond ou une réponse automatique RFC 3834) — il n’est jamais retenu pour paiement et ne rebondit jamais lui-même. Lorsque la preuve d’origine est activée (une section
[pepsi-origin], voir PREUVE D’ORIGINE ci-dessous ; elle est activée par défaut), on vérifie que le rebond en est bien un que nous avons provoqué, et un rebond vérifié est redirigé vers NEXT_STAGE. Tout rebond que nous ne pouvons vérifier — un rebond sans preuvePepsi-Originvalide, ou tout rebond lorsque la preuve d’origine n’est pas configurée — est acheminé vers BOUNCE_TARGET_STAGE lorsqu’une telle étape est câblée (par exemple une étape de quarantaine ou de rejet), sinon redirigé vers NEXT_STAGE inchangé (un rebond n’est jamais silencieusement abandonné).state.spam = true— le message est abandonné (la ligne est supprimée).state.spam = falseoustate.paid = true— le message est redirigé vers NEXT_STAGE. Une classification explicite (posée par un autre composant) l’emporte sur le flux de paiement.state.paid = falseest présent — un bon de commande a été créé précédemment et le message a été réveillé à nouveau (par le webhook de paiementpepsi-resumeappelant le/resumede pepsi-httpd, ou par pepsi-dispatch(1) lorsque le délai a expiré). L’étape demande au backend marchand si le bon de commande est maintenant payé :payé — rediriger vers NEXT_STAGE ;
impayé et au-delà du délai stocké — rejeter le message : réacheminer vers le BOUNCE_STAGE de l’étape (câblez-le vers pepsi-stage-bounce(1) pour un DSN de rejet, ou pepsi-stage-discard(1) pour l’abandonner silencieusement) ; lorsqu’aucun BOUNCE_STAGE n’est configuré, la ligne est simplement supprimée ;
impayé mais encore avant le délai — le message est remis en pause jusqu’au délai et un avertissement est journalisé. Cela ne devrait pas arriver en fonctionnement normal (un message en pause ne se réveille qu’au paiement ou au délai). Il reste
paused, et nonpending: une ligne que le dispatcher pourrait réclamer aussitôt tournerait en boucle contre la reprise anticipée qui l’a réveillée.
pas de champ
paid(première rencontre) — l’étape crée un bon de commande marchand v1 tarifé par ORDER_CHOICES (order_idest le jeton externe du message, de sorte que le{{order_id}}du webhookpepsi-resumese résout vers ce message), puis met en pause le message — jusqu’au point de contrôle DELAY_DSN_AFTER lorsqu’il en est configuré un et qu’il tombe avant le délai, sinon jusqu’à PAYMENT_DEADLINE — en notantpaid: falseet le délai absolu dansstate.
Le délai est stocké dans state.pay_deadline (secondes epoch) car pepsi-dispatch(1) efface la colonne timeout de la ligne lorsqu’il remet en file un message en pause, de sorte qu’il ne pourrait sinon être récupéré à la réexécution.
Après avoir mis le message en pause à cette première rencontre, l’étape émet aussi une réponse automatique de demande de paiement à l’expéditeur (lorsque BLOCK_RESPONSE_STAGE est configuré). La réponse explique que le destinataire exige des expéditeurs inconnus qu’ils paient avant que leur courrier ne soit accepté, et porte l’URI taler://pay/… — à la fois comme un lien et comme un QR code (une partie image/png en ligne référencée par cid:) — accompagnée des options de paiement acceptables (les ORDER_CHOICES, en omettant tout choix impliquant une famille de jetons) et d’un pointeur vers https://wallet.taler.net/ pour les expéditeurs sans wallet. L’expéditeur est nommé d’après le nom d’affichage de l’en-tête From: original lorsqu’il est disponible. La partie lisible par un humain est un corps text/html rendu depuis le modèle Mustache payment-request.<lang>.body sous [pepsi] TEMPLATE_DIR. La ou les langues sont prises dans state.language tel que posé par pepsi-stage-detect-language (exécuté plus tôt dans le pipeline) : la réponse rend un bloc par langue détectée disposant d’un modèle, concaténés dans l’ordre détecté, chacun avec ses options de paiement localisées dans cette langue. Lorsque le message ne porte aucune langue détectée — ou qu’aucune des langues détectées n’a de modèle — le modèle PAYMENT_MESSAGE_DEFAULT_LANGUAGE est utilisé à la place. La réponse utilise l”expéditeur nul <> (de sorte qu’elle ne rebondit jamais elle-même et ne passe par aucune barrière) et est injectée à BLOCK_RESPONSE_STAGE (normalement une chaîne de signature/relais), de sorte qu’elle est signée par DKIM et remise comme tout autre courrier sortant.
La réponse part vers un expéditeur d’enveloppe que personne n’a vérifié ; elle est donc retenue — le message n’en est pas moins mis en attente de paiement — dans deux cas. D’abord, lorsque la RFC 3834 dit que le message ne doit pas recevoir de réponse automatique (Auto-Submitted autre que no, Precedence: bulk|list|junk, X-Auto-Response-Suppress, un champ List-* quelconque, un expéditeur de service, un multipart/report). Ensuite, lorsque le même expéditeur a déjà reçu une demande de paiement pour la même boîte protégée (le premier destinataire d’enveloppe) dans le délai BLOCK_RESPONSE_SUPPRESS (une heure par défaut) : sinon, un flot de messages portant un même expéditeur falsifié deviendrait le même flot de demandes vers cette adresse. La fenêtre est revendiquée et enregistrée en une seule instruction (la fonction payment_request_should_send sur pepsi.payment_request_reply), après que le message a été mis en pause et la réponse rendue, de sorte que deux workers tenant deux messages d’un même expéditeur envoient une seule demande, et qu’un échec avant ce point ne consomme pas la fenêtre. Un expéditeur dont le second message tombe dans la fenêtre ne reçoit aucune invitation à son sujet ; ce message peut tout de même être payé (son bon de commande existe) et, sinon, rebondit à l’échéance comme tout message impayé. 0 s répond à chaque message.
La réponse automatique porte aussi deux en-têtes lisibles par machine afin qu’un Pepsi émetteur puisse régler la demande automatiquement (voir pepsi-stage-auto-pay(1)) : un en-tête Taler: contenant l’URI taler://pay/…, et l’en-tête Pepsi-Origin: du message original copié verbatim (lorsque le message entrant en portait un). Renvoyer en écho la preuve d’origine permet au site d’origine de vérifier que la demande de retour concerne un courrier qu’il a réellement envoyé et de mesurer ce qu’il dépense par message original (toutes les demandes pour un même original — par exemple l’éclatement d’une liste de diffusion — partagent un même nonce Pepsi-Origin). L’en-tête est copié tel quel, non re-signé : seul le site d’origine peut le vérifier (son HMAC a pour clé le secret de ce site).
La réponse est rendue avant que le message ne soit mis en pause. Si aucune réponse ne peut être produite — notamment lorsque le modèle PAYMENT_MESSAGE_DEFAULT_LANGUAGE manque — le message est retenu pour paiement sans réponse, avec un avertissement dans le journal ; rien n’est réservé dans la fenêtre, de sorte qu’un message ultérieur du même expéditeur reçoit une réponse une fois le modèle revenu. pepsi-setup valide d’emblée que ce modèle existe. L’injection de la réponse déjà rendue après la pause relève du meilleur effort : une erreur de base de données à ce moment-là est journalisée, la fenêtre réservée pour elle est rendue, et le message reste en pause pour paiement.
85.1.16.1.3.2. Courrier que la barrière retient par erreur¶
Prudence
Seul l’expéditeur d’enveloppe nul contourne la barrière. Tout le reste qui n’est pas blanchi par state.spam = false — normalement positionné par pepsi-stage-check-whitelist(1) placé avant cette étape — est retenu, y compris le courrier que personne ne peut ou ne veut payer :
Le propre courrier de lien sécurisé de Pepsi. La notification de lien et le courrier de PIN de pepsi-stage-secure-link(1) portent l”expéditeur original comme expéditeur d’enveloppe (délibérément, afin qu’un échec lui parvienne), et non
<>. Lorsque l’un d’eux rentre dans ce déploiement comme courrier entrant vers une boîte protégée par la barrière — un PIN renvoyé à un expéditeur local, un lien vers un destinataire local —, il est retenu jusqu’à PAYMENT_DEADLINE puis rejeté, et la fonctionnalité échoue silencieusement. Une réponse rédigée dans le portail est injectée sur le chemin entrant sans signature, avec le destinataire externe comme expéditeur, et est retenue comme tout autre courrier de cette adresse. Les accusés de lecture utilisent l’expéditeur nul et passent.Messages de listes de diffusion. Un message de liste porte le
From:de son auteur, de sorte qu’une whitelist de correspondants ne le couvre pas, et ses champsList-*suppriment la demande de paiement, de sorte que personne n’est même sollicité : chaque message de liste destiné à un abonné protégé par la barrière est retenu puis rejeté à l’échéance.
Les avis d’absence (pepsi-stage-vacation(1)) et les propres demandes de paiement de cette étape utilisent l’expéditeur nul et ne sont jamais retenus pour paiement ; lorsque BOUNCE_TARGET_STAGE est câblé, ils sont soumis à la vérification de preuve d’origine ci-dessous comme tout autre message à expéditeur nul.
Le contournement consiste à placer ces expéditeurs en whitelist dans un groupe que nomme le WHITELIST_NAME de l’étape de vérification. Pour le courrier dont vos propres domaines sont l’origine, conservez l’exigence DKIM par défaut : un DKIM aligné valide pour example.com ne peut provenir que de votre propre étape de signature, de sorte que le motif ne peut pas être satisfait par un From: falsifié
pepsi-whitelist add correspondents '^[^@]+@example\.com$'
(à répéter pour chaque domaine servi, et pour le domaine de [pepsi-secure-link] NOTIFY_FROM s’il est défini ailleurs). Pour une liste de diffusion, placez en whitelist son identifiant, en nommant le scelleur ARC qui doit s’en porter garant
pepsi-whitelist add-list --sealer lists.example.org \
correspondents users.lists.example.org
Voir pepsi-whitelist(1).
85.1.16.1.4. Configuration¶
Les options résident dans la propre section [stage-<name>] de l’étape (PROGRAM = pepsi-stage-anti-spam) : le NEXT_STAGE obligatoire, BOUNCE_STAGE, BOUNCE_TARGET_STAGE (voir PREUVE D’ORIGINE ci-dessous), les paramètres du bon de commande (les ORDER_CHOICES obligatoires, plus PAYMENT_DEADLINE, DELAY_DSN_AFTER, SUMMARY, FULFILLMENT_MESSAGE) et les paramètres de réponse automatique (BLOCK_RESPONSE_STAGE, BLOCK_RESPONSE_FROM, BLOCK_RESPONSE_SUBJECT, BLOCK_RESPONSE_SUPPRESS, PAYMENT_MESSAGE_DEFAULT_LANGUAGE). L’étape nécessite aussi la section partagée [pepsi-payments] ; le webhook de paiement qui libère un message payé avant son échéance a besoin du RESUME_AUTHORIZATION_TOKEN de [pepsi-httpd]. Tous sont documentés dans pepsi.conf(5).
85.1.16.1.5. Preuve d’origine¶
Rediriger inconditionnellement chaque rebond à expéditeur nul n’est sûr que parce qu’un rebond ne rebondit jamais lui-même — mais cela permet à un attaquant d’injecter du backscatter (des rebonds falsifiés) que Pepsi relaie alors vers un faux « expéditeur original ». Pour rejeter de telles contrefaçons, la section partagée [pepsi-origin] (pepsi.conf(5)) est activée par défaut — pepsi-setup(1) génère un secret aléatoire au premier lancement si aucun n’est configuré. Les deux étapes de relais (pepsi-stage-relay-to-internet(1) et pepsi-stage-relay-to-smarthost(1)) estampillent alors chaque message sortant d’un en-tête Pepsi-Origin — un HMAC sur le compte expéditeur d’origine, un nonce frais de 128 bits, un horodatage et notre HOSTNAME — et enregistrent le nonce dans pepsi.origin_nonce pendant deux semaines.
Un rebond authentique intègre le message original (au moins ses en-têtes, et donc l’en-tête Pepsi-Origin) dans le corps de son DSN. Cette étape parcourt tout le rebond de retour à la recherche de cet en-tête et ne fait confiance au rebond que si les deux vérifications réussissent, dans cet ordre :
le HMAC de l’en-tête se vérifie contre notre secret et son
hostcorrespond à notreHOSTNAME(une vérification rapide et purement cryptographique) ; etseulement ensuite — le nonce est encore présent (et non expiré) dans
pepsi.origin_nonce.
Un rebond que nous ne pouvons vérifier — un rebond qui ne porte aucun en-tête de ce genre, échoue au HMAC, ou dont le nonce n’est plus suivi, et absolument tout rebond lorsque la preuve d’origine est désactivée ([pepsi-origin] ENABLED = no ; aucun ne peut alors être prouvé nôtre) — est acheminé vers BOUNCE_TARGET_STAGE lorsqu’une telle étape est câblée, sinon redirigé vers NEXT_STAGE inchangé (jamais silencieusement abandonné).
BOUNCE_TARGET_STAGE devrait normalement pointer vers une boîte aux lettres de quarantaine ou une étape de revue plutôt que vers un puits sans retour pepsi-stage-discard(1) : les rapports de non-remise standard des grands MTA (Postfix, Exim, Gmail, Exchange) renvoient les en-têtes originaux et se vérifient donc, mais une réelle frange de rebonds légitimes — avis de rejet de spam qui citent peu de l’original, passerelles qui retirent les en-têtes, générateurs de rebonds minimaux ou hérités, et rebonds arrivant après la fenêtre de nonce de deux semaines — ne peuvent être vérifiés, et les abandonner d’emblée fait perdre des rapports de non-remise dont vos propres utilisateurs ont besoin.
85.1.16.1.6. Fichiers¶
<TEMPLATE_DIR>/payment-request.<lang>.bodyLe modèle Mustache pour le corps de réponse de demande de paiement (
TEMPLATE_DIRest l’option[pepsi], par défaut${DATADIR}/templates, c’est-à-dire/usr/share/pepsi/templatespour une installation avec--prefix=/usr). Le modèle pour PAYMENT_MESSAGE_DEFAULT_LANGUAGE (par défautpayment-request.en.body) est obligatoire ; ajoutez à ses côtés les langues quepepsi-stage-detect-languagepeut détecter. Le modèle reçoit les variablesrecipient_name/has_name,protected_mailbox,original_subject,pay_uri/pay_link,qr_cid,wallet_url,has_optionset la listepayment_options(chaqueamountplushas_descriptionetdescription, localisées dans la langue de ce modèle).
85.1.16.1.7. État¶
Entrées : state.spam / state.paid (booléens, facultatifs) classifient le message ; state.pay_deadline (secondes epoch) est le délai que cette étape a écrit ; state.dsn porte les notify/orcpt du destinataire (renvoyés en écho dans un rejet).
Sorties : à la première rencontre, l’étape fusionne {"paid": false, "pay_deadline": <epoch>} dans state et met en pause. Au point de contrôle DELAY_DSN_AFTER, elle ajoute state.delay_sent = true une fois qu’un DSN de délai a été envoyé (de sorte qu’il se déclenche au plus une fois) et remet en pause. Au rejet, elle réachemine vers BOUNCE_STAGE avec un objet state.bounce (kind = permanent) pour pepsi-stage-bounce(1). La disposition de l’état est décrite dans pepsi.state(7).
Transitions (pilotées par state.spam/state.paid, PAYMENT_DEADLINE et le statut du bon de commande marchand) :
en liste blanche (
state.spam = false) ou déjà payé (state.paid = true) → avancer vers NEXT_STAGE ;state.spam = trueexplicite → terminer (la ligne est supprimée) ;première rencontre → créer un bon de commande Taler, injecter la demande de paiement à BLOCK_RESPONSE_STAGE (sauf si elle est retenue, voir ci-dessus), et mettre en pause (jusqu’au point de contrôle DELAY_DSN_AFTER lorsqu’il en est configuré un et qu’il tombe avant, sinon jusqu’à PAYMENT_DEADLINE) ;
réveillé (webhook de reprise ou délai) et maintenant payé → avancer vers NEXT_STAGE ;
réveillé, encore impayé, délai passé → réacheminer vers BOUNCE_STAGE (ou terminer, si aucun BOUNCE_STAGE n’est configuré) ;
marchand indisponible (voir ci-dessus) → nouvelle pause avant l’échéance, avance vers NEXT_STAGE après elle ;
réveillé, encore impayé, délai pas encore atteint → avec DELAY_DSN_AFTER configuré, c’est le point de contrôle de délai : envoyer à l’expéditeur un DSN différé à usage unique (RFC 3461, seulement s’il a demandé
NOTIFY=DELAY) sans libérer le message, puis remettre en pause jusqu’à PAYMENT_DEADLINE ; sans DELAY_DSN_AFTER, un réveil précoce est anormal et le message est remis en pause (paused) jusqu’au délai.
85.1.16.1.8. Commandes¶
- worker
Exécuté comme un worker persistant de pepsi-dispatch(1), lisant les identifiants de message sur l’entrée standard.
85.1.16.1.9. Options globales¶
Ces options globales précèdent la sous-commande.
- -c FILE, –config FILE
Lit la configuration depuis FILE au lieu de parcourir les emplacements par défaut.
- -L LOGLEVEL, –log LOGLEVEL
Règle la verbosité de journalisation (par défaut
info).- -v, –verbose
Affiche les messages de journal de toutes les sources.
- -h, –help ; -V, –version
Affiche un résumé d’utilisation / la version et quitte.
85.1.16.1.10. Code de sortie¶
- 0
Le message a été traité (abandonné, redirigé, mis en pause pour paiement, ou réacheminé pour rejet).
- 1
Une erreur s’est produite (message introuvable ou non
running, ou un paramètre par adresse qui casse la section de l’étape). La raison est écrite dans le journal.Une défaillance de l’hôte (la base de données, un modèle ou un helper inutilisable) n’est pas signalée comme un échec : le message est mis en pause et réessayé, comme le décrit Erreurs d’étape dans pepsi-dispatch(1). Une section qui ne s’analyse pas fait refuser au worker de démarrer (statut 78) au lieu de faire échouer chaque message tour à tour. L’absence de backend
[pepsi-payments]est une telle section.
85.1.16.1.11. Exemples¶
Traiter le message 42 (il doit être running)
echo 42 | pepsi-stage-anti-spam -c /etc/pepsi/pepsi.conf worker
Une étape qui facture un KUDOS et rejette le courrier impayé après deux jours
[stage-anti-spam]
PROGRAM = pepsi-stage-anti-spam
NEXT_STAGE = srs
BOUNCE_STAGE = bounce
BLOCK_RESPONSE_STAGE = dkim-sign
PAYMENT_DEADLINE = 48 h
ORDER_CHOICES = [{"amount":"KUDOS:1"}]
85.1.16.1.12. Voir aussi¶
pepsi-config(1), pepsi-stage-bounce(1), pepsi-stage-discard(1), pepsi-httpd(1), pepsi-dispatch(1), pepsi-setup(1), pepsi.conf(5), pepsi.state(7)
85.1.16.1.13. Bogues¶
Signalez les bogues au gestionnaire de tickets de Pepsi.