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 preuve Pepsi-Origin valide, 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 = false ou state.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 = false est présent — un bon de commande a été créé précédemment et le message a été réveillé à nouveau (par le webhook de paiement pepsi-resume appelant le /resume de 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 non pending : 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_id est le jeton externe du message, de sorte que le {{order_id}} du webhook pepsi-resume se 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 notant paid: false et le délai absolu dans state.

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.1. Backend marchand indisponible

Un backend marchand injoignable, ou qui répond par une erreur à une demande de commande ou de statut, est le problème de l’hôte et non celui de l’expéditeur. Le message reste retenu (paused, la raison dans state.last_error) et le marchand est interrogé de nouveau toutes les cinq minutes, jamais au-delà de PAYMENT_DEADLINE ; l’échéance est fixée à la première tentative, de sorte qu’une panne ne la prolonge pas. Si le marchand ne peut toujours pas être interrogé une fois l’échéance passée, l’étape est fail-open : le message est remis à NEXT_STAGE sans vérification, avec un avertissement dans le journal. Une barrière de paiement incapable de dire si elle a été payée ne doit pas transformer une panne du marchand en courrier perdu. Une demande de statut pour une commande que le marchand ne connaît pas (404) est une réponse, non une panne : la commande est traitée comme impayée.

L’étape nécessite la section partagée [pepsi-payments] (le backend marchand et le jeton d’accès ; voir pepsi.conf(5)) et, pour que le webhook de paiement libère les messages en pause, le RESUME_AUTHORIZATION_TOKEN de [pepsi-httpd] (voir pepsi-httpd(1)).

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 champs List-* 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 :

  1. le HMAC de l’en-tête se vérifie contre notre secret et son host correspond à notre HOSTNAME (une vérification rapide et purement cryptographique) ; et

  2. seulement 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>.body

Le modèle Mustache pour le corps de réponse de demande de paiement (TEMPLATE_DIR est l’option [pepsi], par défaut ${DATADIR}/templates, c’est-à-dire /usr/share/pepsi/templates pour une installation avec --prefix=/usr). Le modèle pour PAYMENT_MESSAGE_DEFAULT_LANGUAGE (par défaut payment-request.en.body) est obligatoire ; ajoutez à ses côtés les langues que pepsi-stage-detect-language peut détecter. Le modèle reçoit les variables recipient_name / has_name, protected_mailbox, original_subject, pay_uri / pay_link, qr_cid, wallet_url, has_options et la liste payment_options (chaque amount plus has_description et description, 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 = true explicite → 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.