fix(optimiseur): un token recopié par l'optimiseur ne sort par aucun journal (brief 4 T7, correctif A)
Constat de T6 (question 2) : la trace masquait un token recopié dans reason ou planId, mais les lignes [Optimiseur] (planId, texte, détail des refus, nom du handshake) et GetOptimizerStatus.name le portaient en clair. - OptimizerSecret : UNE fonction de masquage (texte brut et échappé JSON, objet JSON à toute profondeur, clés comprises ; ligne de trace sérialisée), partagée par le client et les traces. - OptimizerClient masque chaque message reçu dès son analyse, avec le token envoyé sur cette connexion : rien en aval (journal, traces, app) ne voit le secret ; dit une fois. - Test testAnEchoedTokenLeaksNowhere : secret avec " et \ recopié partout ; aucune ligne de journal (tous niveaux), aucune trace, aucune réponse RPC. 4 contre-épreuves mordent. decisions sans endpoint : IDENTIQUE (23 792 lignes), 0 rejeu. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
10683744d2
commit
693e01579f
@ -19,6 +19,7 @@ HEADERS += \
|
|||||||
$$PWD/scheduler/rulebasedscheduler.h \
|
$$PWD/scheduler/rulebasedscheduler.h \
|
||||||
$$PWD/scheduler/optimizerprotocol.h \
|
$$PWD/scheduler/optimizerprotocol.h \
|
||||||
$$PWD/scheduler/optimizerclient.h \
|
$$PWD/scheduler/optimizerclient.h \
|
||||||
|
$$PWD/scheduler/optimizersecret.h \
|
||||||
$$PWD/scheduler/optimizerplan.h \
|
$$PWD/scheduler/optimizerplan.h \
|
||||||
$$PWD/scheduler/socketscheduler.h \
|
$$PWD/scheduler/socketscheduler.h \
|
||||||
$$PWD/scheduler/optimizerstatus.h \
|
$$PWD/scheduler/optimizerstatus.h \
|
||||||
@ -43,6 +44,7 @@ SOURCES += \
|
|||||||
$$PWD/scheduler/rulebasedscheduler.cpp \
|
$$PWD/scheduler/rulebasedscheduler.cpp \
|
||||||
$$PWD/scheduler/optimizerprotocol.cpp \
|
$$PWD/scheduler/optimizerprotocol.cpp \
|
||||||
$$PWD/scheduler/optimizerclient.cpp \
|
$$PWD/scheduler/optimizerclient.cpp \
|
||||||
|
$$PWD/scheduler/optimizersecret.cpp \
|
||||||
$$PWD/scheduler/optimizerplan.cpp \
|
$$PWD/scheduler/optimizerplan.cpp \
|
||||||
$$PWD/scheduler/socketscheduler.cpp \
|
$$PWD/scheduler/socketscheduler.cpp \
|
||||||
$$PWD/scheduler/optimizerstatus.cpp \
|
$$PWD/scheduler/optimizerstatus.cpp \
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
|
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
|
||||||
#include "optimizerclient.h"
|
#include "optimizerclient.h"
|
||||||
|
#include "optimizersecret.h"
|
||||||
|
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
@ -302,6 +303,7 @@ void OptimizerClient::onConnected()
|
|||||||
// Relu à CHAQUE connexion (décision Q2) : un token remplacé vaut dès la tentative suivante.
|
// Relu à CHAQUE connexion (décision Q2) : un token remplacé vaut dès la tentative suivante.
|
||||||
const QByteArray token = readToken();
|
const QByteArray token = readToken();
|
||||||
m_tokenSent = !token.isEmpty();
|
m_tokenSent = !token.isEmpty();
|
||||||
|
m_tokenConnexion = QString::fromUtf8(token);
|
||||||
if (m_tokenSent)
|
if (m_tokenSent)
|
||||||
params.insert(QStringLiteral("token"), QString::fromUtf8(token));
|
params.insert(QStringLiteral("token"), QString::fromUtf8(token));
|
||||||
|
|
||||||
@ -392,7 +394,16 @@ void OptimizerClient::handleLine(const QByteArray &line)
|
|||||||
countProtocolError(QStringLiteral("invalid-json"), QStringLiteral("%1 octets").arg(line.size()));
|
countProtocolError(QStringLiteral("invalid-json"), QStringLiteral("%1 octets").arg(line.size()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const QJsonObject message = doc.object();
|
// Correctif A (brief 4 T7) : un optimiseur peut RECOPIER le token qu'il a reçu. Masqué ICI,
|
||||||
|
// avant toute lecture : nom, version, méthode, plan, motifs — rien en aval ne voit le secret,
|
||||||
|
// ni le journal, ni les traces, ni l'app.
|
||||||
|
int echos = 0;
|
||||||
|
const QJsonObject message = OptimizerSecret::masquerJson(doc.object(), m_tokenConnexion, &echos);
|
||||||
|
if (echos > 0 && !m_echoTokenSignale) {
|
||||||
|
m_echoTokenSignale = true;
|
||||||
|
qCWarning(dcNymeaEnergy()) << "[Optimiseur] l'optimiseur recopie le token dans ses réponses — masqué"
|
||||||
|
" à la réception (journal, traces, app) ; signalé une fois";
|
||||||
|
}
|
||||||
const QJsonValue idValue = message.value(QStringLiteral("id"));
|
const QJsonValue idValue = message.value(QStringLiteral("id"));
|
||||||
|
|
||||||
if (idValue.isUndefined() || idValue.isNull()) {
|
if (idValue.isUndefined() || idValue.isNull()) {
|
||||||
@ -553,6 +564,7 @@ void OptimizerClient::dropSocket()
|
|||||||
tcp->abort();
|
tcp->abort();
|
||||||
socket->deleteLater();
|
socket->deleteLater();
|
||||||
m_buffer.clear();
|
m_buffer.clear();
|
||||||
|
m_tokenConnexion.clear();
|
||||||
m_skippingLine = false;
|
m_skippingLine = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -118,6 +118,11 @@ private:
|
|||||||
* **Le token** est relu dans \c tokenPath à CHAQUE tentative de connexion : le remplacer prend
|
* **Le token** est relu dans \c tokenPath à CHAQUE tentative de connexion : le remplacer prend
|
||||||
* effet à la suivante, sans redémarrage. Il ne figure dans aucune ligne de journal, ni en clair
|
* effet à la suivante, sans redémarrage. Il ne figure dans aucune ligne de journal, ni en clair
|
||||||
* ni par sa longueur ; le message de handshake qui le porte n'est jamais journalisé.
|
* ni par sa longueur ; le message de handshake qui le porte n'est jamais journalisé.
|
||||||
|
* **Un optimiseur qui le recopie** (\c reason, \c planId, \c name…) ne le fait pas sortir : chaque
|
||||||
|
* message reçu est masqué dès son analyse (\c OptimizerSecret::masquerJson(), avec le token
|
||||||
|
* envoyé sur CETTE connexion), avant toute journalisation et avant tout signal — journal, traces
|
||||||
|
* et charges utiles de l'app ne voient que \c "[token masqué]". Le fait est dit une fois par
|
||||||
|
* client, jamais la valeur.
|
||||||
*
|
*
|
||||||
* \invariant Aucune méthode n'attend : toute E/S est asynchrone. \c requestPlan() rend la main
|
* \invariant Aucune méthode n'attend : toute E/S est asynchrone. \c requestPlan() rend la main
|
||||||
* immédiatement ; la réponse arrive par signal, au plus tôt au tour de boucle suivant.
|
* immédiatement ; la réponse arrive par signal, au plus tôt au tour de boucle suivant.
|
||||||
@ -291,6 +296,8 @@ private:
|
|||||||
quint64 m_handshakeId = 0;
|
quint64 m_handshakeId = 0;
|
||||||
quint64 m_pendingId = 0;
|
quint64 m_pendingId = 0;
|
||||||
bool m_tokenSent = false; // le handshake en cours porte un token (pour le diagnostic)
|
bool m_tokenSent = false; // le handshake en cours porte un token (pour le diagnostic)
|
||||||
|
QString m_tokenConnexion; // le token envoyé sur CETTE connexion : ce qu'un écho recopierait
|
||||||
|
bool m_echoTokenSignale = false;
|
||||||
|
|
||||||
qint64 m_handshakeDeadline = -1; // -1 = pas d'échéance
|
qint64 m_handshakeDeadline = -1; // -1 = pas d'échéance
|
||||||
qint64 m_requestDeadline = -1;
|
qint64 m_requestDeadline = -1;
|
||||||
|
|||||||
83
energyplugin/etm/scheduler/optimizersecret.cpp
Normal file
83
energyplugin/etm/scheduler/optimizersecret.cpp
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
|
||||||
|
|
||||||
|
#include "optimizersecret.h"
|
||||||
|
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
|
||||||
|
namespace OptimizerSecret {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// La forme que QJsonDocument donne au token DANS une chaîne : ["…"] sans ses bornes.
|
||||||
|
QByteArray formeEchappee(const QString &token)
|
||||||
|
{
|
||||||
|
const QByteArray tableau = QJsonDocument(QJsonArray{token}).toJson(QJsonDocument::Compact);
|
||||||
|
return tableau.mid(2, tableau.size() - 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonValue masquerValeur(const QJsonValue &v, const QString &token, int *masques)
|
||||||
|
{
|
||||||
|
switch (v.type()) {
|
||||||
|
case QJsonValue::String:
|
||||||
|
return masquerTexte(v.toString(), token, masques);
|
||||||
|
case QJsonValue::Array: {
|
||||||
|
QJsonArray out;
|
||||||
|
for (const QJsonValue &x : v.toArray())
|
||||||
|
out.append(masquerValeur(x, token, masques));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
case QJsonValue::Object:
|
||||||
|
return masquerJson(v.toObject(), token, masques);
|
||||||
|
default:
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
QString masquerTexte(const QString &texte, const QString &token, int *masques)
|
||||||
|
{
|
||||||
|
if (token.isEmpty())
|
||||||
|
return texte;
|
||||||
|
QString out = texte;
|
||||||
|
// L'échappée d'abord : elle CONTIENT la brute quand le token n'a ni " ni \ (elles sont alors
|
||||||
|
// égales), et sinon un optimiseur qui a recopié la forme sérialisée l'a fait sous celle-là.
|
||||||
|
for (const QString &forme : {QString::fromUtf8(formeEchappee(token)), token}) {
|
||||||
|
const int n = out.count(forme);
|
||||||
|
if (n == 0)
|
||||||
|
continue;
|
||||||
|
out.replace(forme, QString::fromUtf8(Masque)); // UTF-8 : « masqué »
|
||||||
|
if (masques)
|
||||||
|
*masques += n;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject masquerJson(const QJsonObject &message, const QString &token, int *masques)
|
||||||
|
{
|
||||||
|
if (token.isEmpty())
|
||||||
|
return message;
|
||||||
|
QJsonObject out;
|
||||||
|
for (auto it = message.constBegin(); it != message.constEnd(); ++it)
|
||||||
|
out.insert(masquerTexte(it.key(), token, masques), masquerValeur(it.value(), token, masques));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray masquerLigneJson(const QByteArray &ligne, const QByteArray &token, int *masques)
|
||||||
|
{
|
||||||
|
if (token.isEmpty())
|
||||||
|
return ligne;
|
||||||
|
const QByteArray echappe = formeEchappee(QString::fromUtf8(token));
|
||||||
|
if (echappe.isEmpty() || !ligne.contains(echappe))
|
||||||
|
return ligne;
|
||||||
|
QByteArray out = ligne;
|
||||||
|
const int n = out.count(echappe);
|
||||||
|
out.replace(echappe, Masque);
|
||||||
|
if (masques)
|
||||||
|
*masques += n;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
56
energyplugin/etm/scheduler/optimizersecret.h
Normal file
56
energyplugin/etm/scheduler/optimizersecret.h
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \file optimizersecret.h
|
||||||
|
* \brief Masquage du token d'optimiseur (brief 4 T7, correctif A) — LA fonction partagée par le
|
||||||
|
* journal, les traces et tout ce qui part vers l'app.
|
||||||
|
*
|
||||||
|
* Le token ne voyage que dans le handshake (§3). Un optimiseur qui le RECOPIE dans ce qu'il
|
||||||
|
* renvoie (\c reason, \c planId, un code, un paramètre, son \c name…) ferait sortir le secret par
|
||||||
|
* chacun des chemins qui relaient son texte : journal \c [Optimiseur], traces, \c GetOptimizerStatus.
|
||||||
|
* Plutôt que de masquer à chacun de ces chemins — et d'oublier le prochain —, le client masque
|
||||||
|
* chaque message REÇU dès son analyse (\c OptimizerClient), avant tout usage : rien en aval ne voit
|
||||||
|
* jamais le secret. L'écrivain des traces masque en plus la ligne sérialisée (défense en
|
||||||
|
* profondeur), avec la même fonction.
|
||||||
|
*
|
||||||
|
* \invariant Aucune fonction d'ici ne journalise, ni le token, ni sa longueur.
|
||||||
|
*/
|
||||||
|
namespace OptimizerSecret {
|
||||||
|
|
||||||
|
//! Ce qui remplace le token, partout.
|
||||||
|
constexpr const char *Masque = "[token masqué]";
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Remplace dans un TEXTE chaque occurrence du token, sous sa forme brute et sous sa forme
|
||||||
|
* échappée JSON (\c " et \c \\ précédés d'un \c \\).
|
||||||
|
* \param texte Texte à masquer. \param token Le secret ; vide = rien à faire.
|
||||||
|
* \param[in,out] masques Incrémenté du nombre de remplacements (peut être \c nullptr).
|
||||||
|
* \return Le texte masqué.
|
||||||
|
*/
|
||||||
|
QString masquerTexte(const QString &texte, const QString &token, int *masques = nullptr);
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Masque un message JSON reçu : chaque chaîne ET chaque clé d'objet, à toute profondeur.
|
||||||
|
* \param message Objet analysé. \param token Le secret ; vide = rien à faire.
|
||||||
|
* \param[in,out] masques Incrémenté du nombre de remplacements (peut être \c nullptr).
|
||||||
|
* \return L'objet masqué. Les nombres, booléens et la structure sont inchangés.
|
||||||
|
*/
|
||||||
|
QJsonObject masquerJson(const QJsonObject &message, const QString &token, int *masques = nullptr);
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Masque une ligne JSON DÉJÀ SÉRIALISÉE (\c QJsonDocument, compacte) : le token y est cherché
|
||||||
|
* sous sa seule forme échappée — c'est ainsi, et seulement ainsi, qu'il y figurerait. Chercher la
|
||||||
|
* forme brute pourrait mordre sur la syntaxe de la ligne et la rendre illisible.
|
||||||
|
* \param ligne Ligne JSON. \param token Le secret, brut ; vide = rien à faire.
|
||||||
|
* \param[in,out] masques Incrémenté du nombre de remplacements (peut être \c nullptr).
|
||||||
|
* \return La ligne masquée.
|
||||||
|
*/
|
||||||
|
QByteArray masquerLigneJson(const QByteArray &ligne, const QByteArray &token, int *masques = nullptr);
|
||||||
|
|
||||||
|
}
|
||||||
@ -14,6 +14,7 @@
|
|||||||
#include <nymeasettings.h>
|
#include <nymeasettings.h>
|
||||||
|
|
||||||
#include "energysettings.h"
|
#include "energysettings.h"
|
||||||
|
#include "optimizersecret.h"
|
||||||
|
|
||||||
Q_DECLARE_LOGGING_CATEGORY(dcNymeaEnergy)
|
Q_DECLARE_LOGGING_CATEGORY(dcNymeaEnergy)
|
||||||
|
|
||||||
@ -122,25 +123,6 @@ QVariantList executees(const Slot &slot, const QHash<QString, LoadAction> &appli
|
|||||||
return l;
|
return l;
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray masquerToken(const QByteArray &ligne, const QByteArray &token, int *masques)
|
|
||||||
{
|
|
||||||
if (masques)
|
|
||||||
*masques = 0;
|
|
||||||
if (token.isEmpty())
|
|
||||||
return ligne;
|
|
||||||
// La forme que QJsonDocument donnerait au token dans une chaîne : ["…"] sans ses bornes.
|
|
||||||
const QByteArray tableau = QJsonDocument(QJsonArray{QString::fromUtf8(token)}).toJson(QJsonDocument::Compact);
|
|
||||||
const QByteArray echappe = tableau.mid(2, tableau.size() - 4);
|
|
||||||
if (echappe.isEmpty() || !ligne.contains(echappe))
|
|
||||||
return ligne;
|
|
||||||
QByteArray out = ligne;
|
|
||||||
const int n = out.count(echappe);
|
|
||||||
out.replace(echappe, QByteArrayLiteral("[token masqué]"));
|
|
||||||
if (masques)
|
|
||||||
*masques = n;
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
Reglage Reglage::lire()
|
Reglage Reglage::lire()
|
||||||
{
|
{
|
||||||
EnergySettings s;
|
EnergySettings s;
|
||||||
@ -196,7 +178,9 @@ void OptimizerTraceWriter::ecrire(const QVariantMap &ligne, const QDate &jour)
|
|||||||
} decompte{m_enAttente.get()};
|
} decompte{m_enAttente.get()};
|
||||||
|
|
||||||
int masques = 0;
|
int masques = 0;
|
||||||
const QByteArray l = OptimizerTrace::masquerToken(
|
// Seconde garde : le client masque déjà chaque message reçu (OptimizerSecret, correctif A) ;
|
||||||
|
// celle-ci couvre un token REMPLACÉ depuis la connexion, que l'écrivain relit du fichier.
|
||||||
|
const QByteArray l = OptimizerSecret::masquerLigneJson(
|
||||||
QJsonDocument(QJsonObject::fromVariantMap(ligne)).toJson(QJsonDocument::Compact),
|
QJsonDocument(QJsonObject::fromVariantMap(ligne)).toJson(QJsonDocument::Compact),
|
||||||
token(), &masques)
|
token(), &masques)
|
||||||
+ '\n';
|
+ '\n';
|
||||||
|
|||||||
@ -32,8 +32,9 @@
|
|||||||
* \c t (instant du cycle, heure locale avec décalage), \c request (les paramètres \c GetPlan
|
* \c t (instant du cycle, heure locale avec décalage), \c request (les paramètres \c GetPlan
|
||||||
* envoyés, ou ceux qui l'auraient été), \c response (ce qui a répondu à CETTE requête),
|
* envoyés, ou ceux qui l'auraient été), \c response (ce qui a répondu à CETTE requête),
|
||||||
* \c executed (actions appliquées au cycle), \c strategy (\c optimizer | \c rules | \c none),
|
* \c executed (actions appliquées au cycle), \c strategy (\c optimizer | \c rules | \c none),
|
||||||
* \c latencyMs (optimiseur seulement). Jamais le token : il ne voyage que dans le handshake,
|
* \c latencyMs (optimiseur seulement). Jamais le token : il ne voyage que dans le handshake ;
|
||||||
* et l'écrivain masque en plus toute occurrence que la réponse brute d'un optimiseur porterait.
|
* le client masque toute réponse qui le recopierait (\c OptimizerSecret), et l'écrivain masque
|
||||||
|
* encore la ligne sérialisée, avec la même fonction.
|
||||||
*
|
*
|
||||||
* \invariant Les traces restent sur le site (invariant 10 du moteur) : aucun envoi, export manuel.
|
* \invariant Les traces restent sur le site (invariant 10 du moteur) : aucun envoi, export manuel.
|
||||||
* \invariant Rien ici ne commande : une trace activée ou non, en échec ou non, ne change aucune
|
* \invariant Rien ici ne commande : une trace activée ou non, en échec ou non, ne change aucune
|
||||||
@ -99,17 +100,6 @@ QVariantMap reponseRegles(const Plan &plan, const QDateTime &now);
|
|||||||
QVariantList executees(const Slot &slot, const QHash<QString, LoadAction> &appliquees,
|
QVariantList executees(const Slot &slot, const QHash<QString, LoadAction> &appliquees,
|
||||||
const QSet<QString> &delestees);
|
const QSet<QString> &delestees);
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Remplace, dans une ligne déjà sérialisée, chaque occurrence du token par
|
|
||||||
* \c "[token masqué]".
|
|
||||||
* \param ligne Ligne JSON. \param token Le secret, brut ; vide = rien à faire.
|
|
||||||
* \param[out] masques Nombre de remplacements (peut être \c nullptr).
|
|
||||||
* \return La ligne masquée.
|
|
||||||
* \note Le token est cherché sous sa forme ÉCHAPPÉE JSON (\c " et \c \\ ) : c'est ainsi qu'il
|
|
||||||
* apparaîtrait dans la ligne.
|
|
||||||
*/
|
|
||||||
QByteArray masquerToken(const QByteArray &ligne, const QByteArray &token, int *masques = nullptr);
|
|
||||||
|
|
||||||
/*! \brief Réglage des traces (\c Optimizer/trace* dans \c energy.conf). */
|
/*! \brief Réglage des traces (\c Optimizer/trace* dans \c energy.conf). */
|
||||||
struct Reglage {
|
struct Reglage {
|
||||||
bool active = false; //!< \c Optimizer/trace — défaut : désactivé.
|
bool active = false; //!< \c Optimizer/trace — défaut : désactivé.
|
||||||
|
|||||||
@ -14,8 +14,10 @@ TARGET = etm-powersync-optimizerclient
|
|||||||
INCLUDEPATH += $$PWD/../../../energyplugin
|
INCLUDEPATH += $$PWD/../../../energyplugin
|
||||||
|
|
||||||
HEADERS += $$PWD/../../../energyplugin/etm/scheduler/optimizerclient.h \
|
HEADERS += $$PWD/../../../energyplugin/etm/scheduler/optimizerclient.h \
|
||||||
|
$$PWD/../../../energyplugin/etm/scheduler/optimizersecret.h \
|
||||||
$$PWD/../common/etm-powersync-fakeoptimizer.h
|
$$PWD/../common/etm-powersync-fakeoptimizer.h
|
||||||
SOURCES += $$PWD/../../../energyplugin/etm/scheduler/optimizerclient.cpp \
|
SOURCES += $$PWD/../../../energyplugin/etm/scheduler/optimizerclient.cpp \
|
||||||
|
$$PWD/../../../energyplugin/etm/scheduler/optimizersecret.cpp \
|
||||||
testoptimizerclient.cpp
|
testoptimizerclient.cpp
|
||||||
|
|
||||||
target.path = $$[QT_INSTALL_PREFIX]/bin
|
target.path = $$[QT_INSTALL_PREFIX]/bin
|
||||||
|
|||||||
@ -134,6 +134,8 @@ private slots:
|
|||||||
void testTokenIsInNoTrace();
|
void testTokenIsInNoTrace();
|
||||||
void testTraceLineSize();
|
void testTraceLineSize();
|
||||||
void testTracerPairsTheResponseByRequestId();
|
void testTracerPairsTheResponseByRequestId();
|
||||||
|
// Brief 4 T7 — correctif A : un token recopié par l'optimiseur ne sort nulle part
|
||||||
|
void testAnEchoedTokenLeaksNowhere();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
//! Banc commun ; \p avecOptimiseur faux = rien n'est configuré.
|
//! Banc commun ; \p avecOptimiseur faux = rien n'est configuré.
|
||||||
@ -1944,5 +1946,107 @@ void TestSocketScheduler::testTracerPairsTheResponseByRequestId()
|
|||||||
QCOMPARE(l.first().value("latencyMs").toInt(), 12);
|
QCOMPARE(l.first().value("latencyMs").toInt(), 12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestSocketScheduler::testAnEchoedTokenLeaksNowhere()
|
||||||
|
{
|
||||||
|
// Correctif A (brief 4 T7). Un optimiseur qui RECOPIE le token — avec les deux caractères que
|
||||||
|
// JSON échappe — dans tout ce qu'il renvoie et que le moteur relaie : son nom et sa version au
|
||||||
|
// handshake, l'identifiant et la stratégie du plan, le texte d'une action, un code hors
|
||||||
|
// catalogue, une clé et une valeur de paramètre de motif, le texte d'une abstention, un loadId
|
||||||
|
// inconnu, une notification. Le secret ne doit apparaître dans AUCUNE ligne de journal (tous
|
||||||
|
// niveaux, nymea compris, hors la trace brute des données ENTRANTES de nymea — voir
|
||||||
|
// testTokenIsWriteOnlyAndNeverLeaks), aucune trace, aucune réponse ni notification RPC.
|
||||||
|
monter();
|
||||||
|
const QString secret = QStringLiteral("T7-echo\"du\\token-9876543210");
|
||||||
|
const QByteArray echappe = QJsonDocument(QJsonArray{secret}).toJson(QJsonDocument::Compact).mid(2).chopped(2);
|
||||||
|
const QStringList fragments{secret, QString::fromUtf8(echappe), QStringLiteral("9876543210")};
|
||||||
|
QCOMPARE(rpc("SetOptimizerToken", {{"token", secret}}).value("energyError").toString(),
|
||||||
|
QString("EnergyErrorNoError"));
|
||||||
|
const QString d = activerTraces();
|
||||||
|
QSignalSpy paquets(m_mockTcpServer, &MockTcpServer::outgoingData);
|
||||||
|
QLoggingCategory::setFilterRules(QStringLiteral("*.debug=true"));
|
||||||
|
Journal j;
|
||||||
|
|
||||||
|
const QJsonObject handshakeAvant = m_handshake;
|
||||||
|
m_handshake = handshakeResult();
|
||||||
|
m_handshake.insert("name", "écho " + secret);
|
||||||
|
m_handshake.insert("protocolVersion", "1.1-" + secret);
|
||||||
|
const int handshakes = m_handshakes.count();
|
||||||
|
m_serveur->abortClient();
|
||||||
|
QVERIFY(waitFor([this, handshakes] { return m_handshakes.count() > handshakes; }, 8000));
|
||||||
|
QCOMPARE(m_handshakes.last().value("token").toString(), secret); // le secret a bien transité
|
||||||
|
SocketScheduler *s = m_arb->socketScheduler();
|
||||||
|
QVERIFY(waitFor([s] { return s->client()->state() == OptimizerClient::Ready; }));
|
||||||
|
|
||||||
|
m_repondre = [this, secret](const QVariantMap &req) {
|
||||||
|
QVariantMap p = plan(req, {action("ecs", {{"powerW", 1000}, {"reason", "écho " + secret},
|
||||||
|
{"reasonCode", "SURPLUS_SETPOINT"},
|
||||||
|
{"reasonParams", QVariantMap{{"budgetW", secret}, {secret, 5},
|
||||||
|
{"setpointW", 1000}}}}),
|
||||||
|
action(m_var, {{"powerW", 500}, {"reasonCode", secret}})});
|
||||||
|
p.insert("planId", secret);
|
||||||
|
p.insert("strategy", secret);
|
||||||
|
p.insert("diagnostics", QVariantMap{{"status", "optimal"}, {"solver", secret}});
|
||||||
|
return p;
|
||||||
|
};
|
||||||
|
const QDateTime t0 = utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0));
|
||||||
|
cycle(t0, -3000);
|
||||||
|
cycle(t0.addSecs(60), -3000);
|
||||||
|
QVERIFY(s->optimiseurAuDernierCycle());
|
||||||
|
QCOMPARE(rpc("GetOptimizerStatus").value("name").toString(), QString("écho [token masqué]"));
|
||||||
|
rpc("GetPlan");
|
||||||
|
m_arb->loadTelemetry();
|
||||||
|
injectAndWait("NymeaEnergy.GetLoadTelemetry");
|
||||||
|
|
||||||
|
m_repondre = [secret](const QVariantMap &) { return QVariantMap{{"abstain", true}, {"reason", "écho " + secret}}; };
|
||||||
|
cycle(t0.addSecs(120), -3000);
|
||||||
|
m_repondre = [this, secret](const QVariantMap &req) {
|
||||||
|
return plan(req, {action("ecs", {{"powerW", 1000}}), action(m_var, {{"powerW", 500}}),
|
||||||
|
action(secret, {{"powerW", 1}})});
|
||||||
|
};
|
||||||
|
cycle(t0.addSecs(180), -3000);
|
||||||
|
m_serveur->sendJson(QJsonObject{{"jsonrpc", "2.0"}, {"method", secret}, {"params", QJsonObject{{secret, secret}}}});
|
||||||
|
settle(200);
|
||||||
|
cycle(t0.addSecs(240), -3000);
|
||||||
|
rpc("GetOptimizerStatus");
|
||||||
|
rpc("GetPlan");
|
||||||
|
|
||||||
|
// Le texte de l'optimiseur a bien coulé au journal — MASQUÉ.
|
||||||
|
QVERIFY2(j.texte().contains("plan [token masqué] reçu"), qUtf8Printable(j.texte().right(3000)));
|
||||||
|
QVERIFY(j.texte().contains("« écho [token masqué] »"));
|
||||||
|
QVERIFY(j.texte().contains("abstention : écho [token masqué]"));
|
||||||
|
QVERIFY(j.texte().contains("charge [token masqué] inconnue"));
|
||||||
|
QCOMPARE(j.compte("recopie le token"), 1);
|
||||||
|
// Aucune ligne du journal hors la trace brute ENTRANTE de nymea (la pose du token).
|
||||||
|
for (const QString &f : fragments) {
|
||||||
|
QStringList fuites;
|
||||||
|
for (const QString &x : j.lignes(f))
|
||||||
|
if (!x.startsWith(QLatin1String("JsonRpcTraffic: ")))
|
||||||
|
fuites << x.left(300);
|
||||||
|
QVERIFY2(fuites.isEmpty(), qUtf8Printable(fuites.join('\n')));
|
||||||
|
}
|
||||||
|
// Aucune réponse ni notification RPC (5 réponses au moins : 2 statuts, 2 plans, la télémétrie).
|
||||||
|
QVERIFY2(paquets.count() >= 5, qPrintable(QString::number(paquets.count())));
|
||||||
|
for (const QList<QVariant> &p : paquets)
|
||||||
|
for (const QString &f : fragments)
|
||||||
|
QVERIFY2(!p.last().toByteArray().contains(f.toUtf8()), p.last().toByteArray().left(300).constData());
|
||||||
|
// Aucune trace.
|
||||||
|
QVERIFY(traces(d, 5).count() >= 5);
|
||||||
|
QByteArray tout;
|
||||||
|
for (const QString &nom : fichiersTrace(d)) {
|
||||||
|
QFile f(QDir(d).filePath(nom));
|
||||||
|
QVERIFY(f.open(QIODevice::ReadOnly));
|
||||||
|
tout += f.readAll();
|
||||||
|
}
|
||||||
|
QVERIFY(tout.contains("[token masqué]"));
|
||||||
|
for (const QString &f : fragments)
|
||||||
|
QVERIFY2(!tout.contains(f.toUtf8()), qPrintable(f));
|
||||||
|
|
||||||
|
m_handshake = handshakeAvant;
|
||||||
|
QFile::remove(OptimizerClient::defaultTokenPath());
|
||||||
|
QLoggingCategory::setFilterRules(QStringLiteral(
|
||||||
|
"*.debug=false\nApplication.debug=true\nSimulation.debug=true\nExperiences.debug=true\n"
|
||||||
|
"NymeaEnergy.debug=true\nEnergyMocks.debug=true\nDBus.warning=false"));
|
||||||
|
}
|
||||||
|
|
||||||
QTEST_MAIN(TestSocketScheduler)
|
QTEST_MAIN(TestSocketScheduler)
|
||||||
#include "testsocketscheduler.moc"
|
#include "testsocketscheduler.moc"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user