mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-07 02:37:14 +00:00
fix(frontend): add the missing AmneziaWG config download on the sub page
The subscription page already gave WireGuard links their own "Config" block (copy/download/QR of the actual .conf, via wireguardConfigFromLink reversing the wireguard:// query params) but had no equivalent for AmneziaWG's vpn:// links -- its isWireguardLink gate never matched them, and no reverse-parse helper existed for this page specifically. Every other surface (InboundInfoModal, ClientInfoModal, ClientQrModal) already had this parity; this was the one page that didn't. Fixed by adding amneziawgConfigFromLink (inbound-link.ts), simpler than its WireGuard counterpart since a vpn:// payload already *is* the plain .conf text -- just base64url-decode it, no query-param reconstruction needed -- and wiring it into SubPage.tsx alongside the existing WireGuard block, reusing the same pages.clients.amneziaWgConfig label the other three surfaces already use.
This commit is contained in:
@@ -1064,6 +1064,34 @@ export function wireguardConfigFromLink(link: string, fallbackRemark = ''): stri
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// Reverse of toBase64Url above -- recovers a vpn:// link's plain .conf
|
||||
// payload for display/copy/download/QR, the AmneziaWG counterpart of
|
||||
// wireguardConfigFromLink. Simpler than that function: a vpn:// link's
|
||||
// payload already *is* the .conf text (see genAmneziaWGLink's own doc
|
||||
// comment), so there's nothing to reconstruct from query params -- just
|
||||
// decode. Mirrors link-label.tsx's own private fromBase64Url (used there
|
||||
// only to pull the remark/port back out for the tag label); duplicated
|
||||
// rather than imported since both are tiny, self-contained, and each
|
||||
// file already owns the matching encode or decode half of this pair.
|
||||
function fromBase64Url(value: string): string {
|
||||
const b64 = value.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4);
|
||||
const binary = atob(padded);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
||||
return new TextDecoder().decode(bytes);
|
||||
}
|
||||
|
||||
export function amneziawgConfigFromLink(link: string): string {
|
||||
const trimmed = link.trim();
|
||||
if (!trimmed.startsWith('vpn://')) return '';
|
||||
try {
|
||||
return fromBase64Url(trimmed.slice('vpn://'.length));
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export type { WireguardInboundPeer };
|
||||
|
||||
function isUnixSocketListen(listen: string): boolean {
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
|
||||
import { ClipboardManager, IntlUtil, LanguageManager } from '@/utils';
|
||||
import { isPostQuantumLink, wireguardConfigFromLink } from '@/lib/xray/inbound-link';
|
||||
import { amneziawgConfigFromLink, isPostQuantumLink, wireguardConfigFromLink } from '@/lib/xray/inbound-link';
|
||||
import { LinkTags, parseLinkParts } from '@/lib/xray/link-label';
|
||||
import ConfigBlock from '@/components/clients/ConfigBlock';
|
||||
import { setMessageInstance } from '@/utils/messageBus';
|
||||
@@ -451,6 +451,7 @@ export default function SubPage() {
|
||||
const qrLabel = parts?.remark || rowTitle;
|
||||
const canQr = !isPostQuantumLink(link);
|
||||
const isWireguardLink = link.startsWith('wireguard://') || link.startsWith('wg://');
|
||||
const isAmneziawgLink = link.startsWith('vpn://');
|
||||
return (
|
||||
<Fragment key={link}>
|
||||
<div className="sub-link-row">
|
||||
@@ -506,6 +507,15 @@ export default function SubPage() {
|
||||
tagColor="cyan"
|
||||
/>
|
||||
)}
|
||||
{isAmneziawgLink && (
|
||||
<ConfigBlock
|
||||
label={t('pages.clients.amneziaWgConfig')}
|
||||
text={amneziawgConfigFromLink(link)}
|
||||
fileName={`${rowTitle || 'peer'}.conf`}
|
||||
qrRemark={rowTitle}
|
||||
tagColor="purple"
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
amneziawgConfigFromLink,
|
||||
genAmneziaWGConfig,
|
||||
genAmneziaWGLink,
|
||||
genHysteriaLink,
|
||||
@@ -409,6 +410,29 @@ describe('genAmneziaWGLink vpn:// scheme', () => {
|
||||
it('returns an empty string when the peer index has no client', () => {
|
||||
expect(genAmneziaWGLink({ ...input, peerIndex: 5 })).toBe('');
|
||||
});
|
||||
|
||||
// The subscription page's own reverse of the above: recovers a vpn://
|
||||
// link's .conf text for the same copy/download/QR "Config" block
|
||||
// WireGuard already gets there (wireguardConfigFromLink's AmneziaWG
|
||||
// counterpart) -- found missing from that page in production (no
|
||||
// download-config affordance for AmneziaWG links, unlike WireGuard's),
|
||||
// even though every other surface in the panel (InboundInfoModal,
|
||||
// ClientInfoModal, ClientQrModal) already had parity.
|
||||
it('amneziawgConfigFromLink round-trips genAmneziaWGLink byte-identical to genAmneziaWGConfig', () => {
|
||||
const link = genAmneziaWGLink(input);
|
||||
expect(amneziawgConfigFromLink(link)).toBe(genAmneziaWGConfig(input));
|
||||
});
|
||||
});
|
||||
|
||||
describe('amneziawgConfigFromLink edge cases', () => {
|
||||
it('returns an empty string for a non-vpn:// link', () => {
|
||||
expect(amneziawgConfigFromLink('wireguard://abc')).toBe('');
|
||||
expect(amneziawgConfigFromLink('')).toBe('');
|
||||
});
|
||||
|
||||
it('returns an empty string for an unparseable vpn:// payload', () => {
|
||||
expect(amneziawgConfigFromLink('vpn://not-valid-base64url!!!')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveAddr precedence', () => {
|
||||
|
||||
Reference in New Issue
Block a user