Initial: Cloudflare Workers + D1 licensing server
This commit is contained in:
56
README.md
Normal file
56
README.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# cowl-licensing
|
||||||
|
|
||||||
|
Cloudflare Workers + D1 replacement for the ComponentOwl PHP/MySQL licensing server.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
cowl-licensing/
|
||||||
|
├── wrangler.toml # Worker config (fill in database_id)
|
||||||
|
├── schema.sql # D1 table definitions
|
||||||
|
├── seed.sql # Data import (637 serials, 976 licenses, 11 products)
|
||||||
|
└── src/
|
||||||
|
└── index.js # Single-file Worker — all 8 endpoints
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Create the D1 database
|
||||||
|
wrangler d1 create cowl-licensing
|
||||||
|
# Copy the database_id into wrangler.toml
|
||||||
|
|
||||||
|
# 2. Create tables
|
||||||
|
wrangler d1 execute cowl-licensing --file=schema.sql
|
||||||
|
|
||||||
|
# 3. Import data
|
||||||
|
wrangler d1 execute cowl-licensing --file=seed.sql
|
||||||
|
|
||||||
|
# 4. Deploy
|
||||||
|
wrangler deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
All endpoints mirror the original PHP API exactly (`.php` URLs preserved).
|
||||||
|
|
||||||
|
| Method | Path | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| POST | `/v1/activate.php` | Activate a license |
|
||||||
|
| GET/POST | `/v1/check_update.php` | Check for product update |
|
||||||
|
| POST | `/v1/get_licenses.php` | List licenses for a product |
|
||||||
|
| GET/POST | `/v1/get_products.php` | List all products + editions |
|
||||||
|
| POST | `/v1/get_serial_numbers.php` | List serial numbers for a product |
|
||||||
|
| POST | `/v1/keygen.php` | Generate new serial numbers |
|
||||||
|
| POST | `/v1/renew_license.php` | Renew license expiration |
|
||||||
|
| POST | `/v1/update_product.php` | Update product version |
|
||||||
|
|
||||||
|
All requests/responses use XML. Request body: `<request>...</request>`. Response: `<?xml version="1.0" encoding="utf-8"?><response>...</response>`.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Special serials `FREE` (edition 1) and `TRIAL` (edition 2) bypass the serial_numbers table
|
||||||
|
- Activation limit of `0` = unlimited activations
|
||||||
|
- Version limit of `0` = all versions allowed
|
||||||
|
- `keygen` caps at 100 keys per call; generated serials are 16-char uppercase alphanumeric (BMT Micro compatible)
|
||||||
|
- All queries use parameterized statements (SQL injection safe)
|
||||||
55
schema.sql
Normal file
55
schema.sql
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
-- cowl-licensing D1 schema
|
||||||
|
-- Run: wrangler d1 execute cowl-licensing --file=schema.sql
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS products (
|
||||||
|
bmt_id INTEGER NOT NULL,
|
||||||
|
product TEXT NOT NULL,
|
||||||
|
product_name TEXT NOT NULL,
|
||||||
|
display_name TEXT NOT NULL,
|
||||||
|
short_name TEXT NOT NULL,
|
||||||
|
edition INTEGER NOT NULL,
|
||||||
|
version TEXT NOT NULL,
|
||||||
|
limit_version INTEGER NOT NULL,
|
||||||
|
limit_activation INTEGER NOT NULL,
|
||||||
|
limit_subscription INTEGER NOT NULL,
|
||||||
|
limit_support INTEGER NOT NULL,
|
||||||
|
download_url TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (bmt_id, product)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS editions (
|
||||||
|
product TEXT NOT NULL,
|
||||||
|
edition INTEGER NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (product, edition)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS serial_numbers (
|
||||||
|
serial_number TEXT NOT NULL PRIMARY KEY,
|
||||||
|
product TEXT NOT NULL,
|
||||||
|
edition INTEGER NOT NULL,
|
||||||
|
limit_version INTEGER NOT NULL,
|
||||||
|
limit_activation INTEGER NOT NULL,
|
||||||
|
limit_subscription INTEGER NOT NULL,
|
||||||
|
limit_support INTEGER NOT NULL,
|
||||||
|
note TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS licenses (
|
||||||
|
registration_key TEXT NOT NULL,
|
||||||
|
serial_number TEXT NOT NULL,
|
||||||
|
product TEXT NOT NULL,
|
||||||
|
edition INTEGER,
|
||||||
|
version INTEGER,
|
||||||
|
timestamp INTEGER NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
notify INTEGER NOT NULL,
|
||||||
|
expiration_subscription INTEGER NOT NULL,
|
||||||
|
expiration_support INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (registration_key, serial_number, product)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_licenses_serial ON licenses(serial_number);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_licenses_product ON licenses(product);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_serial_numbers_product ON serial_numbers(product);
|
||||||
396
src/index.js
Normal file
396
src/index.js
Normal file
@@ -0,0 +1,396 @@
|
|||||||
|
// cowl-licensing - Cloudflare Workers + D1 licensing server
|
||||||
|
// Replaces PHP 7 + MySQL backend for ComponentOwl products
|
||||||
|
|
||||||
|
const XML_HEADER = '<?xml version="1.0" encoding="utf-8"?>';
|
||||||
|
|
||||||
|
function xmlResponse(content) {
|
||||||
|
return new Response(`${XML_HEADER}<response>${content}</response>`, {
|
||||||
|
headers: { 'Content-Type': 'text/xml; charset=utf-8' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function xmlError(message) {
|
||||||
|
return xmlResponse(`<error>${escXml(message)}</error>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function escXml(s) {
|
||||||
|
if (s == null) return '';
|
||||||
|
return String(s)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseField(xml, field) {
|
||||||
|
const re = new RegExp(`<${field}>([\\s\\S]*?)<\\/${field}>`);
|
||||||
|
const m = xml.match(re);
|
||||||
|
return m ? m[1].trim() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBody(request) {
|
||||||
|
try { return await request.text(); } catch { return ''; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a BMT Micro-style 16-char uppercase alphanumeric serial
|
||||||
|
function generateSerial() {
|
||||||
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
||||||
|
return Array.from(bytes, b => chars[b % chars.length]).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Handlers ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function handleActivate(request, env) {
|
||||||
|
const body = await getBody(request);
|
||||||
|
|
||||||
|
const serial_number = parseField(body, 'serial_number');
|
||||||
|
const product = parseField(body, 'product');
|
||||||
|
const version = parseField(body, 'version');
|
||||||
|
const reg_key = parseField(body, 'registration_key');
|
||||||
|
const name = parseField(body, 'name') ?? '';
|
||||||
|
const email = parseField(body, 'email') ?? '';
|
||||||
|
|
||||||
|
if (!serial_number || !product || !reg_key) {
|
||||||
|
return xmlError('Missing required fields');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate product exists
|
||||||
|
const prod = await env.DB.prepare(
|
||||||
|
'SELECT * FROM products WHERE product = ? LIMIT 1'
|
||||||
|
).bind(product).first();
|
||||||
|
if (!prod) return xmlError('Product not found');
|
||||||
|
|
||||||
|
let edition, limit_version, limit_activation, limit_subscription, limit_support;
|
||||||
|
|
||||||
|
// Special serials
|
||||||
|
if (serial_number === 'FREE') {
|
||||||
|
edition = 1; limit_version = 0; limit_activation = 0;
|
||||||
|
limit_subscription = 86400; limit_support = 86400;
|
||||||
|
} else if (serial_number === 'TRIAL') {
|
||||||
|
edition = 2; limit_version = 0; limit_activation = 0;
|
||||||
|
limit_subscription = 86400; limit_support = 86400;
|
||||||
|
} else {
|
||||||
|
const sn = await env.DB.prepare(
|
||||||
|
'SELECT * FROM serial_numbers WHERE serial_number = ? AND product = ?'
|
||||||
|
).bind(serial_number, product).first();
|
||||||
|
if (!sn) return xmlError('Serial number not found');
|
||||||
|
|
||||||
|
edition = sn.edition;
|
||||||
|
limit_version = sn.limit_version;
|
||||||
|
limit_activation = sn.limit_activation;
|
||||||
|
limit_subscription = sn.limit_subscription;
|
||||||
|
limit_support = sn.limit_support;
|
||||||
|
|
||||||
|
// Check activation limit (0 = unlimited)
|
||||||
|
if (limit_activation > 0) {
|
||||||
|
const { count } = await env.DB.prepare(
|
||||||
|
'SELECT COUNT(*) as count FROM licenses WHERE serial_number = ? AND product = ?'
|
||||||
|
).bind(serial_number, product).first();
|
||||||
|
// Allow re-activation of same reg_key
|
||||||
|
const existing = await env.DB.prepare(
|
||||||
|
'SELECT 1 FROM licenses WHERE registration_key = ? AND serial_number = ? AND product = ?'
|
||||||
|
).bind(reg_key, serial_number, product).first();
|
||||||
|
if (!existing && count >= limit_activation) {
|
||||||
|
return xmlError('Activation limit reached');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check version limit (0 = unlimited)
|
||||||
|
if (limit_version > 0 && version) {
|
||||||
|
const reqVer = parseInt(version.split('.')[0], 10) || 0;
|
||||||
|
if (reqVer > limit_version) return xmlError('Version not licensed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const exp_sub = now + limit_subscription;
|
||||||
|
const exp_supp = now + limit_support;
|
||||||
|
const ver_int = version ? (parseInt(version.split('.')[0], 10) || 0) : 0;
|
||||||
|
|
||||||
|
// Upsert license
|
||||||
|
await env.DB.prepare(`
|
||||||
|
INSERT INTO licenses
|
||||||
|
(registration_key, serial_number, product, edition, version, timestamp,
|
||||||
|
name, email, notify, expiration_subscription, expiration_support)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
|
||||||
|
ON CONFLICT(registration_key, serial_number, product) DO UPDATE SET
|
||||||
|
edition = excluded.edition,
|
||||||
|
version = excluded.version,
|
||||||
|
timestamp = excluded.timestamp,
|
||||||
|
name = excluded.name,
|
||||||
|
email = excluded.email,
|
||||||
|
expiration_subscription = excluded.expiration_subscription,
|
||||||
|
expiration_support = excluded.expiration_support
|
||||||
|
`).bind(reg_key, serial_number, product, edition, ver_int, now,
|
||||||
|
name, email, exp_sub, exp_supp).run();
|
||||||
|
|
||||||
|
return xmlResponse(
|
||||||
|
`<serial_number>${escXml(serial_number)}</serial_number>` +
|
||||||
|
`<product>${escXml(product)}</product>` +
|
||||||
|
`<edition>${edition}</edition>` +
|
||||||
|
`<version>${escXml(version ?? '')}</version>` +
|
||||||
|
`<registration_key>${escXml(reg_key)}</registration_key>` +
|
||||||
|
`<timestamp>${now}</timestamp>` +
|
||||||
|
`<name>${escXml(name)}</name>` +
|
||||||
|
`<email>${escXml(email)}</email>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCheckUpdate(request, env) {
|
||||||
|
let product = null;
|
||||||
|
if (request.method === 'POST') {
|
||||||
|
const body = await getBody(request);
|
||||||
|
product = parseField(body, 'product');
|
||||||
|
} else {
|
||||||
|
product = new URL(request.url).searchParams.get('product');
|
||||||
|
}
|
||||||
|
if (!product) return xmlError('Missing product');
|
||||||
|
|
||||||
|
const row = await env.DB.prepare(
|
||||||
|
'SELECT version, download_url FROM products WHERE product = ? LIMIT 1'
|
||||||
|
).bind(product).first();
|
||||||
|
if (!row) return xmlError('Product not found');
|
||||||
|
|
||||||
|
return xmlResponse(
|
||||||
|
`<version>${escXml(row.version)}</version>` +
|
||||||
|
`<download_url>${escXml(row.download_url)}</download_url>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGetLicenses(request, env) {
|
||||||
|
const body = await getBody(request);
|
||||||
|
const product = parseField(body, 'product');
|
||||||
|
if (!product) return xmlError('Missing product');
|
||||||
|
|
||||||
|
const { results } = await env.DB.prepare(
|
||||||
|
'SELECT * FROM licenses WHERE product = ? ORDER BY timestamp DESC'
|
||||||
|
).bind(product).all();
|
||||||
|
|
||||||
|
const items = results.map(r =>
|
||||||
|
`<license>` +
|
||||||
|
`<registration_key>${escXml(r.registration_key)}</registration_key>` +
|
||||||
|
`<serial_number>${escXml(r.serial_number)}</serial_number>` +
|
||||||
|
`<product>${escXml(r.product)}</product>` +
|
||||||
|
`<edition>${r.edition ?? ''}</edition>` +
|
||||||
|
`<version>${r.version ?? ''}</version>` +
|
||||||
|
`<timestamp>${r.timestamp}</timestamp>` +
|
||||||
|
`<name>${escXml(r.name)}</name>` +
|
||||||
|
`<email>${escXml(r.email)}</email>` +
|
||||||
|
`<notify>${r.notify}</notify>` +
|
||||||
|
`<expiration_subscription>${r.expiration_subscription}</expiration_subscription>` +
|
||||||
|
`<expiration_support>${r.expiration_support}</expiration_support>` +
|
||||||
|
`</license>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
return xmlResponse(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGetProducts(request, env) {
|
||||||
|
const { results: prods } = await env.DB.prepare(
|
||||||
|
'SELECT * FROM products ORDER BY product, bmt_id'
|
||||||
|
).all();
|
||||||
|
const { results: eds } = await env.DB.prepare(
|
||||||
|
'SELECT * FROM editions ORDER BY product, edition'
|
||||||
|
).all();
|
||||||
|
|
||||||
|
// Group editions by product
|
||||||
|
const edMap = {};
|
||||||
|
for (const e of eds) {
|
||||||
|
if (!edMap[e.product]) edMap[e.product] = [];
|
||||||
|
edMap[e.product].push(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = prods.map(p => {
|
||||||
|
const productEds = (edMap[p.product] || []).map(e =>
|
||||||
|
`<edition><id>${e.edition}</id><name>${escXml(e.name)}</name></edition>`
|
||||||
|
).join('');
|
||||||
|
return `<product>` +
|
||||||
|
`<bmt_id>${p.bmt_id}</bmt_id>` +
|
||||||
|
`<product>${escXml(p.product)}</product>` +
|
||||||
|
`<product_name>${escXml(p.product_name)}</product_name>` +
|
||||||
|
`<display_name>${escXml(p.display_name)}</display_name>` +
|
||||||
|
`<short_name>${escXml(p.short_name)}</short_name>` +
|
||||||
|
`<edition>${p.edition}</edition>` +
|
||||||
|
`<version>${escXml(p.version)}</version>` +
|
||||||
|
`<limit_version>${p.limit_version}</limit_version>` +
|
||||||
|
`<limit_activation>${p.limit_activation}</limit_activation>` +
|
||||||
|
`<limit_subscription>${p.limit_subscription}</limit_subscription>` +
|
||||||
|
`<limit_support>${p.limit_support}</limit_support>` +
|
||||||
|
`<download_url>${escXml(p.download_url)}</download_url>` +
|
||||||
|
`<editions>${productEds}</editions>` +
|
||||||
|
`</product>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
return xmlResponse(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGetSerialNumbers(request, env) {
|
||||||
|
const body = await getBody(request);
|
||||||
|
const product = parseField(body, 'product');
|
||||||
|
if (!product) return xmlError('Missing product');
|
||||||
|
|
||||||
|
const { results } = await env.DB.prepare(
|
||||||
|
'SELECT * FROM serial_numbers WHERE product = ? ORDER BY serial_number'
|
||||||
|
).bind(product).all();
|
||||||
|
|
||||||
|
const items = results.map(r =>
|
||||||
|
`<serial_number>` +
|
||||||
|
`<serial_number>${escXml(r.serial_number)}</serial_number>` +
|
||||||
|
`<product>${escXml(r.product)}</product>` +
|
||||||
|
`<edition>${r.edition}</edition>` +
|
||||||
|
`<limit_version>${r.limit_version}</limit_version>` +
|
||||||
|
`<limit_activation>${r.limit_activation}</limit_activation>` +
|
||||||
|
`<limit_subscription>${r.limit_subscription}</limit_subscription>` +
|
||||||
|
`<limit_support>${r.limit_support}</limit_support>` +
|
||||||
|
`<note>${escXml(r.note ?? '')}</note>` +
|
||||||
|
`</serial_number>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
return xmlResponse(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleKeygen(request, env) {
|
||||||
|
const body = await getBody(request);
|
||||||
|
|
||||||
|
const keycount = parseInt(parseField(body, 'keycount') ?? '1', 10);
|
||||||
|
const productid = parseField(body, 'productid');
|
||||||
|
const note = parseField(body, 'note') ?? '';
|
||||||
|
|
||||||
|
// Serial config — can be overridden per-request or derived from product
|
||||||
|
let edition = parseInt(parseField(body, 'edition') ?? '3', 10);
|
||||||
|
let limit_version = parseInt(parseField(body, 'limit_version') ?? '0', 10);
|
||||||
|
let limit_activation = parseInt(parseField(body, 'limit_activation') ?? '3', 10);
|
||||||
|
let limit_subscription = parseInt(parseField(body, 'limit_subscription') ?? '86400', 10);
|
||||||
|
let limit_support = parseInt(parseField(body, 'limit_support') ?? '86400', 10);
|
||||||
|
|
||||||
|
// If productid given, look up defaults from products table
|
||||||
|
if (productid) {
|
||||||
|
const prod = await env.DB.prepare(
|
||||||
|
'SELECT * FROM products WHERE product = ? LIMIT 1'
|
||||||
|
).bind(productid).first();
|
||||||
|
if (prod) {
|
||||||
|
edition = prod.edition;
|
||||||
|
limit_version = prod.limit_version;
|
||||||
|
limit_activation = prod.limit_activation;
|
||||||
|
limit_subscription = prod.limit_subscription;
|
||||||
|
limit_support = prod.limit_support;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const product = productid ?? parseField(body, 'product') ?? '';
|
||||||
|
const count = Math.min(Math.max(keycount, 1), 100); // cap at 100
|
||||||
|
|
||||||
|
const generated = [];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
let serial;
|
||||||
|
// Retry on collision (extremely unlikely)
|
||||||
|
for (let attempt = 0; attempt < 5; attempt++) {
|
||||||
|
serial = generateSerial();
|
||||||
|
const existing = await env.DB.prepare(
|
||||||
|
'SELECT 1 FROM serial_numbers WHERE serial_number = ?'
|
||||||
|
).bind(serial).first();
|
||||||
|
if (!existing) break;
|
||||||
|
}
|
||||||
|
if (product) {
|
||||||
|
await env.DB.prepare(`
|
||||||
|
INSERT OR IGNORE INTO serial_numbers
|
||||||
|
(serial_number, product, edition, limit_version, limit_activation,
|
||||||
|
limit_subscription, limit_support, note)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`).bind(serial, product, edition, limit_version, limit_activation,
|
||||||
|
limit_subscription, limit_support, note || null).run();
|
||||||
|
}
|
||||||
|
generated.push(serial);
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = generated.map(s =>
|
||||||
|
`<keydata><serial_number>${escXml(s)}</serial_number></keydata>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
return xmlResponse(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRenewLicense(request, env) {
|
||||||
|
const body = await getBody(request);
|
||||||
|
|
||||||
|
const serial_number = parseField(body, 'serial_number');
|
||||||
|
const expiration_subscription = parseField(body, 'expiration_subscription');
|
||||||
|
const expiration_support = parseField(body, 'expiration_support');
|
||||||
|
|
||||||
|
if (!serial_number) return xmlError('Missing serial_number');
|
||||||
|
|
||||||
|
const exp_sub = expiration_subscription != null ? parseInt(expiration_subscription, 10) : null;
|
||||||
|
const exp_supp = expiration_support != null ? parseInt(expiration_support, 10) : null;
|
||||||
|
|
||||||
|
if (exp_sub != null && exp_supp != null) {
|
||||||
|
await env.DB.prepare(`
|
||||||
|
UPDATE licenses SET
|
||||||
|
expiration_subscription = ?,
|
||||||
|
expiration_support = ?
|
||||||
|
WHERE serial_number = ?
|
||||||
|
`).bind(exp_sub, exp_supp, serial_number).run();
|
||||||
|
} else if (exp_sub != null) {
|
||||||
|
await env.DB.prepare(
|
||||||
|
'UPDATE licenses SET expiration_subscription = ? WHERE serial_number = ?'
|
||||||
|
).bind(exp_sub, serial_number).run();
|
||||||
|
} else if (exp_supp != null) {
|
||||||
|
await env.DB.prepare(
|
||||||
|
'UPDATE licenses SET expiration_support = ? WHERE serial_number = ?'
|
||||||
|
).bind(exp_supp, serial_number).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
return xmlResponse(
|
||||||
|
`<serial_number>${escXml(serial_number)}</serial_number>` +
|
||||||
|
`<expiration_subscription>${exp_sub ?? ''}</expiration_subscription>` +
|
||||||
|
`<expiration_support>${exp_supp ?? ''}</expiration_support>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUpdateProduct(request, env) {
|
||||||
|
const body = await getBody(request);
|
||||||
|
|
||||||
|
const product = parseField(body, 'product');
|
||||||
|
const version = parseField(body, 'version');
|
||||||
|
|
||||||
|
if (!product || !version) return xmlError('Missing product or version');
|
||||||
|
|
||||||
|
await env.DB.prepare(
|
||||||
|
'UPDATE products SET version = ? WHERE product = ?'
|
||||||
|
).bind(version, product).run();
|
||||||
|
|
||||||
|
return xmlResponse(
|
||||||
|
`<product>${escXml(product)}</product>` +
|
||||||
|
`<version>${escXml(version)}</version>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Router ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const ROUTES = {
|
||||||
|
'/v1/activate.php': handleActivate,
|
||||||
|
'/v1/check_update.php': handleCheckUpdate,
|
||||||
|
'/v1/get_licenses.php': handleGetLicenses,
|
||||||
|
'/v1/get_products.php': handleGetProducts,
|
||||||
|
'/v1/get_serial_numbers.php': handleGetSerialNumbers,
|
||||||
|
'/v1/keygen.php': handleKeygen,
|
||||||
|
'/v1/renew_license.php': handleRenewLicense,
|
||||||
|
'/v1/update_product.php': handleUpdateProduct,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async fetch(request, env) {
|
||||||
|
const path = new URL(request.url).pathname;
|
||||||
|
const handler = ROUTES[path];
|
||||||
|
if (!handler) {
|
||||||
|
return new Response('Not Found', { status: 404 });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return await handler(request, env);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return xmlError('Internal server error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
8
wrangler.toml
Normal file
8
wrangler.toml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
name = "cowl-licensing"
|
||||||
|
main = "src/index.js"
|
||||||
|
compatibility_date = "2024-01-01"
|
||||||
|
|
||||||
|
[[d1_databases]]
|
||||||
|
binding = "DB"
|
||||||
|
database_name = "cowl-licensing"
|
||||||
|
database_id = "TO_BE_FILLED"
|
||||||
Reference in New Issue
Block a user