Il file che scarichi è generato per un prodotto. La classe, il nome del file e il file di cache portano tutti un nome derivato da quel prodotto, e l’indirizzo chiamato viene assemblato a runtime anziché scritto in un pezzo solo. Due dei tuoi stessi prodotti non condividono un solo identificativo.
Siamo chiari su cosa ti dà. Chi apre i tuoi file e li legge trova il controllo in due minuti: questo non è cambiato e non cambierà mai. Quello che ferma è l’altra cosa: uno script che cerca un nome di classe noto in migliaia di prodotti e lo toglie da solo. È così che le copie nulled si producono in serie, e qui non funziona più. Il costo di piratare il tuo prodotto passa da niente a un esame manuale, prodotto per prodotto.
Gli esempi di questa pagina chiamano la classe Licence. Nel tuo download porta il suo vero nome, mostrato accanto al pulsante di download nella tua area autore.
Apri quello che corrisponde al tuo prodotto. Ogni blocco è completo e pronto da incollare; rinomina monmodule e adatta la conservazione della chiave alle tue impostazioni. Gli ambienti PHP usano la libreria scaricabile; gli altri mostrano il client completo, cache e verifica della firma incluse.
La forma più breve. Metti il controllo all’ingresso di ciò che proteggi: un front controller, una pagina di amministrazione, uno script pianificato.
require_once __DIR__ . '/Licence.php';
$licence = new Licence(
$config['licence'],
__DIR__ . '/cache'
);
if (!$licence->valide()) {
http_response_code(403);
exit($licence->message());
}
Tieni una sola istanza per l’intero modulo. Controlla in getContent() perché il commerciante veda il motivo del rifiuto nella schermata di configurazione, e anche negli hook di visualizzazione perché un modulo configurato a metà non compaia in negozio.
require_once dirname(__FILE__) . '/Licence.php';
class MonModule extends Module
{
private $licence = null;
private function licence()
{
if ($this->licence === null) {
$this->licence = new Licence(
Configuration::get('MONMODULE_LICENCE'),
_PS_CACHE_DIR_ . 'monmodule'
);
}
return $this->licence;
}
public function getContent()
{
if (Tools::isSubmit('submitLicence')) {
Configuration::updateValue('MONMODULE_LICENCE', Tools::getValue('licence'));
}
$l = $this->licence();
if (!$l->valide()) {
return $this->displayError($l->message()) . $this->formulaireLicence();
}
if ($l->essai()) {
return $this->displayWarning($this->l('Development licence')) . $this->contenu();
}
return $this->contenu();
}
public function hookDisplayHeader()
{
if (!$this->licence()->valide()) {
return '';
}
return $this->contenuFront();
}
}
Verificato su 1.7, 8 e 9. Le chiamate usate — Module, Configuration, Tools, _PS_CACHE_DIR_ — hanno attraversato le rimozioni della 9.0 senza essere toccate. Le deprecazioni annunciate per la 10.0 riguardano i controller di amministrazione, che questo esempio non usa.
Un’istanza statica in una funzione, un avviso in amministrazione e il corpo del plugin dietro il controllo. Non eseguire il controllo sul front di ogni pagina: la cache lo rende economico, ma un avviso sta in amministrazione.
require_once __DIR__ . '/Licence.php';
function monplug_licence() {
static $l = null;
if ($l === null) {
$l = new Licence(
get_option('monplug_licence'),
WP_CONTENT_DIR . '/cache/monplug'
);
}
return $l;
}
add_action('admin_notices', function () {
$l = monplug_licence();
if (!$l->valide()) {
printf('<div class="notice notice-error"><p>%s</p></div>', esc_html($l->message()));
} elseif ($l->essai()) {
echo '<div class="notice notice-warning"><p>'
. esc_html__('Development licence', 'monplug') . '</p></div>';
}
});
add_action('init', function () {
if (monplug_licence()->valide()) {
monplug_demarrer();
}
});
Le cinque chiamate usate — add_action, add_filter, get_option, esc_html, WP_CONTENT_DIR — non sono cambiate da WordPress 5.0 e funzionano ancora sul ramo 7.1. Niente qui dipende dall’editor a blocchi o da una rotta REST.
Per WooCommerce, condiziona ciò che aggiungi al negozio anziché l’intero plugin: un metodo di pagamento che sparisce è più chiaro per il commerciante di un plugin che muore in silenzio.
add_filter('woocommerce_payment_gateways', function ($passerelles) {
if (monplug_licence()->valide()) {
$passerelles[] = 'WC_Gateway_MonPlug';
}
return $passerelles;
});
add_action('woocommerce_admin_field_monplug_licence', function () {
$l = monplug_licence();
if (!$l->valide()) {
echo '<div class="error inline"><p>' . esc_html($l->message()) . '</p></div>';
}
});
Avvolgi la libreria in un model e iniettalo dove serve. La cartella di cache deve essere quella di Magento, altrimenti un deploy cancellerà il tuo verdetto a ogni rilascio.
namespace Editeur\MonModule\Model;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\Filesystem\DirectoryList;
class Licence
{
private $licence;
public function __construct(ScopeConfigInterface $config, DirectoryList $dossiers)
{
$this->licence = new \Licence(
(string) $config->getValue('monmodule/general/licence'),
$dossiers->getPath(DirectoryList::CACHE) . '/monmodule'
);
}
public function valide(): bool
{
return $this->licence->valide();
}
public function message(): string
{
return $this->licence->message();
}
}
La libreria non ha namespace: dichiarala nel composer.json del tuo modulo invece di chiamare require_once a mano.
{
"autoload": {
"files": [
"Licence.php"
]
}
}
Un middleware è il posto giusto: registralo sul gruppo di rotte che appartiene al tuo prodotto, mai globalmente, così un problema di licenza non travolge mai il resto dell’applicazione.
namespace App\Licence;
use Closure;
use Illuminate\Http\Request;
class VerifieLicence
{
private $licence;
public function __construct()
{
$this->licence = new \Licence(
config('monmodule.licence'),
storage_path('app/monmodule')
);
}
public function handle(Request $requete, Closure $suivant)
{
if (!$this->licence->valide()) {
abort(403, $this->licence->message());
}
return $suivant($requete);
}
}
Un event subscriber su kernel.request, che salta le sotto-richieste. Passa la chiave e la cartella di cache come argomenti nella definizione del servizio anziché leggere la configurazione dentro la classe.
namespace App\Licence;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
class LicenceSubscriber implements EventSubscriberInterface
{
private $licence;
public function __construct(string $cle, string $dossierCache)
{
$this->licence = new \Licence($cle, $dossierCache);
}
public function onKernelRequest(RequestEvent $evenement): void
{
if (!$evenement->isMainRequest()) {
return;
}
if (!$this->licence->valide()) {
throw new AccessDeniedHttpException($this->licence->message());
}
}
public static function getSubscribedEvents(): array
{
return [KernelEvents::REQUEST => ['onKernelRequest', 16]];
}
}
isMainRequest() è arrivata in Symfony 5.3. Su una versione precedente chiama isMasterRequest(): tutto il resto è identico.
Nessuna dipendenza oltre alla libreria standard. Questo è il client completo: risposta firmata, cache legata al proprio host, riutilizzo per 24 ore e proroga di 30 giorni, le stesse regole della libreria PHP.
import { createPublicKey, verify } from 'node:crypto';
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { dirname } from 'node:path';
const API = 'https://addonsmarket.com/api/licence';
const PUBLIQUE = '/PTkKQpweo9XCLOS0+b0vgrecx7Ly/SsAuwTiA737Tw=';
const SURSIS = 2592000;
const publique = createPublicKey({
key: Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'),
Buffer.from(PUBLIQUE, 'base64')]),
format: 'der',
type: 'spki',
});
const canonique = (d) =>
JSON.stringify(Object.fromEntries(Object.keys(d).sort().map((k) => [k, d[k]])));
const signee = (rep, hote) =>
rep && rep.donnees && rep.signature
&& rep.donnees.hote === hote
&& verify(null, Buffer.from(canonique(rep.donnees), 'utf8'), publique,
Buffer.from(rep.signature, 'base64'));
export async function etat(cle, hote, fichierCache) {
const maintenant = Math.floor(Date.now() / 1000);
let cache = null;
try {
const lu = JSON.parse(await readFile(fichierCache, 'utf8'));
if (signee(lu, hote)) cache = lu;
} catch {}
if (cache && cache.donnees.revalider > maintenant) return cache.donnees;
try {
const r = await fetch(API, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'User-Agent': 'MonProduit/1.0' },
body: JSON.stringify({ cle, url: hote }),
signal: AbortSignal.timeout(6000),
});
if (!r.ok) throw new Error(String(r.status));
const rep = await r.json();
if (!signee(rep, hote)) throw new Error('signature');
await mkdir(dirname(fichierCache), { recursive: true });
await writeFile(fichierCache, JSON.stringify(rep));
return rep.donnees;
} catch {
if (cache && cache.donnees.emis + SURSIS > maintenant) return cache.donnees;
return { ok: false, motif: 'reseau' };
}
}
Node 18 o successivo: fetch è diventato globale nella 18, e AbortSignal.timeout nella 17.3. Su un runtime precedente sostituisci questi due con https.request e un tuo timer.
Poi chiamalo una volta per richiesta, e non bloccare mai per un guasto di rete.
import { etat } from './licence.mjs';
const d = await etat(process.env.LICENCE, req.hostname, './var/licence.json');
if (!d.ok && d.motif !== 'reseau') {
return res.status(403).json({ erreur: d.motif });
}
Non esiste un modo onesto di verificare una licenza in un browser. Quello che scrivi gira sulla macchina dell’acquirente, in codice che può leggere, e toglierlo costa una riga negli strumenti per sviluppatori. Non forniamo una libreria front perché sarebbe venderti un placebo.
Se il tuo prodotto ha un server, controlla lì ed esponi il verdetto come dato. L’interfaccia potrà dire qualcosa di utile senza che il controllo stia nel browser.
app.get('/api/etat', async (req, res) => {
const d = await etat(process.env.LICENCE, req.hostname, './var/licence.json');
res.json({ actif: d.ok, essai: d.essai });
});
const [actif, setActif] = useState(null);
useEffect(() => {
fetch('/api/etat')
.then((r) => r.json())
.then((d) => setActif(d.actif));
}, []);
if (actif === false) {
return <Bandeau>Licence inactive</Bandeau>;
}
Se il tuo prodotto è un template front senza alcun server, non c’è nulla da verificare a runtime e non bisogna fingere il contrario. La licenza si applica dove può: al download e tramite le condizioni che il tuo acquirente ha accettato.
Un’app Shopify ha un proprio server: usa lì il client Node o PHP e invia il dominio del negozio, l’indirizzo myshopify.com oppure il dominio personalizzato se è su quello che ti basi. Un tema Shopify non ha server, quindi vale la sezione front qui sopra.
Libreria standard più cryptography per la firma. Le stesse regole di sempre: riutilizzo per 24 ore, conservazione dell’ultimo verdetto per 30 giorni se taciamo, blocco solo per un rifiuto firmato.
import base64, json, time, urllib.request
from pathlib import Path
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
API = 'https://addonsmarket.com/api/licence'
PUBLIQUE = '/PTkKQpweo9XCLOS0+b0vgrecx7Ly/SsAuwTiA737Tw='
SURSIS = 2592000
_publique = Ed25519PublicKey.from_public_bytes(base64.b64decode(PUBLIQUE))
def _canonique(d):
return json.dumps(d, sort_keys=True, separators=(',', ':'), ensure_ascii=False)
def _signee(rep, hote):
try:
if rep['donnees']['hote'] != hote:
return False
_publique.verify(base64.b64decode(rep['signature']),
_canonique(rep['donnees']).encode('utf-8'))
return True
except (KeyError, TypeError, ValueError, InvalidSignature):
return False
def etat(cle, hote, fichier_cache):
maintenant = int(time.time())
chemin = Path(fichier_cache)
cache = None
try:
lu = json.loads(chemin.read_text(encoding='utf-8'))
if _signee(lu, hote):
cache = lu
except (OSError, ValueError):
pass
if cache and cache['donnees']['revalider'] > maintenant:
return cache['donnees']
try:
corps = json.dumps({'cle': cle, 'url': hote}).encode('utf-8')
requete = urllib.request.Request(API, data=corps, headers={
'Content-Type': 'application/json',
'User-Agent': 'MonProduit/1.0',
})
with urllib.request.urlopen(requete, timeout=6) as r:
rep = json.loads(r.read().decode('utf-8'))
if not _signee(rep, hote):
raise ValueError('signature')
chemin.parent.mkdir(parents=True, exist_ok=True)
chemin.write_text(json.dumps(rep), encoding='utf-8')
return rep['donnees']
except Exception:
if cache and cache['donnees']['emis'] + SURSIS > maintenant:
return cache['donnees']
return {'ok': False, 'motif': 'reseau'}
Per tutto il resto: Go, Ruby, .NET, Java, uno script di shell. Un POST, un corpo JSON. I campi sono nella sezione API più sotto, e ciò che devi verificare prima di fidarti della risposta nella sezione sulla firma.
curl -X POST https://addonsmarket.com/api/licence \
-H 'Content-Type: application/json' \
-H 'User-Agent: MonProduit/1.0' \
-d '{"cle":"VOTRE_CLE","url":"boutique.exemple.fr"}'
Scrivici dalla tua area autore. Una domanda che è stato necessario porre significa di solito che a questa pagina manca un paragrafo.