feat: update QR code generation to use vpn:// format with JSON and zlib compression
This commit is contained in:
+52
-12
@@ -108,7 +108,7 @@ class QrUtil
|
|||||||
* Header (8 bytes): version (4) + length (4) + config text
|
* Header (8 bytes): version (4) + length (4) + config text
|
||||||
* No compression, no JSON wrapper
|
* No compression, no JSON wrapper
|
||||||
*/
|
*/
|
||||||
private static function encodeSimpleConf(string $confText): string
|
public static function encodeSimpleConf(string $confText): string
|
||||||
{
|
{
|
||||||
$version = 0x07C00200; // Amnezia magic version number (updated for newer app compatibility)
|
$version = 0x07C00200; // Amnezia magic version number (updated for newer app compatibility)
|
||||||
$length = strlen($confText);
|
$length = strlen($confText);
|
||||||
@@ -119,21 +119,38 @@ class QrUtil
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Encode config in vpn:// URL format used by newer Amnezia app
|
* Encode config in vpn:// URL format used by newer Amnezia app
|
||||||
* Format: vpn://<base64url(header + compressed config)>
|
* Format: vpn://<base64url(uint32 BE uncompressed_len + zlib compressed JSON)>
|
||||||
* Header: version (4 bytes) + uncompressed length (4 bytes)
|
*
|
||||||
* Payload: gzipped config text
|
* Structure:
|
||||||
|
* - 4 bytes: uint32 BE — length of JSON after decompression
|
||||||
|
* - N bytes: zlib-compressed JSON (level 9, magic 0x78 0xDA)
|
||||||
|
* - Entire block encoded in Base64url without padding
|
||||||
*/
|
*/
|
||||||
private static function encodeVpnUrlConf(string $confText): string
|
public static function encodeVpnUrlConf(string $confText, string $protocolSlug = ''): string
|
||||||
{
|
{
|
||||||
// Based on real Amnezia app format - no compression, just header + config
|
// Build JSON envelope like the real Amnezia app
|
||||||
$version = 0x07C00200; // Amnezia magic version number
|
$envelope = self::buildOldEnvelopeFromConf($confText, $protocolSlug);
|
||||||
$length = strlen($confText);
|
$jsonBytes = json_encode($envelope, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||||
|
if ($jsonBytes === false) {
|
||||||
|
throw new RuntimeException('json_encode failed');
|
||||||
|
}
|
||||||
|
|
||||||
// Header: version (4 bytes big-endian) + length (4 bytes big-endian)
|
$jsonBytes = (string) $jsonBytes;
|
||||||
$header = pack('N2', $version, $length);
|
$uncompressedLength = strlen($jsonBytes);
|
||||||
$payload = $header . $confText;
|
|
||||||
|
|
||||||
// Return just the base64url encoded payload (vpn:// prefix added by caller if needed)
|
// Compress with zlib level 9 (produces 0x78 0xDA header)
|
||||||
|
$compressed = gzcompress($jsonBytes, 9);
|
||||||
|
if ($compressed === false) {
|
||||||
|
throw new RuntimeException('gzcompress failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header: uint32 BE with uncompressed length
|
||||||
|
$header = pack('N', $uncompressedLength);
|
||||||
|
|
||||||
|
// Payload: header + compressed data
|
||||||
|
$payload = $header . $compressed;
|
||||||
|
|
||||||
|
// Base64url encode without padding
|
||||||
return self::urlsafe_b64_encode($payload);
|
return self::urlsafe_b64_encode($payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +166,29 @@ class QrUtil
|
|||||||
return self::encodeOldPayloadFromJson($jsonPayload);
|
return self::encodeOldPayloadFromJson($jsonPayload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode config in Amnezia app format for vpn:// URL
|
||||||
|
* Format: 3-byte length (big-endian) + zlib compressed JSON data
|
||||||
|
* This matches the real Amnezia app vpn:// URL format
|
||||||
|
*/
|
||||||
|
public static function encodeAmneziaVpnUrl(string $confText, string $protocolSlug = ''): string
|
||||||
|
{
|
||||||
|
// Build JSON envelope like the real Amnezia app
|
||||||
|
$payload = self::buildOldEnvelopeFromConf($confText, $protocolSlug);
|
||||||
|
$jsonPayload = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||||
|
|
||||||
|
// Compress the JSON
|
||||||
|
$compressed = gzcompress($jsonPayload, 9);
|
||||||
|
if ($compressed === false) {
|
||||||
|
throw new RuntimeException('gzcompress failed');
|
||||||
|
}
|
||||||
|
$length = strlen($compressed);
|
||||||
|
// 3-byte length in big-endian
|
||||||
|
$lengthBytes = pack('N', $length);
|
||||||
|
$header = substr($lengthBytes, 1); // Take last 3 bytes
|
||||||
|
return self::urlsafe_b64_encode($header . $compressed);
|
||||||
|
}
|
||||||
|
|
||||||
private static function resolveServerDescription(?string $endpointHost): string
|
private static function resolveServerDescription(?string $endpointHost): string
|
||||||
{
|
{
|
||||||
$desc = (string) ($endpointHost ?? '');
|
$desc = (string) ($endpointHost ?? '');
|
||||||
|
|||||||
+2
-9
@@ -1269,15 +1269,8 @@ class VpnClient
|
|||||||
return self::generateQRCode($config, $protocolSlug);
|
return self::generateQRCode($config, $protocolSlug);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For AWG2, use compressed format (second QR code format)
|
// For AWG2 and other WireGuard/AWG, use vpn:// URL format with JSON + zlib
|
||||||
if ($protocolSlug === 'awg2') {
|
$payloadVpn = QrUtil::encodeVpnUrlConf($config, $protocolSlug);
|
||||||
$payloadCompressed = QrUtil::encodeCompressedConf($config, $protocolSlug);
|
|
||||||
$dataUri = QrUtil::pngBase64($payloadCompressed);
|
|
||||||
return $dataUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For other WireGuard/AWG, use vpn:// URL format
|
|
||||||
$payloadVpn = QrUtil::encodeVpnUrlPayload($config, $protocolSlug);
|
|
||||||
$dataUri = QrUtil::pngBase64($payloadVpn);
|
$dataUri = QrUtil::pngBase64($payloadVpn);
|
||||||
return $dataUri;
|
return $dataUri;
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
|
|||||||
+2
-2
@@ -1172,9 +1172,9 @@ Router::get('/clients/{id}', function ($params) {
|
|||||||
try {
|
try {
|
||||||
$qrCodeVpnUrl = VpnClient::generateQRCodeVpnUrl($clientData['config'], 'awg2');
|
$qrCodeVpnUrl = VpnClient::generateQRCodeVpnUrl($clientData['config'], 'awg2');
|
||||||
|
|
||||||
// Generate vpn:// URL string (add vpn:// prefix)
|
// Generate vpn:// URL string using vpn:// format (JSON + zlib)
|
||||||
require_once __DIR__ . '/../inc/QrUtil.php';
|
require_once __DIR__ . '/../inc/QrUtil.php';
|
||||||
$vpnUrlConfig = 'vpn://' . QrUtil::encodeVpnUrlPayload($clientData['config'], 'awg2');
|
$vpnUrlConfig = 'vpn://' . QrUtil::encodeVpnUrlConf($clientData['config'], 'awg2');
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
// Ignore errors, just don't show the second QR
|
// Ignore errors, just don't show the second QR
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user