mirror of
https://github.com/maeneko/forgetting.git
synced 2026-08-25 15:24:26 +00:00
Protocol generation is now derived from awg1.conf rather than stored: an interface speaks 3.1 when HeaderProtectionKey is set and 2.0 otherwise. The 3.0/3.1-only keys (HeaderProtectionKey, ContentPaddingAddition, the Rekey/Reject/Keepalive timers, MaxHandshakeAttempts, RandomTrailers, DisableCookies) are read from the conf and emitted only when non-empty, so 2.0 output stays byte-identical — verified by diffing both the client .conf and the full vpn:// string against the previous implementation. Changing generation invalidates every issued vpn:// key, so add POST /api/users/reissue. It recovers the client private key from inside the stored blob (the only place it exists), keeping ip/pub_key/psk_key intact, so nothing changes on the wire and users only need to re-import. users gains key_gen and vpn_key_prev, and users.db is snapshotted before the pass. install.sh gates 3.1 on module 3.x and kernel >= 5.5 (header protection needs the chacha library API, absent before 5.5) and falls back to writing a 2.0 conf instead of aborting, so older kernels keep working as before. HeaderProtectionKey is generated with awg genpsk and preserved on KEEP_DATA. The panel marks keys issued on a different generation and offers to reissue them; /api/v1 now returns gen alongside name/ip/vpn_key. Also add MIT license headers across the awg-ui sources.
98 lines
3.6 KiB
TypeScript
98 lines
3.6 KiB
TypeScript
// Copyright (c) 2026 Ivan Vasilev
|
|
// This source code is licensed under the MIT license found in the
|
|
// LICENSE file in the root directory of this source tree.
|
|
// Общие типы, константы и утилиты для всех вкладок панели.
|
|
import axios from 'axios';
|
|
// Поколение протокола AmneziaWG, на параметрах которого выдан vpn:// ключ.
|
|
export type AwgGen = '2' | '3.1';
|
|
export interface User {
|
|
name: string;
|
|
ip: string;
|
|
pub_key: string;
|
|
vpn_key: string;
|
|
key_gen: AwgGen;
|
|
online: boolean;
|
|
lastHandshake: number;
|
|
rx?: number;
|
|
tx?: number;
|
|
}
|
|
export interface ServerInfo {
|
|
name: string;
|
|
ip: string;
|
|
peers: number;
|
|
gen: AwgGen;
|
|
}
|
|
export interface ApiKey {
|
|
id: number;
|
|
label: string;
|
|
prefix: string;
|
|
server_id: number;
|
|
created_at: number;
|
|
last_used: number | null;
|
|
}
|
|
export interface PageProps {
|
|
token: string;
|
|
showMsg: (text: string) => void;
|
|
}
|
|
export const TOKEN_KEY = 'awg_token';
|
|
export const THEME_KEY = 'awg_theme';
|
|
export async function apiFetch(method: string, path: string, token: string, body?: object): Promise<any> {
|
|
const r = await axios({ method, url: path, headers: { Authorization: `Bearer ${token}` }, data: body });
|
|
return r.data;
|
|
}
|
|
|
|
export async function vpnKeyToConf(vpnKey: string): Promise<string> {
|
|
const b64 = vpnKey.replace('vpn://', '').replace(/-/g, '+').replace(/_/g, '/');
|
|
const raw = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
|
|
const stream = new Blob([raw.slice(4)]).stream().pipeThrough(new DecompressionStream('deflate'));
|
|
const json = await new Response(stream).text();
|
|
const data = JSON.parse(json);
|
|
const awg = data.containers[0].awg;
|
|
try {
|
|
const lc = JSON.parse(awg.last_config || '{}');
|
|
return (lc.config || '')
|
|
.replace('$PRIMARY_DNS', data.dns1 || '1.1.1.1')
|
|
.replace('$SECONDARY_DNS', data.dns2 || '1.0.0.1');
|
|
} catch {
|
|
return (awg.last_config || '')
|
|
.replace('$PRIMARY_DNS', data.dns1 || '1.1.1.1')
|
|
.replace('$SECONDARY_DNS', data.dns2 || '1.0.0.1');
|
|
}
|
|
}
|
|
export function downloadFile(filename: string, text: string) {
|
|
const url = URL.createObjectURL(new Blob([text], { type: 'text/plain;charset=utf-8' }));
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
export async function copyText(text: string) {
|
|
if (navigator.clipboard) {
|
|
await navigator.clipboard.writeText(text);
|
|
} else {
|
|
const ta = document.createElement('textarea');
|
|
ta.value = text;
|
|
ta.style.cssText = 'position:fixed;opacity:0';
|
|
document.body.appendChild(ta);
|
|
ta.select();
|
|
document.execCommand('copy');
|
|
document.body.removeChild(ta);
|
|
}
|
|
}
|
|
export function bytes(n: number | undefined): string {
|
|
if (!n) return '—';
|
|
if (n < 1024) return n + ' B';
|
|
if (n < 1_048_576) return (n / 1024).toFixed(1) + ' KB';
|
|
if (n < 1_073_741_824) return (n / 1_048_576).toFixed(1) + ' MB';
|
|
return (n / 1_073_741_824).toFixed(2) + ' GB';
|
|
}
|
|
export function timeAgo(ts: number | undefined): string {
|
|
if (!ts) return '—';
|
|
const sec = Math.floor(Date.now() / 1000) - ts;
|
|
if (sec < 60) return sec + ' сек';
|
|
if (sec < 3600) return Math.floor(sec / 60) + ' мин';
|
|
return Math.floor(sec / 3600) + ' ч';
|
|
} |