#include <stdio.h>
#include <string.h>
#include "hymma/hlm.h"
#include "hlm_json.h"
#define HLM_DEFAULT_BASE_URL "https://license-management.com/api/"
#define HLM_DEFAULT_VALID_DAYS 90u
#define HLM_HTTP_ATTEMPTS 3
#define HLM_HTTP_BACKOFF_BASE_MS 200u
#define HLM_HTTP_BACKOFF_CAP_MS 5000u
#define HLM_HTTP_TOTAL_WAIT_CAP_MS 10000u
static size_t json_escape(const char *in, char *out, size_t cap)
{
size_t o = 0;
for (; *in != '\0'; in++) {
unsigned char c = (unsigned char)*in;
if (c == '"' || c == '\\') {
if (o + 2 >= cap) return (size_t)-1;
out[o++] = '\\';
out[o++] = (char)c;
} else if (c < 0x20) {
if (o + 6 >= cap) return (size_t)-1;
o += (size_t)snprintf(out + o, cap - o, "\\u%04x", c);
} else {
if (o + 1 >= cap) return (size_t)-1;
out[o++] = (char)c;
}
}
if (o >= cap) return (size_t)-1;
out[o] = '\0';
return o;
}
static void make_correlation_id(hlm_client *c, char out[33])
{
static const char HEX[] = "0123456789abcdef";
uint64_t seed;
int i;
c->corr_counter += 0x9e3779b9u;
seed = (uint64_t)(c->cfg.clock.now ? c->cfg.clock.now(c->cfg.clock.user) : 0);
seed = seed * 6364136223846793005ULL + c->corr_counter;
for (i = 0; i < 32; i++) {
seed = seed * 6364136223846793005ULL + 1442695040888963407ULL;
out[i] = HEX[(seed >> 60) & 0xf];
}
out[32] = '\0';
}
static const char *format_param(hlm_alg alg)
{
switch (alg) {
case HLM_ALG_ES256: return "es256";
case HLM_ALG_EDDSA: return "eddsa";
default: return "jws";
}
}
static int http_is_success(int status)
{
return status >= 200 && status < 300;
}
static int is_transient(int status)
{
if (status == 429 || status == 408) return 1;
if (status >= 500 && status != 501 && status != 505) return 1;
return 0;
}
static void backoff_wait(hlm_client *c, unsigned ms, unsigned *total_ms)
{
if (ms == 0) return;
if (*total_ms + ms > HLM_HTTP_TOTAL_WAIT_CAP_MS)
ms = HLM_HTTP_TOTAL_WAIT_CAP_MS - *total_ms;
*total_ms += ms;
if (c->cfg.sleep.sleep_ms != NULL && ms > 0)
c->cfg.sleep.sleep_ms(c->cfg.sleep.user, ms);
}
static int send_req(hlm_client *c, const char *method, const char *path_query,
const char *body, const char *idempotency_key,
int *status_out)
{
hlm_http_request req;
char url[512];
char corr[33];
unsigned total_wait_ms = 0;
int attempt;
if (c->cfg.http.send == NULL) return HLM_E_HTTP;
if (snprintf(url, sizeof(url), "%s%s", c->cfg.base_url, path_query) >=
(int)sizeof(url))
return HLM_E_ARG;
make_correlation_id(c, corr);
req.method = method;
req.url = url;
req.body = body;
req.api_key = c->cfg.client_api_key;
req.idempotency_key = idempotency_key;
req.correlation_id = corr;
for (attempt = 1; attempt <= HLM_HTTP_ATTEMPTS; attempt++) {
hlm_http_response resp;
int r;
resp.status = 0;
resp.body = c->resp;
resp.body_cap = sizeof(c->resp) - 1;
resp.body_len = 0;
resp.retry_after_seconds = -1;
resp.failure = HLM_NET_UNKNOWN;
r = c->cfg.http.send(c->cfg.http.user, &req, &resp);
if (r == HLM_OK && resp.status != 0) {
c->resp[resp.body_len < sizeof(c->resp) ? resp.body_len
: sizeof(c->resp) - 1] = '\0';
c->last_http_status = resp.status;
c->last_net_failure = HLM_NET_NONE;
if (resp.status == 407) {
c->last_net_failure = HLM_NET_PROXY;
return HLM_E_HTTP;
}
if (resp.status == 401 || resp.status == 403) return HLM_E_AUTH;
if (!is_transient(resp.status) || attempt == HLM_HTTP_ATTEMPTS) {
*status_out = resp.status;
return HLM_OK;
}
if (resp.retry_after_seconds >= 0) {
unsigned ms = (unsigned)resp.retry_after_seconds * 1000u;
if (ms > HLM_HTTP_BACKOFF_CAP_MS) ms = HLM_HTTP_BACKOFF_CAP_MS;
backoff_wait(c, ms, &total_wait_ms);
} else {
unsigned ms = HLM_HTTP_BACKOFF_BASE_MS << (attempt - 1);
if (ms > HLM_HTTP_BACKOFF_CAP_MS) ms = HLM_HTTP_BACKOFF_CAP_MS;
backoff_wait(c, ms, &total_wait_ms);
}
continue;
}
c->last_net_failure = resp.failure != HLM_NET_NONE ? resp.failure
: HLM_NET_UNKNOWN;
if (attempt == HLM_HTTP_ATTEMPTS) return HLM_E_HTTP;
backoff_wait(c, HLM_HTTP_BACKOFF_BASE_MS << (attempt - 1),
&total_wait_ms);
}
return HLM_E_HTTP;
}
static void capture_error_detail(hlm_client *c)
{
static const char *const FIELDS[] = { "detail", "message", "title", "error" };
hlm_json_tok toks[32];
hlm_json_doc doc;
size_t len = strlen(c->resp);
int ntok = hlm_json_parse(c->resp, len, toks, 32);
size_t i;
c->last_error[0] = '\0';
if (ntok <= 0 || toks[0].type != HLM_JSON_OBJECT) return;
hlm_json_doc_init(&doc, c->resp, len, toks, ntok);
for (i = 0; i < sizeof(FIELDS) / sizeof(FIELDS[0]); i++) {
int t = hlm_json_member(&doc, 0, FIELDS[i]);
if (t >= 0 && doc.toks[t].type == HLM_JSON_STRING &&
hlm_json_string(&doc, t, c->last_error,
sizeof(c->last_error)) != (size_t)-1)
return;
c->last_error[0] = '\0';
}
}
static int map_api_error(hlm_client *c, int status)
{
capture_error_detail(c);
if (status == 401 || status == 403) return HLM_E_AUTH;
if (strstr(c->resp, "trial_quota") != NULL) return HLM_E_TRIAL_QUOTA;
if (strstr(c->resp, "embedded-format-requires-receipt") != NULL)
return HLM_E_PAID_FORMAT_REQUIRED;
if (strstr(c->resp, "sandbox_limit") != NULL ||
strstr(c->resp, "ALU limit") != NULL)
return HLM_E_PLAN_LIMIT;
return HLM_E_API;
}
const char *hlm_net_failure_str(hlm_net_failure f)
{
switch (f) {
case HLM_NET_NONE: return "None";
case HLM_NET_NAME_RESOLUTION: return "NameResolution";
case HLM_NET_SECURE_CONNECTION: return "SecureConnection";
case HLM_NET_PROXY: return "Proxy";
case HLM_NET_TIMEOUT: return "Timeout";
case HLM_NET_CONNECTION: return "Connection";
default: return "Unknown";
}
}
int hlm_net_message(hlm_net_failure f, const char *host, char *out, size_t cap)
{
int n;
if (out == NULL || cap == 0) return HLM_E_ARG;
if (host == NULL || host[0] == '\0') host = "the license server";
switch (f) {
case HLM_NET_NAME_RESOLUTION:
n = snprintf(out, cap,
"This computer's network could not find %s. A company DNS "
"filter or firewall is probably blocking it. Ask your IT "
"team to allow %s, or install the license file by hand.",
host, host);
break;
case HLM_NET_SECURE_CONNECTION:
n = snprintf(out, cap,
"A secure connection to %s could not be set up. A proxy or "
"security product may be inspecting HTTPS traffic. Ask your "
"IT team to allow %s, or install the license file by hand.",
host, host);
break;
case HLM_NET_PROXY:
n = snprintf(out, cap,
"The network proxy refused the connection to %s. Ask your "
"IT team to allow %s, or install the license file by hand.",
host, host);
break;
case HLM_NET_TIMEOUT:
n = snprintf(out, cap,
"%s did not answer in time. The network may be slow or "
"blocking it. Try again, or install the license file by "
"hand.",
host);
break;
default:
n = snprintf(out, cap,
"Could not connect to %s. Check the internet connection and "
"firewall, or install the license file by hand.",
host);
break;
}
if (n < 0) {
out[0] = '\0';
return HLM_E_BUFFER;
}
return (size_t)n >= cap ? HLM_E_BUFFER : HLM_OK;
}
static void base_url_host(const char *url, char *out, size_t cap)
{
const char *p = strstr(url, "://");
size_t n = 0;
p = p != NULL ? p + 3 : url;
while (p[n] != '\0' && p[n] != '/' && p[n] != ':' && p[n] != '?' &&
n + 1 < cap) {
out[n] = p[n];
n++;
}
out[n] = '\0';
}
static void begin_call(hlm_client *c)
{
c->last_net_failure = HLM_NET_NONE;
}
static int end_call(hlm_client *c, int r)
{
if (r == HLM_E_HTTP && c->last_net_failure != HLM_NET_NONE) {
char host[128];
base_url_host(c->cfg.base_url, host, sizeof(host));
hlm_net_message(c->last_net_failure, host, c->last_error,
sizeof(c->last_error));
}
return r;
}
static int fetch_server_time(hlm_client *c, int64_t *now_out)
{
int status, r;
size_t len;
const char *s;
r = send_req(c, "GET", "DateTime", NULL, NULL, &status);
if (r != HLM_OK) return r;
if (!http_is_success(status)) return map_api_error(c, status);
s = c->resp;
len = strlen(s);
if (len >= 2 && s[0] == '"' && s[len - 1] == '"') {
s++;
len -= 2;
}
return hlm_parse_iso8601(s, len, now_out) == 0 ? HLM_OK : HLM_E_FORMAT;
}
static void use_local_clock(hlm_client *c)
{
int64_t t = c->cfg.clock.now(c->cfg.clock.user);
if (t < c->time_floor) t = c->time_floor;
c->eval_now = t;
}
static void resolve_now(hlm_client *c)
{
int64_t t;
if (c->cfg.timesync.now != NULL) {
if (c->cfg.timesync.now(c->cfg.timesync.user, &t) == HLM_OK)
goto trusted;
if (c->cfg.http.send != NULL && fetch_server_time(c, &t) == HLM_OK)
goto trusted;
}
use_local_clock(c);
return;
trusted:
if (t > c->time_floor) c->time_floor = t;
c->eval_now = t;
}
int hlm_client_init(hlm_client *c, const hlm_client_cfg *cfg)
{
if (c == NULL || cfg == NULL) return HLM_E_ARG;
if (cfg->product_id == NULL || cfg->keys == NULL || cfg->key_count == 0 ||
cfg->crypto.verify == NULL || cfg->machine_id == NULL ||
cfg->machine_name == NULL)
return HLM_E_ARG;
memset(c, 0, sizeof(*c));
c->cfg = *cfg;
if (c->cfg.base_url == NULL) c->cfg.base_url = HLM_DEFAULT_BASE_URL;
if (c->cfg.valid_days == 0) c->cfg.valid_days = HLM_DEFAULT_VALID_DAYS;
if (c->cfg.format == 0) c->cfg.format = HLM_ALG_RS256;
if (c->cfg.clock.now == NULL) c->cfg.clock = hlm_clock_system();
return HLM_OK;
}
static void adopt_verified(hlm_client *c, const hlm_license *lic)
{
if (lic->created != HLM_TIME_NONE && lic->created > c->time_floor)
c->time_floor = lic->created;
if (lic->updated != HLM_TIME_NONE && lic->updated > c->time_floor)
c->time_floor = lic->updated;
if (c->eval_now < c->time_floor) c->eval_now = c->time_floor;
c->license = *lic;
c->status = hlm_license_status(lic, c->eval_now);
c->has_license = 1;
}
static int adopt_license(hlm_client *c, const char *jws, size_t len)
{
hlm_license lic;
int r = hlm_license_check_machine(jws, len, c->cfg.keys, c->cfg.key_count,
&c->cfg.crypto, c->cfg.product_id,
c->cfg.machine_id,
c->cfg.machine_id_legacy, c->eval_now,
&lic, NULL);
if (r != HLM_OK) return r;
adopt_verified(c, &lic);
return HLM_OK;
}
static void url_encode(const char *s, char *out, size_t cap)
{
static const char hexd[] = "0123456789ABCDEF";
size_t o = 0;
for (; *s != '\0'; s++) {
unsigned char ch = (unsigned char)*s;
int plain = (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') ||
(ch >= '0' && ch <= '9') || ch == '-' || ch == '_' ||
ch == '.' || ch == '~';
if (plain) {
if (o + 1 >= cap) break;
out[o++] = (char)ch;
} else {
if (o + 3 >= cap) break;
out[o++] = '%';
out[o++] = hexd[ch >> 4];
out[o++] = hexd[ch & 15];
}
}
out[o < cap ? o : cap - 1] = '\0';
}
static int ensure_computer(hlm_client *c, char *pc_id, size_t pc_id_cap)
{
char mac_esc[HLM_MAX_MACHINE_ID * 2], name_esc[HLM_MAX_NAME * 2];
char body[768], idem[256], path[512];
const char *legacy = c->cfg.machine_id_legacy;
int status, r;
if (legacy != NULL && legacy[0] == '\0') legacy = NULL;
if (json_escape(c->cfg.machine_id, mac_esc, sizeof(mac_esc)) == (size_t)-1 ||
json_escape(c->cfg.machine_name, name_esc, sizeof(name_esc)) == (size_t)-1)
return HLM_E_ARG;
if (legacy != NULL) {
char legacy_esc[HLM_MAX_MACHINE_ID * 2];
if (json_escape(legacy, legacy_esc, sizeof(legacy_esc)) == (size_t)-1)
return HLM_E_ARG;
snprintf(body, sizeof(body),
"{\"MacAddress\":\"%s\",\"Name\":\"%s\",\"LegacyMacAddress\":\"%s\"}",
mac_esc, name_esc, legacy_esc);
} else {
snprintf(body, sizeof(body), "{\"MacAddress\":\"%s\",\"Name\":\"%s\"}",
mac_esc, name_esc);
}
snprintf(idem, sizeof(idem), "computer:%s:%s", c->cfg.machine_id,
c->cfg.machine_name);
r = send_req(c, "POST", "computer", body, idem, &status);
if (r != HLM_OK) return r;
if (!http_is_success(status) && status != 409)
return map_api_error(c, status);
if (legacy != NULL) {
char name_url[HLM_MAX_NAME * 3];
url_encode(c->cfg.machine_name, name_url, sizeof(name_url));
snprintf(path, sizeof(path),
"computer?macAddress=%s&legacyMacAddress=%s&name=%s",
c->cfg.machine_id, legacy, name_url);
} else {
snprintf(path, sizeof(path), "computer?macAddress=%s",
c->cfg.machine_id);
}
r = send_req(c, "GET", path, NULL, NULL, &status);
if (r != HLM_OK) return r;
if (!http_is_success(status)) return map_api_error(c, status);
{
hlm_json_tok toks[64];
hlm_json_doc doc;
size_t resp_len = strlen(c->resp);
int ntok = hlm_json_parse(c->resp, resp_len, toks, 64);
int t;
if (ntok <= 0) return HLM_E_FORMAT;
hlm_json_doc_init(&doc, c->resp, resp_len, toks, ntok);
t = hlm_json_member(&doc, 0, "id");
if (t < 0) t = hlm_json_member(&doc, 0, "Id");
if (t < 0 ||
hlm_json_string(&doc, t, pc_id, pc_id_cap) == (size_t)-1)
return HLM_E_FORMAT;
}
return HLM_OK;
}
static int post_license(hlm_client *c, const char *pc_id)
{
char body[256], idem[256];
int status, r;
snprintf(body, sizeof(body), "{\"Product\":\"%s\",\"Computer\":\"%s\"}",
c->cfg.product_id, pc_id);
snprintf(idem, sizeof(idem), "license:%s:%s", pc_id, c->cfg.product_id);
r = send_req(c, "POST", "license", body, idem, &status);
if (r != HLM_OK) return r;
if (!http_is_success(status) && status != 409)
return map_api_error(c, status);
return HLM_OK;
}
static int get_signed_license(hlm_client *c, const char *pc_id)
{
char path[512];
int status, r;
size_t len;
snprintf(path, sizeof(path),
"license?computer=%s&product=%s&validDays=%u&format=%s",
pc_id, c->cfg.product_id, c->cfg.valid_days,
format_param(c->cfg.format));
r = send_req(c, "GET", path, NULL, NULL, &status);
if (r != HLM_OK) return r;
if (!http_is_success(status)) return map_api_error(c, status);
len = strlen(c->resp);
if (len == 0 || c->resp[0] == '<') return HLM_E_FORMAT;
r = adopt_license(c, c->resp, len);
if (r != HLM_OK) return r;
if (c->cfg.storage.write != NULL) {
if (c->cfg.storage.write(c->cfg.storage.user, c->resp, len) != HLM_OK)
return HLM_E_STORAGE;
}
return HLM_OK;
}
static int refresh_internal(hlm_client *c)
{
char pc_id[HLM_MAX_ID];
int r;
r = ensure_computer(c, pc_id, sizeof(pc_id));
if (r != HLM_OK) return r;
r = post_license(c, pc_id);
if (r != HLM_OK) return r;
return get_signed_license(c, pc_id);
}
int hlm_client_refresh(hlm_client *c)
{
if (c == NULL) return HLM_E_ARG;
begin_call(c);
resolve_now(c);
return end_call(c, refresh_internal(c));
}
static int check_internal(hlm_client *c)
{
int r;
if (c->cfg.storage.read != NULL) {
int n = c->cfg.storage.read(c->cfg.storage.user, c->buf,
sizeof(c->buf) - 1);
if (n > 0) {
c->buf[n] = '\0';
r = adopt_license(c, c->buf, (size_t)n);
if (r == HLM_OK && c->status != HLM_STATUS_EXPIRED)
return HLM_OK;
}
}
r = refresh_internal(c);
if (r == HLM_E_HTTP && c->has_license) {
return HLM_OK;
}
if (r == HLM_E_HTTP && c->cfg.http.send == NULL && !c->has_license)
return HLM_E_NO_LICENSE;
return r;
}
int hlm_client_check(hlm_client *c)
{
if (c == NULL) return HLM_E_ARG;
begin_call(c);
resolve_now(c);
return end_call(c, check_internal(c));
}
static int patch_license(hlm_client *c, const char *code_or_null)
{
char body[512];
int status, r;
if (!c->has_license) return HLM_E_NO_LICENSE;
if (code_or_null != NULL) {
char code_esc[HLM_MAX_CODE * 2];
if (json_escape(code_or_null, code_esc, sizeof(code_esc)) == (size_t)-1)
return HLM_E_ARG;
snprintf(body, sizeof(body), "{\"License\":\"%s\",\"Code\":\"%s\"}",
c->license.id, code_esc);
} else {
snprintf(body, sizeof(body), "{\"License\":\"%s\",\"Code\":null}",
c->license.id);
}
r = send_req(c, "PATCH", "license", body, NULL, &status);
if (r != HLM_OK) return r;
if (status == 404) {
r = refresh_internal(c);
if (r != HLM_OK) return r;
r = send_req(c, "PATCH", "license", body, NULL, &status);
if (r != HLM_OK) return r;
}
if (status != 204 && !http_is_success(status))
return map_api_error(c, status);
return HLM_OK;
}
static int set_receipt_code_internal(hlm_client *c, const char *code_or_null)
{
int r;
if (!c->has_license) {
r = check_internal(c);
if (r != HLM_OK) return r;
}
r = patch_license(c, code_or_null);
if (r != HLM_OK) return r;
return refresh_internal(c);
}
static int set_receipt_code(hlm_client *c, const char *code_or_null)
{
begin_call(c);
resolve_now(c);
return end_call(c, set_receipt_code_internal(c, code_or_null));
}
int hlm_client_activate(hlm_client *c, const char *receipt_code)
{
if (c == NULL || receipt_code == NULL || receipt_code[0] == '\0')
return HLM_E_ARG;
return set_receipt_code(c, receipt_code);
}
int hlm_client_deactivate(hlm_client *c)
{
if (c == NULL) return HLM_E_ARG;
return set_receipt_code(c, NULL);
}
int hlm_client_import(hlm_client *c, const char *document, size_t len)
{
hlm_license lic;
int r;
if (c == NULL || document == NULL) return HLM_E_ARG;
r = hlm_license_import(document, len, c->cfg.keys, c->cfg.key_count,
&c->cfg.crypto, c->cfg.product_id,
c->cfg.machine_id, c->cfg.machine_id_legacy,
&c->cfg.storage, &lic);
if (r < 0) return r;
hlm_import_message(r, &lic, NULL, c->last_error, sizeof(c->last_error));
if (r == HLM_IMPORT_INSTALLED) {
use_local_clock(c);
adopt_verified(c, &lic);
}
return r;
}