118 lines
6.5 KiB
PHP
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);
|