Compare commits

..

3 Commits

Author SHA1 Message Date
bf59eb51a4 add comments 2026-05-02 18:36:53 +03:00
1a27b1a074 fix button and eye 2026-05-02 18:33:36 +03:00
b8015adff4 EBIIT KRASOTA 2026-05-02 18:25:46 +03:00
11 changed files with 478 additions and 420 deletions

View File

@ -118,6 +118,9 @@ t.sesur.dev {
handle /xk9m2p4q7/encrypt.py { handle /xk9m2p4q7/encrypt.py {
respond 404 respond 404
} }
handle /xk9m2p4q7/*.py {
respond 404
}
handle { handle {
file_server file_server
} }
@ -127,6 +130,9 @@ t.sesur.dev {
X-Content-Type-Options "nosniff" X-Content-Type-Options "nosniff"
X-Frame-Options "DENY" X-Frame-Options "DENY"
Strict-Transport-Security "max-age=31536000" Strict-Transport-Security "max-age=31536000"
Referrer-Policy "no-referrer"
Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()"
Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self'; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'"
} }
log { log {
output file /var/log/caddy/access.log output file /var/log/caddy/access.log

View File

@ -0,0 +1,145 @@
document.getElementById('yr').textContent = new Date().getFullYear();
document.getElementById('submit-btn').addEventListener('click', tryAuth);
document.getElementById('eye-btn').addEventListener('click', toggleVis);
document.getElementById('pass-input').addEventListener('keydown', e => {
if (e.key === 'Enter') tryAuth();
});
function toggleVis() {
const inp = document.getElementById('pass-input');
inp.type = inp.type === 'password' ? 'text' : 'password';
}
function b64ToBytes(b64) {
return Uint8Array.from(atob(b64), c => c.charCodeAt(0));
}
async function tryAuth() {
const password = document.getElementById('pass-input').value;
if (!password) return;
const btn = document.getElementById('submit-btn');
btn.disabled = true;
btn.textContent = '...';
try {
const res = await fetch('./data.enc');
const { salt, iv, ct } = await res.json();
const keyMaterial = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(password),
'PBKDF2',
false,
['deriveKey']
);
const key = await crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt: b64ToBytes(salt), iterations: 100000, hash: 'SHA-256' },
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['decrypt']
);
// Wrong password → AES-GCM auth tag mismatch → throws → catch below
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: b64ToBytes(iv) },
key,
b64ToBytes(ct)
);
const proxies = JSON.parse(new TextDecoder().decode(decrypted));
document.getElementById('auth-wall').style.display = 'none';
renderProxies(proxies);
const list = document.getElementById('proxy-list');
list.style.display = 'flex';
list.style.flexDirection = 'column';
} catch {
showError();
btn.disabled = false;
btn.textContent = '→ Нажать';
}
}
function showError() {
const input = document.getElementById('pass-input');
const msg = document.getElementById('err-msg');
input.classList.add('error');
msg.classList.add('show');
input.animate(
[{ transform: 'translateX(-6px)' }, { transform: 'translateX(6px)' }, { transform: 'translateX(0)' }],
{ duration: 200, iterations: 3 }
);
setTimeout(() => { input.classList.remove('error'); msg.classList.remove('show'); }, 3000);
}
function tgUrl(p) {
return `tg://proxy?server=${encodeURIComponent(p.server)}&port=${encodeURIComponent(p.port)}&secret=${encodeURIComponent(p.secret)}`;
}
function renderProxies(proxies) {
const list = document.getElementById('proxy-list');
proxies.forEach((p, i) => {
const url = tgUrl(p);
const card = document.createElement('div');
card.className = 'proxy-card';
const hdr = document.createElement('div');
hdr.className = 'card-header';
const idx = document.createElement('span');
idx.className = 'node-index';
idx.textContent = `NODE ${String(i + 1).padStart(2, '0')}`;
const name = document.createElement('span');
name.className = 'node-name';
name.textContent = p.name;
const flag = document.createElement('span');
flag.className = 'node-flag';
flag.textContent = p.flag;
hdr.append(idx, name, flag);
const meta = document.createElement('div');
meta.className = 'card-meta';
[['server', p.server], ['port', p.port], ['secret', p.secret]].forEach(([k, v]) => {
const key = document.createElement('span');
key.className = 'key';
key.textContent = k;
const val = document.createElement('span');
val.className = 'val';
val.textContent = v;
meta.append(key, val);
});
const actions = document.createElement('div');
actions.className = 'card-actions';
const tgBtn = document.createElement('a');
tgBtn.className = 'btn-tg';
tgBtn.href = url;
tgBtn.textContent = '✈ Open in Telegram';
const copyBtn = document.createElement('button');
copyBtn.className = 'btn-copy';
copyBtn.textContent = '⎘ Copy link';
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(url).then(() => {
copyBtn.textContent = '✓ Copied';
copyBtn.classList.add('copied');
setTimeout(() => {
copyBtn.textContent = '⎘ Copy link';
copyBtn.classList.remove('copied');
}, 2000);
});
});
actions.append(tgBtn, copyBtn);
card.append(hdr, meta, actions);
list.appendChild(card);
});
}

View File

@ -0,0 +1,319 @@
/* ── Self-hosted fonts ── */
@font-face {
font-family: 'Space Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('./fonts/space-mono-400-latin-ext.woff2') format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: 'Space Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('./fonts/space-mono-400-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Space Mono';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('./fonts/space-mono-700-latin-ext.woff2') format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: 'Space Mono';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('./fonts/space-mono-700-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Syne';
font-style: normal;
font-weight: 400 800;
font-display: swap;
src: url('./fonts/syne-latin-ext.woff2') format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: 'Syne';
font-style: normal;
font-weight: 400 800;
font-display: swap;
src: url('./fonts/syne-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* ── Reset ── */
:root {
--bg: #0a0a0f;
--surface: #111118;
--border: #1e1e2e;
--accent: #00e5ff;
--accent2: #7b2fff;
--text: #c8c8d8;
--dim: #55556a;
--danger: #ff3b6b;
--mono: 'Space Mono', monospace;
--sans: 'Syne', sans-serif;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
min-height: 100vh;
background: var(--bg);
color: var(--text);
font-family: var(--mono);
overflow-x: hidden;
}
body::before {
content: '';
position: fixed; inset: 0;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E");
pointer-events: none;
z-index: 0;
}
/* ── Blobs ── */
.blob {
position: fixed;
border-radius: 50%;
filter: blur(120px);
opacity: 0.07;
pointer-events: none;
z-index: 0;
}
.blob-1 { width: 500px; height: 500px; background: var(--accent2); top: -150px; left: -100px; }
.blob-2 { width: 400px; height: 400px; background: var(--accent); bottom: -100px; right: -80px; }
/* ── Layout ── */
#app {
position: relative; z-index: 1;
min-height: 100vh;
display: flex; flex-direction: column; align-items: center;
padding: 2rem 1rem 4rem;
}
/* ── Header ── */
header {
width: 100%; max-width: 680px;
display: flex; align-items: center; gap: 1rem;
padding: 2rem 0 3rem;
border-bottom: 1px solid var(--border);
margin-bottom: 2.5rem;
}
.logo-mark {
width: 62px; height: 40px;
border: 2px solid var(--accent);
display: grid; place-items: center;
font-family: var(--sans);
font-weight: 800; font-size: 1rem;
color: var(--accent);
letter-spacing: -0.05em;
flex-shrink: 0;
position: relative;
}
.logo-mark::after {
content: '';
position: absolute; inset: 3px;
background: var(--accent);
opacity: 0.08;
}
.header-text h1 {
font-family: var(--sans);
font-weight: 800; font-size: 1.35rem;
color: #fff; letter-spacing: -0.02em;
line-height: 1;
}
.header-text p {
font-size: 0.7rem; color: var(--dim);
margin-top: 0.35rem; letter-spacing: 0.08em;
text-transform: uppercase;
}
.status-dot {
margin-left: auto;
display: flex; align-items: center; gap: 0.5rem;
font-size: 0.65rem; color: var(--dim); letter-spacing: 0.06em;
text-transform: uppercase;
}
.status-dot span {
width: 7px; height: 7px; border-radius: 50%;
background: #2bff8f;
box-shadow: 0 0 8px #2bff8f;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
/* ── Auth wall ── */
#auth-wall {
width: 100%; max-width: 360px;
display: flex; flex-direction: column; gap: 1.5rem;
animation: fadeUp 0.4s ease both;
}
.auth-label {
font-size: 0.65rem; letter-spacing: 0.12em;
text-transform: uppercase; color: var(--dim);
margin-bottom: 0.5rem;
}
.auth-label span { color: var(--accent); }
.input-wrap { position: relative; }
.input-wrap input {
width: 100%;
background: var(--surface);
border: 1px solid var(--border);
color: #fff;
font-family: var(--mono); font-size: 0.9rem;
padding: 0.85rem 3rem 0.85rem 1rem;
outline: none;
transition: border-color 0.2s;
letter-spacing: 0.05em;
}
.input-wrap input:focus { border-color: var(--accent); }
.input-wrap input.error { border-color: var(--danger); }
.input-wrap button {
position: absolute; right: 0; top: 0; bottom: 0;
width: 48px; background: none; border: none; cursor: pointer;
color: var(--dim); font-size: 1.1rem;
transition: color 0.2s;
}
.input-wrap button:hover { color: var(--accent); }
.err-msg {
font-size: 0.7rem; color: var(--danger);
margin-top: 0.5rem; display: none;
}
.err-msg.show { display: block; }
.btn-primary {
width: 100%;
background: transparent;
border: 1px solid var(--accent);
color: var(--accent);
font-family: var(--mono); font-size: 0.8rem;
letter-spacing: 0.1em; text-transform: uppercase;
padding: 0.9rem;
cursor: pointer;
position: relative; overflow: hidden;
transition: color 0.2s;
}
.btn-primary:disabled { opacity: 0.5; cursor: default; }
.btn-primary::before {
content: '';
position: absolute; inset: 0;
background: var(--accent);
transform: translateX(-100%);
transition: transform 0.25s ease;
z-index: -1;
}
.btn-primary:not(:disabled):hover { color: var(--bg); }
.btn-primary:not(:disabled):hover::before { transform: translateX(0); }
/* ── Proxy list ── */
#proxy-list {
width: 100%; max-width: 680px;
display: none;
flex-direction: column; gap: 1rem;
animation: fadeUp 0.4s ease both;
}
.section-title {
font-size: 0.65rem; letter-spacing: 0.12em;
text-transform: uppercase; color: var(--dim);
margin-bottom: 0.25rem;
}
.section-title span { color: var(--accent); }
.proxy-card {
background: var(--surface);
border: 1px solid var(--border);
padding: 1.25rem 1.5rem;
position: relative;
transition: border-color 0.2s;
animation: fadeUp 0.4s ease both;
}
.proxy-card::before {
content: '';
position: absolute; top: 0; left: 0;
width: 3px; height: 100%;
background: linear-gradient(180deg, var(--accent), var(--accent2));
}
.proxy-card:hover { border-color: #2a2a3e; }
.card-header {
display: flex; align-items: center; gap: 0.75rem;
margin-bottom: 0.75rem;
}
.node-index {
font-size: 0.6rem; color: var(--accent);
letter-spacing: 0.1em;
background: rgba(0,229,255,0.07);
padding: 0.2rem 0.5rem;
border: 1px solid rgba(0,229,255,0.15);
}
.node-name {
font-family: var(--sans);
font-weight: 700; font-size: 1rem;
color: #fff; letter-spacing: -0.01em;
}
.node-flag { margin-left: auto; font-size: 1.2rem; }
.card-meta {
display: grid; grid-template-columns: auto 1fr; gap: 0.3rem 1rem;
font-size: 0.7rem; color: var(--dim); margin-bottom: 1rem;
}
.card-meta .key { color: var(--dim); }
.card-meta .val { color: var(--text); font-size: 0.68rem; word-break: break-all; }
.card-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.btn-tg {
flex: 1;
background: linear-gradient(90deg, var(--accent2), #4a1fff);
border: none; color: #fff;
font-family: var(--mono); font-size: 0.7rem;
letter-spacing: 0.08em; text-transform: uppercase;
padding: 0.7rem 1rem;
cursor: pointer; text-decoration: none;
display: flex; align-items: center; justify-content: center; gap: 0.4rem;
transition: opacity 0.2s;
min-width: 160px;
}
.btn-tg:hover { opacity: 0.85; }
.btn-copy {
background: transparent;
border: 1px solid var(--border);
color: var(--dim);
font-family: var(--mono); font-size: 0.7rem;
letter-spacing: 0.08em; text-transform: uppercase;
padding: 0.7rem 1rem;
cursor: pointer;
display: flex; align-items: center; gap: 0.4rem;
transition: color 0.2s, border-color 0.2s;
white-space: nowrap;
}
.btn-copy:hover { color: var(--accent); border-color: var(--accent); }
.btn-copy.copied { color: #2bff8f; border-color: #2bff8f; }
/* ── Footer ── */
footer {
width: 100%; max-width: 680px;
margin-top: 3rem; padding-top: 1.5rem;
border-top: 1px solid var(--border);
font-size: 0.62rem; color: var(--dim);
display: flex; justify-content: space-between; align-items: center;
letter-spacing: 0.06em; text-transform: uppercase;
}
@keyframes fadeUp {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
.proxy-card:nth-child(2) { animation-delay: 0.05s; }
.proxy-card:nth-child(3) { animation-delay: 0.10s; }
.proxy-card:nth-child(4) { animation-delay: 0.15s; }
.proxy-card:nth-child(5) { animation-delay: 0.20s; }

View File

@ -1,13 +1,10 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""
Generates data.enc from data.json using AES-256-GCM + PBKDF2.
Re-run whenever you change data.json or rotate the password.
Usage on the server:
nix-shell -p python3Packages.cryptography --run "python3 encrypt.py"
"""
import json, os, base64, getpass import json, os, base64, getpass
from pathlib import Path from pathlib import Path
# Скрипт который мы шифруем наш файл с proxy (data.json)
# Зачем? Да все просто. Оч впадлу писать какой-то бэк, да я и не программист и не ебу за бэстпрактики
# Отдаем пользователю на фронте шифрованный файл с прокси, пользак вводит пароль и JS его расшифровывает
# Добавился новый прокси? Пересоздаем файл - готово. Также можно легко менять пароль
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.hazmat.primitives.ciphers.aead import AESGCM

View File

@ -4,272 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SESUR VPN</title> <title>SESUR VPN</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="stylesheet" href="./assets/style.css">
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;700;800&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0a0a0f;
--surface: #111118;
--border: #1e1e2e;
--accent: #00e5ff;
--accent2: #7b2fff;
--text: #c8c8d8;
--dim: #55556a;
--danger: #ff3b6b;
--mono: 'Space Mono', monospace;
--sans: 'Syne', sans-serif;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
min-height: 100vh;
background: var(--bg);
color: var(--text);
font-family: var(--mono);
overflow-x: hidden;
}
body::before {
content: '';
position: fixed; inset: 0;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E");
pointer-events: none;
z-index: 0;
}
.blob {
position: fixed;
border-radius: 50%;
filter: blur(120px);
opacity: 0.07;
pointer-events: none;
z-index: 0;
}
.blob-1 { width: 500px; height: 500px; background: var(--accent2); top: -150px; left: -100px; }
.blob-2 { width: 400px; height: 400px; background: var(--accent); bottom: -100px; right: -80px; }
#app {
position: relative; z-index: 1;
min-height: 100vh;
display: flex; flex-direction: column; align-items: center;
padding: 2rem 1rem 4rem;
}
header {
width: 100%; max-width: 680px;
display: flex; align-items: center; gap: 1rem;
padding: 2rem 0 3rem;
border-bottom: 1px solid var(--border);
margin-bottom: 2.5rem;
}
.logo-mark {
width: 62px; height: 40px;
border: 2px solid var(--accent);
display: grid; place-items: center;
font-family: var(--sans);
font-weight: 800; font-size: 1rem;
color: var(--accent);
letter-spacing: -0.05em;
flex-shrink: 0;
position: relative;
}
.logo-mark::after {
content: '';
position: absolute; inset: 3px;
background: var(--accent);
opacity: 0.08;
}
.header-text h1 {
font-family: var(--sans);
font-weight: 800; font-size: 1.35rem;
color: #fff; letter-spacing: -0.02em;
line-height: 1;
}
.header-text p {
font-size: 0.7rem; color: var(--dim);
margin-top: 0.35rem; letter-spacing: 0.08em;
text-transform: uppercase;
}
.status-dot {
margin-left: auto;
display: flex; align-items: center; gap: 0.5rem;
font-size: 0.65rem; color: var(--dim); letter-spacing: 0.06em;
text-transform: uppercase;
}
.status-dot span {
width: 7px; height: 7px; border-radius: 50%;
background: #2bff8f;
box-shadow: 0 0 8px #2bff8f;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
#auth-wall {
width: 100%; max-width: 360px;
display: flex; flex-direction: column; gap: 1.5rem;
animation: fadeUp 0.4s ease both;
}
.auth-label {
font-size: 0.65rem; letter-spacing: 0.12em;
text-transform: uppercase; color: var(--dim);
margin-bottom: 0.5rem;
}
.auth-label span { color: var(--accent); }
.input-wrap { position: relative; }
.input-wrap input {
width: 100%;
background: var(--surface);
border: 1px solid var(--border);
color: #fff;
font-family: var(--mono); font-size: 0.9rem;
padding: 0.85rem 3rem 0.85rem 1rem;
outline: none;
transition: border-color 0.2s;
letter-spacing: 0.05em;
}
.input-wrap input:focus { border-color: var(--accent); }
.input-wrap input.error { border-color: var(--danger); }
.input-wrap button {
position: absolute; right: 0; top: 0; bottom: 0;
width: 48px; background: none; border: none; cursor: pointer;
color: var(--dim); font-size: 1.1rem;
transition: color 0.2s;
}
.input-wrap button:hover { color: var(--accent); }
.err-msg {
font-size: 0.7rem; color: var(--danger);
margin-top: 0.5rem; display: none;
}
.err-msg.show { display: block; }
.btn-primary {
width: 100%;
background: transparent;
border: 1px solid var(--accent);
color: var(--accent);
font-family: var(--mono); font-size: 0.8rem;
letter-spacing: 0.1em; text-transform: uppercase;
padding: 0.9rem;
cursor: pointer;
position: relative; overflow: hidden;
transition: color 0.2s;
}
.btn-primary:disabled { opacity: 0.5; cursor: default; }
.btn-primary::before {
content: '';
position: absolute; inset: 0;
background: var(--accent);
transform: translateX(-100%);
transition: transform 0.25s ease;
z-index: -1;
}
.btn-primary:not(:disabled):hover { color: var(--bg); }
.btn-primary:not(:disabled):hover::before { transform: translateX(0); }
#proxy-list {
width: 100%; max-width: 680px;
display: none;
flex-direction: column; gap: 1rem;
animation: fadeUp 0.4s ease both;
}
.section-title {
font-size: 0.65rem; letter-spacing: 0.12em;
text-transform: uppercase; color: var(--dim);
margin-bottom: 0.25rem;
}
.section-title span { color: var(--accent); }
.proxy-card {
background: var(--surface);
border: 1px solid var(--border);
padding: 1.25rem 1.5rem;
position: relative;
transition: border-color 0.2s;
animation: fadeUp 0.4s ease both;
}
.proxy-card::before {
content: '';
position: absolute; top: 0; left: 0;
width: 3px; height: 100%;
background: linear-gradient(180deg, var(--accent), var(--accent2));
}
.proxy-card:hover { border-color: #2a2a3e; }
.card-header {
display: flex; align-items: center; gap: 0.75rem;
margin-bottom: 0.75rem;
}
.node-index {
font-size: 0.6rem; color: var(--accent);
letter-spacing: 0.1em;
background: rgba(0,229,255,0.07);
padding: 0.2rem 0.5rem;
border: 1px solid rgba(0,229,255,0.15);
}
.node-name {
font-family: var(--sans);
font-weight: 700; font-size: 1rem;
color: #fff; letter-spacing: -0.01em;
}
.node-flag { margin-left: auto; font-size: 1.2rem; }
.card-meta {
display: grid; grid-template-columns: auto 1fr; gap: 0.3rem 1rem;
font-size: 0.7rem; color: var(--dim); margin-bottom: 1rem;
}
.card-meta .key { color: var(--dim); }
.card-meta .val { color: var(--text); font-size: 0.68rem; word-break: break-all; }
.card-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.btn-tg {
flex: 1;
background: linear-gradient(90deg, var(--accent2), #4a1fff);
border: none; color: #fff;
font-family: var(--mono); font-size: 0.7rem;
letter-spacing: 0.08em; text-transform: uppercase;
padding: 0.7rem 1rem;
cursor: pointer; text-decoration: none;
display: flex; align-items: center; justify-content: center; gap: 0.4rem;
transition: opacity 0.2s;
min-width: 160px;
}
.btn-tg:hover { opacity: 0.85; }
.btn-copy {
background: transparent;
border: 1px solid var(--border);
color: var(--dim);
font-family: var(--mono); font-size: 0.7rem;
letter-spacing: 0.08em; text-transform: uppercase;
padding: 0.7rem 1rem;
cursor: pointer;
display: flex; align-items: center; gap: 0.4rem;
transition: color 0.2s, border-color 0.2s;
white-space: nowrap;
}
.btn-copy:hover { color: var(--accent); border-color: var(--accent); }
.btn-copy.copied { color: #2bff8f; border-color: #2bff8f; }
footer {
width: 100%; max-width: 680px;
margin-top: 3rem; padding-top: 1.5rem;
border-top: 1px solid var(--border);
font-size: 0.62rem; color: var(--dim);
display: flex; justify-content: space-between; align-items: center;
letter-spacing: 0.06em; text-transform: uppercase;
}
@keyframes fadeUp {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
.proxy-card:nth-child(2) { animation-delay: 0.05s; }
.proxy-card:nth-child(3) { animation-delay: 0.10s; }
.proxy-card:nth-child(4) { animation-delay: 0.15s; }
.proxy-card:nth-child(5) { animation-delay: 0.20s; }
</style>
</head> </head>
<body> <body>
<div class="blob blob-1"></div> <div class="blob blob-1"></div>
@ -291,11 +26,11 @@
<p class="auth-label">Введи <span>пароль</span></p> <p class="auth-label">Введи <span>пароль</span></p>
<div class="input-wrap"> <div class="input-wrap">
<input type="password" id="pass-input" placeholder="Введите пароль" autocomplete="off" /> <input type="password" id="pass-input" placeholder="Введите пароль" autocomplete="off" />
<button onclick="toggleVis()">👁</button> <button id="eye-btn">👁</button>
</div> </div>
<p class="err-msg" id="err-msg">Неправильный пароль. Ты знаешь кому писать.</p> <p class="err-msg" id="err-msg">Неправильный пароль. Ты знаешь кому писать.</p>
</div> </div>
<button class="btn-primary" id="submit-btn" onclick="tryAuth()">→ Нажать</button> <button class="btn-primary" id="submit-btn">→ Нажать</button>
</div> </div>
<div id="proxy-list"> <div id="proxy-list">
@ -308,150 +43,6 @@
</div> </div>
<script> <script src="./assets/app.js"></script>
document.getElementById('yr').textContent = new Date().getFullYear();
document.getElementById('pass-input').addEventListener('keydown', e => {
if (e.key === 'Enter') tryAuth();
});
function toggleVis() {
const inp = document.getElementById('pass-input');
inp.type = inp.type === 'password' ? 'text' : 'password';
}
function b64ToBytes(b64) {
return Uint8Array.from(atob(b64), c => c.charCodeAt(0));
}
async function tryAuth() {
const password = document.getElementById('pass-input').value;
if (!password) return;
const btn = document.getElementById('submit-btn');
btn.disabled = true;
btn.textContent = '...';
try {
const res = await fetch('./data.enc');
const { salt, iv, ct } = await res.json();
const keyMaterial = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(password),
'PBKDF2',
false,
['deriveKey']
);
const key = await crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt: b64ToBytes(salt), iterations: 100000, hash: 'SHA-256' },
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['decrypt']
);
// Wrong password → AES-GCM auth tag mismatch → throws → catch below
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: b64ToBytes(iv) },
key,
b64ToBytes(ct)
);
const proxies = JSON.parse(new TextDecoder().decode(decrypted));
document.getElementById('auth-wall').style.display = 'none';
renderProxies(proxies);
const list = document.getElementById('proxy-list');
list.style.display = 'flex';
list.style.flexDirection = 'column';
} catch {
showError();
btn.disabled = false;
btn.textContent = '→ Нажать';
}
}
function showError() {
const input = document.getElementById('pass-input');
const msg = document.getElementById('err-msg');
input.classList.add('error');
msg.classList.add('show');
input.animate(
[{ transform: 'translateX(-6px)' }, { transform: 'translateX(6px)' }, { transform: 'translateX(0)' }],
{ duration: 200, iterations: 3 }
);
setTimeout(() => { input.classList.remove('error'); msg.classList.remove('show'); }, 3000);
}
function tgUrl(p) {
return `tg://proxy?server=${encodeURIComponent(p.server)}&port=${encodeURIComponent(p.port)}&secret=${encodeURIComponent(p.secret)}`;
}
function renderProxies(proxies) {
const list = document.getElementById('proxy-list');
proxies.forEach((p, i) => {
const url = tgUrl(p);
const card = document.createElement('div');
card.className = 'proxy-card';
const hdr = document.createElement('div');
hdr.className = 'card-header';
const idx = document.createElement('span');
idx.className = 'node-index';
idx.textContent = `NODE ${String(i + 1).padStart(2, '0')}`;
const name = document.createElement('span');
name.className = 'node-name';
name.textContent = p.name;
const flag = document.createElement('span');
flag.className = 'node-flag';
flag.textContent = p.flag;
hdr.append(idx, name, flag);
const meta = document.createElement('div');
meta.className = 'card-meta';
[['server', p.server], ['port', p.port], ['secret', p.secret]].forEach(([k, v]) => {
const key = document.createElement('span');
key.className = 'key';
key.textContent = k;
const val = document.createElement('span');
val.className = 'val';
val.textContent = v;
meta.append(key, val);
});
const actions = document.createElement('div');
actions.className = 'card-actions';
const tgBtn = document.createElement('a');
tgBtn.className = 'btn-tg';
tgBtn.href = url;
tgBtn.textContent = '✈ Open in Telegram';
const copyBtn = document.createElement('button');
copyBtn.className = 'btn-copy';
copyBtn.textContent = '⎘ Copy link';
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(url).then(() => {
copyBtn.textContent = '✓ Copied';
copyBtn.classList.add('copied');
setTimeout(() => {
copyBtn.textContent = '⎘ Copy link';
copyBtn.classList.remove('copied');
}, 2000);
});
});
actions.append(tgBtn, copyBtn);
card.append(hdr, meta, actions);
list.appendChild(card);
});
}
</script>
</body> </body>
</html> </html>