flupydev.ru · license gateway v2.4

Лицензия на плагин?
Проверим за 60 мс.

Плагин при запуске шлёт запрос на flupydev.ru/api/v1/license/check — сервер проверяет ключ, подписывает ответ по HMAC и говорит: запускать или гасить. Всё.

12 847
проверок сегодня
62ms
средний отклик
99.98%
uptime / 30 дней
3 214
ключей выдано
flupy@gate: ~/license — ssh
// 01

Проверка лицензии

Вбей ключ в формате FLPY-XXXX-XXXX-XXXX и получи ровно тот же ответ, который улетает плагину при старте сервера.

# отпечаток ядра сервера · ключ можно привязать к железу
// демо-ключи — клик подставит в форму
* демо-режим: ответы генерируются в браузере. Боевой эндпоинт и код сервера — в разделах API и Деплой.
RESPONSE · application/json ready
# консоль проверки — введи ключ слева и жми «Запросить»
// 02

Как это работает

Полный цикл от onEnable() до запуска плагина — меньше ста миллисекунд.

ШАГ 01

Запуск сервера

Ядро грузит плагин, тот читает ключ из config.yml и считает HWID.

--?
ШАГ 02

HTTPS-запрос

GET на flupydev.ru/api/v1/license/check с key, hwid и версией плагина.

--?
ШАГ 03

Проверка в БД

Сервер ищет ключ: срок, статус, привязка к железу, лимит серверов.

--?
ШАГ 04

Подпись HMAC

Ответ подписывается SHA-256 — подделать его по дороге невозможно.

--?
ШАГ 05

Вердикт

valid=true > плагин стартует. Иначе — лок и сообщение в консоль.

?Защита подписью

Каждый ответ подписан HMAC-SHA256. Плагин сверяет подпись локально — эмуляция ответа через hosts не прокатит.

?Привязка HWID

Ключ опционально биндится к железу: утащил конфиг на другой сервер — 403.

?Grace-режим

Если API лежит (5xx), плагин работает кэш-режим до 6 часов — легалы не страдают.

// 03

Документация API

Один эндпоинт, один метод, никаких токенов со стороны плагина — только сам ключ.

GET https://flupydev.ru/api/v1/license/check
// параметры запроса
ПараметрОписание
key reqЛицензионный ключ вида FLPY-XXXX-XXXX-XXXX
hwid optОтпечаток сервера. Если ключ привязан и не совпадёт — 403
plugin optИмя плагина — для логов и статистики
build optВерсия плагина — для контроля минимальной версии
// ответ 200 · лицензия активна
{
  "status": "ok",
  "valid": true,
  "product": "FlupyCore Pro",
  "owner": "Steve_2012",
  "expires_at": "2027-12-31T23:59:59Z",
  "max_servers": 1,
  "hwid_bind": "A1B2-C3D4-E5F6",
  "signature": "hmac:a3f9c2…e81d"
}
200Лицензия активна запускаем плагин
403KEY_REVOKED / HWID_MISMATCH ключ отозван или чужое железо
410KEY_EXPIRED срок подписки истёк
404NOT_FOUND такого ключа нет в базе
400INVALID_FORMAT ключ не похож на ключ
429RATE_LIMITED >5 запросов / 5 сек с одного IP
# проверка ключа
curl -s "https://flupydev.ru/api/v1/license/check?key=FLPY-1111-2222-3333&hwid=A1B2-C3D4-E5F6&plugin=FlupyCore" | jq .

# > 200 OK
{
  "status": "ok",
  "valid": true,
  "product": "FlupyCore Pro",
  "expires_at": "2027-12-31T23:59:59Z",
  "signature": "hmac:a3f9c2…e81d"
}

# отозванный ключ > 403 { "valid": false, "reason": "KEY_REVOKED" }
// onEnable() твоего плагина
@Override
public void onEnable() {
    Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
        try {
            String key  = getConfig().getString("license.key");
            String hwid = LicenseUtil.fingerprint();  // HWID сервера

            URL url = new URL("https://flupydev.ru/api/v1/license/check"
                + "?key="  + URLEncoder.encode(key, "UTF-8")
                + "&hwid=" + hwid + "&plugin=" + getName());

            HttpURLConnection c = (HttpURLConnection) url.openConnection();
            c.setConnectTimeout(4000);
            c.setReadTimeout(4000);

            if (c.getResponseCode() != 200) { lock("HTTP " + c.getResponseCode()); return; }

            JsonObject r = JsonParser.parseReader(new InputStreamReader(
                c.getInputStream(), StandardCharsets.UTF_8)).getAsJsonObject();

            if (!r.get("valid").getAsBoolean()) { lock(r.get("reason").getAsString()); return; }
            if (!LicenseUtil.verifySign(r))      { lock("BAD_SIGNATURE"); return; }

            Bukkit.getScheduler().runTask(this, () ->
                getLogger().info("Лицензия активна — запуск."));
        } catch (Exception e) { graceMode(); }  // API лежит > кэш-режим
    });
}
<?php // api/v1/license/check/index.php — минимальный шлюз
require 'db.php';
header('Content-Type: application/json');

$key  = strtoupper($_GET['key']  ?? '');
$hwid = strtoupper($_GET['hwid'] ?? '');

if (!preg_match('/^FLPY-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/', $key))
    die(fail(400, 'INVALID_FORMAT'));

$st = $pdo->prepare('SELECT * FROM licenses WHERE `key` = ?');
$st->execute([$key]);
if (!$row = $st->fetch()) die(fail(404, 'NOT_FOUND'));
if ($row['revoked'])        die(fail(403, 'KEY_REVOKED'));
if (time() > strtotime($row['expires_at'])) die(fail(410, 'KEY_EXPIRED'));
if ($row['hwid'] && $hwid !== $row['hwid']) die(fail(403, 'HWID_MISMATCH'));

$data = ['status'=>'ok','valid'=>true,'product'=>$row['product'],'expires_at'=>$row['expires_at']];
$data['signature'] = 'hmac:'.hash_hmac('sha256', json_encode($data), $SECRET);
echo json_encode($data);
// 04

Поднять у себя

Хочешь свой гейтвей вместо общего — весь серверный слой укладывается в один файл и одну таблицу.

  1. Стек

    Nginx + PHP 8.3-fpm (или Node 20), обязательно TLS. Хватит самого дешёвого VPS — нагрузка копеечная.

  2. Таблица licenses

    Поля: key, product, owner, hwid, expires_at, revoked, max_servers. Индекс по key.

  3. Секрет подписи

    HMAC_SECRET живёт только в .env на сервере. В плагин зашивается тот же секрет — им сверяется подпись ответа.

  4. Cron

    Раз в час помечать истёкшие, слать алерты в ТГ при всплеске 403 — кто-то гоняет отозванный ключ.

  5. Grace на стороне плагина

    Таймаут 3–5 сек, при 5xx — кэш-режим до 6 часов. Легальные юзеры не должны лежать вместе с твоим сервером.

// CHEAT-SHEET
? проверяй формат ключа на входе
? логируй каждый чек: ip, key, plugin
? rate-limit: 5 req / 5 sec / IP
? ответ < 150 мс, иначе плагин висит
? не отдавай ключи по POST без TLS
? не храни секрет в коде плагина открыто
? не блокируй старт сервера при 5xx