$ts) { return self::fail('license expired'); } } if (!self::checkBinding($lic, $meta, $licPath)) { return self::fail('environment not allowed'); } $remote = isset($lic['remote']) && is_array($lic['remote']) ? $lic['remote'] : array(); if (!empty($remote['url'])) { if (!self::remoteVerify($remote, $lic, $licPath)) { return self::fail('remote verification failed'); } } $code = self::decrypt($meta); if ($code === null) { return self::fail('payload integrity check failed'); } return self::execute($code); } /* ------------------------------------------------------------ 许可证 */ private static function findLicense(array $meta) { $cands = array(); if (!empty($meta['lic_path'])) { $cands[] = $meta['lic_path']; } foreach (array('LJC_LICENSE', 'LJC_LICENSE_PATH', 'SG_LIC_PATH') as $env) { $v = getenv($env); if ($v) { $cands[] = $v; } } $name = getenv('LJC_LICENSE_NAME') ? getenv('LJC_LICENSE_NAME') : 'ljc.lic'; $here = isset($meta['src']) ? dirname($meta['src']) : __DIR__; $cands[] = $here . DIRECTORY_SEPARATOR . $name; $cands[] = $here . DIRECTORY_SEPARATOR . 'licenses' . DIRECTORY_SEPARATOR . $name; // 归档部署:逐级向上找(最多 8 层),loader/lic 通常在包根 $up = $here; for ($i = 0; $i < 8; $i++) { $parent = dirname($up); $cands[] = $parent . DIRECTORY_SEPARATOR . $name; if ($parent === $up) { break; } $up = $parent; } $cands[] = __DIR__ . DIRECTORY_SEPARATOR . $name; foreach ($cands as $c) { if ($c && @is_file($c) && @is_readable($c)) { return $c; } } return null; } private static function readLicense($path) { $raw = @file_get_contents($path); if ($raw === false) { return null; } $lines = array(); foreach (preg_split("/\r\n|\n|\r/", $raw) as $ln) { $ln = trim($ln); if ($ln === '' || $ln[0] === ';' || $ln[0] === '#') { continue; } $lines[] = $ln; } if (count($lines) < 2) { return null; } $payload = $lines[count($lines) - 2]; $sig = $lines[count($lines) - 1]; $calc = hash_hmac('sha256', $payload, self::signKey()); if (!self::hashEquals($calc, $sig)) { return null; } $json = base64_decode($payload, true); if ($json === false) { return null; } $doc = json_decode($json, true); if (!is_array($doc)) { return null; } if (!empty($doc['revoked'])) { return null; } $doc['__payload'] = $payload; $doc['__sig'] = $sig; return $doc; } /* ------------------------------------------------------------ 绑定校验 */ private static function checkBinding(array $lic, array $meta, $licPath) { $bind = isset($lic['bind']) && is_array($lic['bind']) ? $lic['bind'] : array(); $rules = 0; $hit = false; if (!empty($bind['domains'])) { $rules++; $hosts = self::hosts(); foreach ((array)$bind['domains'] as $pat) { $pat = strtolower(trim($pat)); if ($pat === '') { continue; } foreach ($hosts as $h) { if ($h === $pat) { $hit = true; break 2; } if (strpos($pat, '*.') === 0) { $suffix = substr($pat, 1); if (substr($h, -strlen($suffix)) === $suffix) { $hit = true; break 2; } } } } } if (!empty($bind['ips'])) { $rules++; $have = self::localIps(); foreach ((array)$bind['ips'] as $ip) { if (in_array(trim($ip), $have, true)) { $hit = true; break; } } } if (!empty($bind['macs'])) { $rules++; $want = array(); foreach ((array)$bind['macs'] as $m) { $n = self::normMac($m); if ($n !== '') { $want[] = $n; } } $have = array(); foreach (self::localMacs() as $m) { $have[] = self::normMac($m); } if (array_intersect($want, $have)) { $hit = true; } } if (!empty($bind['machine_ids'])) { $rules++; $mid = self::machineId(); foreach ((array)$bind['machine_ids'] as $m) { $m = trim($m); if ($m !== '' && ($m === $mid || ($mid !== '' && strpos($mid, $m) === 0))) { $hit = true; break; } } } if (!empty($bind['paths'])) { $rules++; $dir = isset($meta['src']) ? dirname($meta['src']) : __DIR__; foreach ((array)$bind['paths'] as $p) { $p = rtrim(trim($p), '/*'); if ($p !== '' && strpos($dir, $p) === 0) { $hit = true; break; } } } if ($rules === 0) { return true; } return $hit; } /* ------------------------------------------------------------ 远程验证 */ private static function remoteVerify(array $remote, array $lic, $licPath) { $url = $remote['url']; $body = http_build_query(array( 'lic' => isset($lic['__payload']) ? $lic['__payload'] : '', 'sig' => isset($lic['__sig']) ? $lic['__sig'] : '', 'host' => self::primaryHost(), 'mid' => self::machineId(), 'file' => isset($GLOBALS['LJC_CURRENT_FILE']) ? $GLOBALS['LJC_CURRENT_FILE'] : '', )); $resp = self::httpPost($url, $body, isset($remote['timeout']) ? (int)$remote['timeout'] : 4); if ($resp === false) { $failOpen = !empty($remote['fail_open']); return $failOpen; } $data = json_decode($resp, true); if (is_array($data)) { return isset($data['status']) && $data['status'] === 'ok'; } return stripos($resp, 'ok') !== false; } private static function httpPost($url, $body, $timeout) { if (function_exists('curl_init')) { $ch = curl_init($url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $body); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, $timeout <= 0 ? 4 : $timeout); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, CURLOPT_USERAGENT, 'LJC-Runtime/1'); $out = curl_exec($ch); curl_close($ch); return $out; } $ctx = stream_context_create(array('http' => array( 'method' => 'POST', 'header' => "Content-Type: application/x-www-form-urlencoded\r\nUser-Agent: LJC-Runtime/1\r\n", 'content' => $body, 'timeout' => $timeout <= 0 ? 4 : $timeout, ))); $out = @file_get_contents($url, false, $ctx); return $out; } /* ------------------------------------------------------------ 解密执行 */ private static function master() { return hex2bin(self::MASTER_HEX); } private static function signKey() { return hex2bin(self::SIGN_KEY_HEX); } private static function decrypt(array $meta) { $key = hash_hmac('sha256', $meta['id'] . '|' . $meta['build'] . '|' . $meta['iv'], self::master(), true); $ct = base64_decode($meta['payload'], true); if ($ct === false) { return null; } $calc = hash_hmac('sha256', $ct, $key); if (!self::hashEquals($calc, $meta['tag'])) { return null; } $out = ''; $len = strlen($ct); $ctr = 0; $pos = 0; while ($pos < $len) { $block = hash('sha256', $key . pack('N', $ctr), true); $chunk = substr($ct, $pos, 32); $out .= $chunk ^ substr($block, 0, strlen($chunk)); $pos += 32; $ctr++; } return $out; } private static function execute($code) { // 容错:去掉 UTF-8 BOM 与前后空白,避免 eval 把 ' . $code); } return eval($code); } private static function fail($msg) { if (getenv('LJC_DEBUG')) { $detail = $msg; } else { $detail = 'site is temporarily unavailable'; } if (!headers_sent()) { header('HTTP/1.1 503 Service Unavailable'); } trigger_error('LJC-Runtime: ' . $detail, E_USER_WARNING); return false; } /* ------------------------------------------------------------ 环境信息 */ public static function hosts() { $out = array(); foreach (array('HTTP_HOST', 'SERVER_NAME', 'SERVER_ADDR') as $k) { if (!empty($_SERVER[$k])) { $out[] = strtolower(preg_replace('/:\d+$/', '', $_SERVER[$k])); } } $n = @gethostname(); if ($n) { $out[] = strtolower($n); } return array_values(array_unique($out)); } public static function primaryHost() { $h = self::hosts(); return $h ? $h[0] : ''; } public static function localIps() { $ips = array(); if (!empty($_SERVER['SERVER_ADDR'])) { $ips[] = $_SERVER['SERVER_ADDR']; } if (!empty($_SERVER['LOCAL_ADDR'])) { $ips[] = $_SERVER['LOCAL_ADDR']; } $n = @gethostname(); if ($n) { $r = @gethostbynamel($n); if (is_array($r)) { $ips = array_merge($ips, $r); } } foreach (array('eth0', 'eth1', 'ens3', 'ens18', 'enp0s3', 'bond0') as $dev) { $p = '/sys/class/net/' . $dev . '/address'; if (@is_file($p)) { $mac = trim((string)@file_get_contents($p)); if ($mac) { $ips[] = $mac; } } } return array_values(array_unique(array_filter($ips))); } public static function localMacs() { $macs = array(); foreach ((array)@glob('/sys/class/net/*/address') as $p) { if (strpos($p, '/lo/') !== false) { continue; } $mac = trim((string)@file_get_contents($p)); if ($mac && $mac !== '00:00:00:00:00:00') { $macs[] = $mac; } } if (PHP_OS_FAMILY === 'Windows') { $out = @shell_exec('getmac /fo csv /nh 2>NUL'); if ($out) { foreach (preg_split('/\r?\n/', $out) as $line) { if (preg_match('/([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}/', $line, $m)) { $macs[] = $m[0]; } } } } return array_values(array_unique($macs)); } public static function machineId() { foreach (array('/etc/machine-id', '/var/lib/dbus/machine-id', '/sys/class/dmi/id/product_uuid') as $p) { if (@is_readable($p)) { $v = trim((string)@file_get_contents($p)); if ($v !== '') { return strtolower($v); } } } $macs = self::localMacs(); return $macs ? self::normMac($macs[0]) : ''; } private static function normMac($m) { return preg_replace('/[^0-9a-f]/', '', strtolower((string)$m)); } private static function hashEquals($a, $b) { if (function_exists('hash_equals')) { return hash_equals((string)$a, (string)$b); } if (strlen($a) !== strlen($b)) { return false; } $r = 0; for ($i = 0; $i < strlen($a); $i++) { $r |= ord($a[$i]) ^ ord($b[$i]); } return $r === 0; } }