Files
zyt/artifacts/private_ai_app_identity_20261027.php
2026-10-27 14:03:35 +08:00

118 lines
6.5 KiB
PHP

<?php
declare(strict_types=1);
// Private read-only app-key identity check. Never install under server/public.
// GET /info: https://docs.dify.ai/en/api-reference/applications/get-app-info
ini_set('display_errors', '0');
ini_set('log_errors', '0');
header('Content-Type: application/json');
if (PHP_SAPI !== 'cgi-fcgi' || ($_SERVER['REMOTE_ADDR'] ?? '') !== '127.0.0.1'
|| ($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST'
|| (int) ($_SERVER['CONTENT_LENGTH'] ?? 0) < 1 || (int) ($_SERVER['CONTENT_LENGTH'] ?? 0) > 128) {
http_response_code(403); echo '{"ok":false,"error_code":"PRIVATE_FASTCGI_POST_REQUIRED"}'; exit;
}
$started = hrtime(true);
$profile = null;
$httpStatus = 0;
$handle = null;
$out = null;
try {
$input = json_decode((string) file_get_contents('php://input', false, null, 0, 129), true, 4, JSON_THROW_ON_ERROR);
if (!is_array($input) || array_keys($input) !== ['profile'] || !in_array($input['profile'], ['qwen', 'openai'], true)) {
throw new \RuntimeException('APP_INFO_INPUT_INVALID');
}
$profile = $input['profile']; unset($input);
$root = dirname(__DIR__) . '/server';
require $root . '/vendor/autoload.php';
$app = new \think\App($root . '/');
require_once $root . '/vendor/topthink/framework/src/helper.php';
$app->env->load($root . '/.env');
$cfg = require $root . '/config/prescription_ai.php';
$app->config->set($cfg, 'prescription_ai');
$base = $cfg['base_url'] ?? null;
$key = $cfg['models'][$profile]['api_key'] ?? null;
$ca = $cfg['required_attachment_upload']['ca_file'] ?? '';
$parts = is_string($base) ? parse_url($base) : false;
if (($cfg['enable'] ?? null) !== true || !is_string($base) || strlen($base) > 2048
|| preg_match('/[\x00-\x20\x7f\\\\]/', $base) !== 0 || filter_var($base, FILTER_VALIDATE_URL) === false
|| !is_array($parts) || !in_array(strtolower($parts['scheme'] ?? ''), ['http', 'https'], true) || empty($parts['host'])
|| count(array_intersect(['user', 'pass', 'query', 'fragment'], array_keys($parts))) !== 0
|| preg_match('~\A(?:/[A-Za-z0-9_-]+)*/?\z~', $parts['path'] ?? '') !== 1
|| preg_match('~/chat/completions(?:/|$)~i', $parts['path'] ?? '') === 1
|| !is_string($key) || $key === '' || strlen($key) > 4096 || preg_match('/[^\x21-\x7e]/', $key) !== 0
|| !is_string($ca) || preg_match('/[\x00-\x1f\x7f]/', $ca) !== 0
|| ($ca !== '' && (!is_file($ca) || !is_readable($ca)))) {
throw new \RuntimeException('APP_INFO_CONFIG_INVALID');
}
$apiRoot = rtrim($base, '/');
if (str_ends_with($apiRoot, '/chat-messages')) $apiRoot = substr($apiRoot, 0, -strlen('/chat-messages'));
$protocol = strtolower($parts['scheme']) === 'https' ? CURLPROTO_HTTPS : CURLPROTO_HTTP;
unset($cfg, $base, $parts);
if (!function_exists('curl_init')) throw new \RuntimeException('APP_INFO_CURL_UNAVAILABLE');
$handle = curl_init();
if ($handle === false) { $handle = null; throw new \RuntimeException('APP_INFO_TRANSPORT_FAILED'); }
$body = '';
$oversize = false;
$deadline = hrtime(true) + 3000000000;
$options = [
CURLOPT_URL => $apiRoot . '/info',
CURLOPT_HTTPGET => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Accept: application/json'],
CURLOPT_RETURNTRANSFER => false,
CURLOPT_HEADER => false,
CURLOPT_VERBOSE => false,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_MAXREDIRS => 0,
CURLOPT_PROXY => '',
CURLOPT_NOPROXY => '*',
CURLOPT_PROTOCOLS => $protocol,
CURLOPT_REDIR_PROTOCOLS => $protocol,
CURLOPT_NETRC => CURL_NETRC_IGNORED,
CURLOPT_UNRESTRICTED_AUTH => false,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_TIMEOUT_MS => 3000,
CURLOPT_CONNECTTIMEOUT_MS => 3000,
CURLOPT_NOSIGNAL => true,
CURLOPT_WRITEFUNCTION => static function ($unused, string $chunk) use (&$body, &$oversize, $deadline): int {
if (hrtime(true) >= $deadline) return 0;
if (strlen($chunk) > 32768 - strlen($body)) { $body = ''; $oversize = true; return 0; }
$body .= $chunk; return strlen($chunk);
},
];
if ($ca !== '') $options[CURLOPT_CAINFO] = $ca;
if (!curl_setopt_array($handle, $options)) throw new \RuntimeException('APP_INFO_TRANSPORT_FAILED');
unset($options, $key, $apiRoot, $ca);
$executed = curl_exec($handle);
$errno = curl_errno($handle);
$httpStatus = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
if ($httpStatus < 100 || $httpStatus > 599) $httpStatus = 0;
if ($oversize) throw new \RuntimeException('APP_INFO_RESPONSE_TOO_LARGE');
if (hrtime(true) >= $deadline || $errno === CURLE_OPERATION_TIMEDOUT) throw new \RuntimeException('APP_INFO_TIMEOUT');
if ($executed !== true || $errno !== 0) throw new \RuntimeException('APP_INFO_TRANSPORT_FAILED');
if ($httpStatus !== 200) throw new \RuntimeException('APP_INFO_HTTP_ERROR');
$decoded = json_decode($body, true, 16, JSON_THROW_ON_ERROR); unset($body);
$name = is_array($decoded) ? ($decoded['name'] ?? null) : null;
$mode = is_array($decoded) ? ($decoded['mode'] ?? null) : null;
unset($decoded);
if (!is_string($name) || $name === '' || strlen($name) > 200 || preg_match('//u', $name) !== 1
|| preg_match('/[\x00-\x1f\x7f<>]/', $name) !== 0
|| ($mode !== null && !in_array($mode, ['chat', 'advanced-chat', 'workflow', 'completion', 'agent-chat', 'agent'], true))) {
throw new \RuntimeException('APP_INFO_RESPONSE_INVALID');
}
// No app ID, description, tags, author, URLs, headers, inputs or outputs leave.
// Older Dify releases may omit mode; missing mode remains explicitly null.
$out = ['ok' => true, 'profile' => $profile, 'app_name' => $name, 'app_mode' => $mode, 'http_status' => $httpStatus];
} catch (\Throwable $error) {
$codes = ['APP_INFO_INPUT_INVALID', 'APP_INFO_CONFIG_INVALID', 'APP_INFO_CURL_UNAVAILABLE', 'APP_INFO_TRANSPORT_FAILED',
'APP_INFO_RESPONSE_TOO_LARGE', 'APP_INFO_TIMEOUT', 'APP_INFO_HTTP_ERROR', 'APP_INFO_RESPONSE_INVALID'];
$code = in_array($error->getMessage(), $codes, true) ? $error->getMessage() : 'APP_INFO_DIAGNOSTIC_FAILED';
$out = ['ok' => false, 'profile' => $profile, 'http_status' => $httpStatus, 'error_code' => $code];
} finally {
if ($handle !== null) curl_close($handle);
unset($key, $cfg, $body, $decoded, $options, $app);
}
$out['elapsed_ms'] = max(0, (int) floor((hrtime(true) - $started) / 1e6));
echo json_encode($out, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);