#if !defined _lh_license_core_included #define _lh_license_core_included #include #include #include #include /* REQUIRED per-plugin defines (put in your .sma before include): #define LH_LIC_FOLDER #define LH_LIC_TAG #define LH_LIC_PLUGINID OPTIONAL: #define LH_LIC_PORT_CVAR "port" // default */ #ifndef LH_LIC_PORT_CVAR #define LH_LIC_PORT_CVAR "port" #endif #if !defined LH_LIC_FOLDER #error LH_LIC_FOLDER is required before including lh_license_core.inc #endif #if !defined LH_LIC_TAG #error LH_LIC_TAG is required before including lh_license_core.inc #endif #if !defined LH_LIC_PLUGINID #error LH_LIC_PLUGINID is required before including lh_license_core.inc #endif // Heartbeat every 60 sec, lock after 120 sec without ACTIVE refresh #define LH_HB_SECONDS 60 #define LH_LOCK_AFTER 120 #define LH_EXPIRY_CHECK_SECONDS 30 // Retry behavior (primary then backup, with extra retries) #define LH_RETRY_PRIMARY 2 #define LH_RETRY_BACKUP 2 // Accept ACTIVE/BANNED/LOCKED response timestamp within this window (seconds) #define LH_TS_WINDOW 90 // XOR key for encoded strings #define LH_XKEY 73 // -------- security state -------- new g_lh_state = 0; new const g_lh_target = 9942; #define LH_ACTIVE() (g_lh_state == g_lh_target) // vault new g_lh_vault = INVALID_HANDLE; new g_lh_last_ok = 0; // license file values new g_lh_user[64]; new g_lh_key[128]; new g_lh_req_ts[16]; new g_lh_req_rn[24]; // anti spam hud new Float:g_lh_nextHud[33]; // retry counters new g_lh_try_primary = 0; new g_lh_try_backup = 0; new bool:g_lh_in_check = false; // log spam control new g_lh_last_print_state = -1; // -1 unknown, 0 locked, 1 active new bool:g_lh_logged_heartbeat_fail = false; // ========= ENCRYPTED STRINGS ========= // Replace these with your own encoded arrays anytime and recompile. // Values must decode correctly with LH_XKEY. // "www.cslonghorn.duckdns.org" new const LH_ENC_HOST1[] = { 62, 62, 62, 103, 42, 58, 37, 38, 39, 46, 33, 38, 59, 39, 103, 45, 60, 42, 34, 45, 39, 58, 103, 38, 59, 46, 0 }; // backup host (can be different) new const LH_ENC_HOST2[] = { 62, 62, 62, 103, 42, 58, 37, 38, 39, 46, 33, 38, 59, 39, 103, 45, 60, 42, 34, 45, 39, 58, 103, 38, 59, 46, 0 }; // "/pluginskeygen/auth.php" new const LH_ENC_PATH[] = { 102, 57, 37, 60, 46, 32, 39, 58, 34, 44, 48, 46, 44, 39, 102, 40, 60, 61, 33, 103, 57, 33, 57, 0 }; /* LH_ENC_SIGN must decode to EXACT same string as LH_SERVER_SECRET in /pluginskeygen/config.php It must be XOR(73) encoded bytes + 0 terminator. */ new const LH_ENC_SIGN[] = { 42,58,61,59,32,34,44,22,120,103,127,22,5,38,39,46,1,38,59,39,22,9,120,112,113,126,0 }; // ----------------- helpers ----------------- stock lh_dec(out[], outlen, const enc[]) { new i = 0; while (i < outlen - 1 && enc[i] != 0) { out[i] = enc[i] ^ LH_XKEY; i++; } out[i] = 0; } stock lh_wipe(buf[], len) { for (new i = 0; i < len; i++) buf[i] = 0; } stock bool:lh_is_digits(const s[]) { if (!s[0]) return false; for (new i = 0; s[i] != 0; i++) { if (s[i] < '0' || s[i] > '9') return false; } return true; } stock bool:lh_valid_host(const host[]) { if (!host[0]) return false; if (contain(host, ".") == -1) return false; for (new i = 0; host[i] != 0; i++) { new c = host[i]; if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '.' || c == '-') { continue; } return false; } return true; } stock bool:lh_valid_path(const path[]) { if (!path[0] || path[0] != '/') return false; for (new i = 0; path[i] != 0; i++) { new c = path[i]; if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '/' || c == '.' || c == '_' || c == '-') { continue; } return false; } return true; } stock lh_get_endpoint(out[], outlen) { new ip[32]; get_user_ip(0, ip, charsmax(ip), 1); new portStr[16]; get_cvar_string(LH_LIC_PORT_CVAR, portStr, charsmax(portStr)); if (!lh_is_digits(portStr)) copy(portStr, charsmax(portStr), "27015"); formatex(out, outlen - 1, "%s:%s", ip, portStr); } stock lh_paths(dir[], dirLen, file[], fileLen) { new cfg[128]; get_configsdir(cfg, charsmax(cfg)); formatex(dir, dirLen - 1, "%s/%s", cfg, LH_LIC_FOLDER); formatex(file, fileLen - 1, "%s/license.ini", dir); } stock lh_create_license_ini() { new dir[192], file[256]; lh_paths(dir, charsmax(dir), file, charsmax(file)); if (!dir_exists(dir)) mkdir(dir); if (!file_exists(file)) { new f = fopen(file, "wt"); if (f) { fprintf(f, "; ==================================================^n"); fprintf(f, "; LongHorn Licensed Mode^n"); fprintf(f, "; ==================================================^n"); fprintf(f, "; Plugin: %s^n", LH_LIC_PLUGINID); fprintf(f, "; Author: LongHorn^n"); fprintf(f, ";^n"); fprintf(f, "; INSTALL:^n"); fprintf(f, "; 1) Put this file in: addons/amxmodx/configs/%s/^n", LH_LIC_FOLDER); fprintf(f, "; 2) Fill USERNAME and KEY below^n"); fprintf(f, "; 3) Restart server^n"); fprintf(f, ";^n"); fprintf(f, "USERNAME = Paste_Username_Here^n"); fprintf(f, "KEY = Paste_Key_Here^n"); fprintf(f, ";^n"); fprintf(f, "; Thank you for your purchase.^n"); fprintf(f, "; Best Regards, LongHorn^n"); fprintf(f, "; ==================================================^n"); fclose(f); } } } stock bool:lh_read_license() { new dir[192], file[256], line[256]; lh_paths(dir, charsmax(dir), file, charsmax(file)); if (!file_exists(file)) return false; g_lh_user[0] = 0; g_lh_key[0] = 0; new f = fopen(file, "rt"); if (!f) return false; while (!feof(f)) { fgets(f, line, charsmax(line)); trim(line); if (!line[0] || line[0] == ';') continue; if (contain(line, "=") == -1) continue; new key[32], value[192]; strtok(line, key, charsmax(key), value, charsmax(value), '='); trim(key); trim(value); if (equali(key, "USERNAME")) copy(g_lh_user, charsmax(g_lh_user), value); else if (equali(key, "KEY")) copy(g_lh_key, charsmax(g_lh_key), value); } fclose(f); if (!g_lh_user[0] || !g_lh_key[0]) return false; if (containi(g_lh_user, "Paste_") != -1 || containi(g_lh_key, "Paste_") != -1) return false; return true; } stock lh_vault_open() { new vname[64]; formatex(vname, charsmax(vname), "lhlic_%s", LH_LIC_FOLDER); g_lh_vault = nvault_open(vname); if (g_lh_vault != INVALID_HANDLE) { new tmp[32]; if (nvault_get(g_lh_vault, "last_ok", tmp, charsmax(tmp))) g_lh_last_ok = str_to_num(tmp); } // Fail-open only inside grace window from last verified ACTIVE. if (!lh_is_expired()) g_lh_state = g_lh_target; else g_lh_state = 0; } stock lh_vault_save_ok() { if (g_lh_vault == INVALID_HANDLE) return; g_lh_last_ok = get_systime(); new ts[16]; num_to_str(g_lh_last_ok, ts, charsmax(ts)); nvault_set(g_lh_vault, "last_ok", ts); } stock bool:lh_is_expired() { if (g_lh_last_ok <= 0) return true; return (get_systime() - g_lh_last_ok) > LH_LOCK_AFTER; } // Minimal URL encode for query parameters stock lh_urlenc(const in[], out[], outlen) { new j = 0; for (new i = 0; in[i] != 0 && j < outlen - 1; i++) { new c = in[i]; if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~') { out[j++] = c; continue; } // encode everything else if (j + 3 >= outlen - 1) break; out[j++] = '%'; new hi = (c >> 4) & 0x0F; new lo = c & 0x0F; out[j++] = (hi < 10) ? ('0' + hi) : ('A' + (hi - 10)); out[j++] = (lo < 10) ? ('0' + lo) : ('A' + (lo - 10)); } out[j] = 0; } // Find substring value for key like "ts=" or "sig=" stock bool:lh_find_kv(const line[], const key[], out[], outlen) { new pos = contain(line, key); if (pos == -1) return false; pos += strlen(key); new j = 0; while (line[pos] && line[pos] != '|' && line[pos] != '^n' && line[pos] != '^r' && j < outlen - 1) { out[j++] = line[pos++]; } out[j] = 0; return (out[0] != 0); } // Parse first signed status line (e.g. ACTIVE|... or BANNED|...) stock bool:lh_parse_signed_line(const buf[], const statusTag[], tsOut[], tsLen, rnOut[], rnLen, sigOut[], sigLen) { new marker[24]; formatex(marker, charsmax(marker), "%s|", statusTag); new pos = contain(buf, marker); if (pos == -1) return false; new line[1024]; new i = pos, j = 0; while (buf[i] && buf[i] != '^n' && buf[i] != '^r' && j < charsmax(line)) { line[j++] = buf[i++]; } line[j] = 0; if (!lh_find_kv(line, "ts=", tsOut, tsLen)) return false; if (!lh_find_kv(line, "rn=", rnOut, rnLen)) return false; if (!lh_find_kv(line, "sig=", sigOut, sigLen)) return false; return true; } stock bool:lh_ts_ok(const tsStr[]) { if (!lh_is_digits(tsStr)) return false; new ts = str_to_num(tsStr); new now = get_systime(); new diff = now - ts; if (diff < 0) diff = -diff; return (diff <= LH_TS_WINDOW); } stock lh_build_sig_payload(const statusTag[], const tsStr[], const rnStr[], out[], outlen) { new ep[64]; lh_get_endpoint(ep, charsmax(ep)); formatex(out, outlen - 1, "%s|%s|%s|%s|%s|%s|%s", statusTag, g_lh_user, g_lh_key, ep, LH_LIC_PLUGINID, tsStr, rnStr); } stock bool:lh_verify_sig(const statusTag[], const tsStr[], const rnStr[], const sigStr[]) { if (!lh_ts_ok(tsStr)) return false; if (!statusTag[0]) return false; if (!lh_is_digits(rnStr)) return false; if (strlen(rnStr) < 6 || strlen(rnStr) > 16) return false; if (!equal(tsStr, g_lh_req_ts)) return false; if (!equal(rnStr, g_lh_req_rn)) return false; new payload[512]; lh_build_sig_payload(statusTag, tsStr, rnStr, payload, charsmax(payload)); new sign[256]; lh_dec(sign, charsmax(sign), LH_ENC_SIGN); // if secret is missing, fail safe if (!sign[0]) { lh_wipe(sign, charsmax(sign)); return false; } new tmp[800]; formatex(tmp, charsmax(tmp) - 1, "%s|%s", sign, payload); new calc[64]; md5(tmp, calc, charsmax(calc)); lh_wipe(sign, charsmax(sign)); lh_wipe(tmp, charsmax(tmp)); return equali(calc, sigStr); } stock lh_print_state_locked() { if (g_lh_last_print_state == 0) return; g_lh_last_print_state = 0; server_print("[%s] License LOCKED | Contact Seller (LongHorn).", LH_LIC_TAG); } stock lh_print_state_active() { if (g_lh_last_print_state == 1) return; g_lh_last_print_state = 1; server_print("[%s] Username: OK | Key: OK | Endpoint: OK", LH_LIC_TAG); server_print("[%s] License ACTIVE | Plugin Enabled | Thank You.", LH_LIC_TAG); } stock lh_end_check() { g_lh_in_check = false; g_lh_req_ts[0] = 0; g_lh_req_rn[0] = 0; } stock lh_retry_or_fail(hostIndex) { if (hostIndex == 0) { g_lh_try_primary++; if (g_lh_try_primary < LH_RETRY_PRIMARY) { set_task(0.4, "LH_License_TryPrimaryAgain"); return; } set_task(0.4, "LH_License_CheckBackup"); return; } g_lh_try_backup++; if (g_lh_try_backup < LH_RETRY_BACKUP) { set_task(0.4, "LH_License_TryBackupAgain"); return; } // Keep last known state until grace window expires. if (!g_lh_logged_heartbeat_fail) { g_lh_logged_heartbeat_fail = true; server_print("[%s] Heartbeat failed (primary+backup). Keeping last known state.", LH_LIC_TAG); } lh_end_check(); } stock lh_get_http_status(const buf[]) { new p = contain(buf, "HTTP/"); if (p == -1) return 0; while (buf[p] && buf[p] != ' ' && buf[p] != '^r' && buf[p] != '^n') p++; if (buf[p] != ' ') return 0; p++; new codeStr[4]; new j = 0; while (buf[p] >= '0' && buf[p] <= '9' && j < charsmax(codeStr)) { codeStr[j++] = buf[p++]; } codeStr[j] = 0; if (j != 3) return 0; return str_to_num(codeStr); } stock lh_extract_http_body(const httpBuf[], bodyOut[], bodyMax) { new p = contain(httpBuf, "^r^n^r^n"); if (p == -1) { copy(bodyOut, bodyMax, httpBuf); return; } p += 4; new j = 0; while (httpBuf[p] && j < bodyMax) { bodyOut[j++] = httpBuf[p++]; } bodyOut[j] = 0; } // ----------------- Public API ----------------- public LH_License_Init() { lh_create_license_ini(); lh_vault_open(); set_task(5.0, "LH_License_CheckNow"); set_task(float(LH_HB_SECONDS), "LH_License_CheckNow", 90110, _, _, "b"); set_task(float(LH_EXPIRY_CHECK_SECONDS), "LH_License_EnforceExpiry", 90120, _, _, "b"); } public LH_License_End() { if (g_lh_vault != INVALID_HANDLE) nvault_close(g_lh_vault); } public bool:LH_License_AllowCmd(id) { if (LH_ACTIVE()) return true; if (lh_is_expired()) g_lh_state = 0; new Float:gt = get_gametime(); if (id >= 1 && id <= 32) { if (gt < g_lh_nextHud[id]) return false; g_lh_nextHud[id] = gt + 3.0; } client_print(id, print_chat, "[%s] Plugin LOCKED | Contact Seller (LongHorn).", LH_LIC_TAG); set_hudmessage(255, 0, 0, -1.0, 0.23, 0, 0.1, 5.0, 0.1, 0.1, 4); show_hudmessage(id, "ATTENTION^nPlugin is LOCKED^nContact Seller (LongHorn)"); return false; } public LH_License_StatusCmd(id) { if (LH_ACTIVE()) client_print(id, print_console, "[%s] License ACTIVE | Plugin Enabled", LH_LIC_TAG); else client_print(id, print_console, "[%s] License LOCKED | Contact Seller (LongHorn)", LH_LIC_TAG); return PLUGIN_HANDLED; } public LH_License_EnforceExpiry() { if (!lh_is_expired()) return; g_lh_state = 0; lh_print_state_locked(); } // ----------------- Network check ----------------- public LH_License_CheckNow() { if (g_lh_in_check) return; g_lh_in_check = true; if (lh_is_expired()) { g_lh_state = 0; lh_print_state_locked(); } if (!lh_read_license()) { g_lh_state = 0; server_print("[%s] License LOCKED | license.ini missing/invalid", LH_LIC_TAG); lh_end_check(); return; } g_lh_try_primary = 0; g_lh_try_backup = 0; LH_License_CheckHost(0); } public LH_License_CheckHost(hostIndex) { new host[96], path[128]; lh_dec(path, charsmax(path), LH_ENC_PATH); if (hostIndex == 0) lh_dec(host, charsmax(host), LH_ENC_HOST1); else lh_dec(host, charsmax(host), LH_ENC_HOST2); if (!lh_valid_host(host) || !lh_valid_path(path)) { g_lh_state = 0; server_print("[%s] License LOCKED | Invalid encoded host/path config", LH_LIC_TAG); lh_wipe(host, charsmax(host)); lh_wipe(path, charsmax(path)); lh_end_check(); return; } new ep[64]; lh_get_endpoint(ep, charsmax(ep)); new ts[16]; num_to_str(get_systime(), ts, charsmax(ts)); new rn[24]; num_to_str(random_num(100000000, 999999999), rn, charsmax(rn)); copy(g_lh_req_ts, charsmax(g_lh_req_ts), ts); copy(g_lh_req_rn, charsmax(g_lh_req_rn), rn); // URL encode parameters new uEnc[256], kEnc[512], epEnc[256], pidEnc[256], tsEnc[64], rnEnc[64]; lh_urlenc(g_lh_user, uEnc, charsmax(uEnc)); lh_urlenc(g_lh_key, kEnc, charsmax(kEnc)); lh_urlenc(ep, epEnc, charsmax(epEnc)); lh_urlenc(LH_LIC_PLUGINID, pidEnc, charsmax(pidEnc)); lh_urlenc(ts, tsEnc, charsmax(tsEnc)); lh_urlenc(rn, rnEnc, charsmax(rnEnc)); new req[2048]; formatex(req, charsmax(req) - 1, "GET %s?u=%s&k=%s&ep=%s&pid=%s&ts=%s&rn=%s HTTP/1.0^r^nHost: %s^r^nUser-Agent: %s/%s^r^nAccept-Encoding: identity^r^nConnection: close^r^n^r^n", path, uEnc, kEnc, epEnc, pidEnc, tsEnc, rnEnc, host, LH_LIC_TAG, LH_LIC_PLUGINID ); lh_wipe(path, charsmax(path)); new err; new sock = socket_open(host, 80, SOCKET_TCP, err); lh_wipe(host, charsmax(host)); if (sock <= 0) { lh_retry_or_fail(hostIndex); return; } socket_send(sock, req, strlen(req)); new args[2]; args[0] = sock; args[1] = hostIndex; new taskId = 120000 + sock; set_task(1.0, "LH_License_ReadResponse", taskId, args, 2); } public LH_License_TryPrimaryAgain() { LH_License_CheckHost(0); } public LH_License_CheckBackup() { LH_License_CheckHost(1); } public LH_License_TryBackupAgain() { LH_License_CheckHost(1); } public LH_License_ReadResponse(args[]) { new sock = args[0]; new hostIndex = args[1]; new buf[4096]; buf[0] = 0; // Read with short readiness waits to avoid false negatives on slow networks. new tmp[2048]; for (new i = 0; i < 6; i++) { if (!socket_is_readable(sock, 200000)) continue; tmp[0] = 0; new got = socket_recv(sock, tmp, charsmax(tmp)); if (got <= 0) break; if (got > charsmax(tmp)) got = charsmax(tmp); tmp[got] = 0; add(buf, charsmax(buf), tmp); if (strlen(buf) >= charsmax(buf) - 1) break; } socket_close(sock); if (lh_get_http_status(buf) != 200) { lh_retry_or_fail(hostIndex); return; } new body[4096]; lh_extract_http_body(buf, body, charsmax(body)); // Signed BANNED => locked hard new tsStr[32], rnStr[32], sigStr[96]; if (lh_parse_signed_line(body, "BANNED", tsStr, charsmax(tsStr), rnStr, charsmax(rnStr), sigStr, charsmax(sigStr))) { if (lh_verify_sig("BANNED", tsStr, rnStr, sigStr)) { g_lh_state = 0; lh_print_state_locked(); lh_end_check(); return; } } // Signed LOCKED => locked hard if (lh_parse_signed_line(body, "LOCKED", tsStr, charsmax(tsStr), rnStr, charsmax(rnStr), sigStr, charsmax(sigStr))) { if (lh_verify_sig("LOCKED", tsStr, rnStr, sigStr)) { g_lh_state = 0; lh_print_state_locked(); lh_end_check(); return; } } // Signed ACTIVE if (lh_parse_signed_line(body, "ACTIVE", tsStr, charsmax(tsStr), rnStr, charsmax(rnStr), sigStr, charsmax(sigStr))) { if (lh_verify_sig("ACTIVE", tsStr, rnStr, sigStr)) { g_lh_state = g_lh_target; lh_vault_save_ok(); g_lh_logged_heartbeat_fail = false; lh_print_state_active(); lh_end_check(); return; } } // Response was reachable but invalid/untrusted. Keep last known state until grace timeout. lh_retry_or_fail(hostIndex); } #endif