feat: bind explicitly authorized HTTP text extraction channel
This commit is contained in:
@@ -75,8 +75,8 @@ final class FollowupAudioProviderConfig
|
||||
catch (FollowupAudioException $error) { return false; }
|
||||
$verified = (string) ($settings['providers'][$profile]['verified_fingerprint'] ?? '');
|
||||
$secure = $provider['driver'] === 'asr_then_llm'
|
||||
? self::secureEndpoint($provider['asr']['base_url'], $provider['allow_loopback_tunnel'])
|
||||
&& self::secureEndpoint($provider['extraction']['base_url'], $provider['allow_loopback_tunnel'])
|
||||
? self::stageEndpointAllowed($provider, 'asr')
|
||||
&& self::stageEndpointAllowed($provider, 'extraction')
|
||||
: str_starts_with($provider['base_url'], 'https://');
|
||||
return $secure && preg_match('/^[a-f0-9]{64}$/D', $verified)
|
||||
&& hash_equals($provider['fingerprint'], $verified);
|
||||
@@ -91,7 +91,7 @@ final class FollowupAudioProviderConfig
|
||||
catch (FollowupAudioException $error) { return false; }
|
||||
$fingerprint = (string) ($settings['providers'][$profile]['preview_verified_fingerprint'] ?? '');
|
||||
$secure = $provider['driver'] === 'asr_then_llm'
|
||||
? self::secureEndpoint($provider['asr']['base_url'], $provider['allow_loopback_tunnel']) && self::secureEndpoint($provider['extraction']['base_url'], $provider['allow_loopback_tunnel'])
|
||||
? self::stageEndpointAllowed($provider, 'asr') && self::stageEndpointAllowed($provider, 'extraction')
|
||||
: str_starts_with($provider['base_url'], 'https://');
|
||||
return $secure && preg_match('/^[a-f0-9]{64}$/D', $fingerprint) && hash_equals($provider['fingerprint'], $fingerprint);
|
||||
}
|
||||
@@ -117,6 +117,24 @@ final class FollowupAudioProviderConfig
|
||||
&& in_array($parts['host'] ?? '', ['127.0.0.1', '[::1]'], true));
|
||||
}
|
||||
|
||||
/** A server-only exact HTTP text endpoint is an explicit exception, not verified TLS. */
|
||||
public static function stageEndpointAllowed(array $provider, string $stage): bool
|
||||
{
|
||||
if (!in_array($stage, ['asr', 'extraction'], true)) { return false; }
|
||||
$part = $provider[$stage] ?? [];
|
||||
$base = $part['base_url'] ?? '';
|
||||
if (!is_string($base)) { return false; }
|
||||
if (self::secureEndpoint($base, ($provider['allow_loopback_tunnel'] ?? false) === true)) { return true; }
|
||||
$pin = $part['http_base_opt_in'] ?? '';
|
||||
$url = parse_url($base);
|
||||
return $stage === 'extraction' && ($part['protocol'] ?? 'openai') === 'openai'
|
||||
&& is_string($pin) && $pin !== '' && hash_equals($pin, $base)
|
||||
&& is_array($url) && ($url['scheme'] ?? '') === 'http' && !empty($url['host'])
|
||||
&& !isset($url['user']) && !isset($url['pass']) && !isset($url['query']) && !isset($url['fragment'])
|
||||
&& !preg_match('/[\\x00-\\x20\\x7f\\\\%]|\\/(?:\\.{1,2})(?:\\/|$)/', $base)
|
||||
&& str_ends_with($base, '/v1');
|
||||
}
|
||||
|
||||
private static function pipeline(array $provider, array $settings, string $profile): array
|
||||
{
|
||||
$allowLoopback = ($provider['allow_loopback_tunnel'] ?? false) === true;
|
||||
@@ -143,10 +161,19 @@ final class FollowupAudioProviderConfig
|
||||
$original = rtrim((string) ($input['base_url'] ?? ''), '/');
|
||||
if (str_ends_with($original, $endpoint)) { $base = substr($original, 0, -strlen($endpoint)); }
|
||||
}
|
||||
if (!self::secureEndpoint($base, $allowLoopback)) { throw new FollowupAudioException('HTTPS_REQUIRED'); }
|
||||
$resolved[$stage] = ['base_url' => $base, 'api_key' => $part['api_key'], 'model' => $part['model']];
|
||||
// Default/explicit OpenAI retains its previous exact fingerprint representation.
|
||||
if ($protocol !== 'openai') { $resolved[$stage] += ['protocol' => $protocol, 'binding_revision' => $revision]; }
|
||||
$pin = $input['http_base_opt_in'] ?? '';
|
||||
if (!is_string($pin)) { throw new FollowupAudioException('CONFIG_INVALID'); }
|
||||
if ($pin !== '') {
|
||||
if ($stage !== 'extraction' || $protocol !== 'openai' || $pin !== $base || !str_starts_with($base, 'http://')) {
|
||||
throw new FollowupAudioException('CONFIG_INVALID');
|
||||
}
|
||||
$resolved[$stage]['http_base_opt_in'] = $pin;
|
||||
$resolved[$stage]['http_policy'] = 'exact-text-endpoint-v1';
|
||||
}
|
||||
if (!self::stageEndpointAllowed($resolved, $stage)) { throw new FollowupAudioException('HTTPS_REQUIRED'); }
|
||||
}
|
||||
$chunkSeconds = (int) ($settings['asr_chunk_seconds'] ?? 120);
|
||||
if ($chunkSeconds < 1 || $chunkSeconds > 120) { throw new FollowupAudioException('CONFIG_INVALID'); }
|
||||
|
||||
Reference in New Issue
Block a user