mirror of
https://github.com/pvlnes/homelab.git
synced 2026-06-03 17:33:49 +00:00
Compare commits
2 Commits
2c0ca60b65
...
1164ef1171
| Author | SHA1 | Date | |
|---|---|---|---|
| 1164ef1171 | |||
| eedb023cf1 |
@ -110,14 +110,18 @@ truenews.sesur.dev {
|
||||
file_server
|
||||
}
|
||||
t.sesur.dev {
|
||||
root * /opt/homelab/services/mtproto_page/xk9m2p4q7
|
||||
root * /opt/homelab/services/mtproto_page
|
||||
|
||||
@data path /data.json
|
||||
basicauth @data {
|
||||
pvlx $2b$05$wXo0zmemeoOJ3ukx4pORSuq/9IoH/Lo5PIvGk3uzNvcAMmtpjI1o2
|
||||
handle /xk9m2p4q7/data.json {
|
||||
respond 404
|
||||
}
|
||||
handle /xk9m2p4q7/encrypt.py {
|
||||
respond 404
|
||||
}
|
||||
handle {
|
||||
file_server
|
||||
}
|
||||
|
||||
file_server
|
||||
header {
|
||||
X-Robots-Tag "noindex, nofollow"
|
||||
X-Content-Type-Options "nosniff"
|
||||
|
||||
1
services/mtproto_page/xk9m2p4q7/data.enc
Normal file
1
services/mtproto_page/xk9m2p4q7/data.enc
Normal file
@ -0,0 +1 @@
|
||||
{"salt": "kHW+St6vw5/6MkfH3tjtuw==", "iv": "LPQqGr84T4UVthgd", "ct": "scddFF7pZD1lxOPUia7RnJIp+ytSdmC1Gi9QvZ8oEQBAzhGAnlO8uoOsG06q915r8rykb0ni7A3XZaL1716PzgLYlh5g8dyrF0Xh/I8EyRlOyN8IiBBczpYeq3gH4R+ofh/tpJ/xzjeGx41hXvQnjpgzy/3PMoGLYzLbVKstD2kcyLZR9qYMBMN7dzkXVAeQpSeBPk6mWIFwMLgxvCRu50Lsic5UNYMdx+rnlwCosW8CMpTajv6mAsY8e+Ks58BuFPfpsPgiXrwVRnvYebv0C6NDjbIiqN8b2/HNBYjEfO1FrrIO1qspGUe3Vulc88eB1F6AX16P0Vp0j3YbuuX6CczEvo8Kq1zjlL9kqwBvisv8Atv6OzHwII2GND1tdd80aglQWtjL7xPCav+j2imAnhmhEEENgsTB1wbphSGSG1x1wWTZmtSgfTEprgDDSRhmnbiI7PcOiKHzOOm3ci3clsJZtF89ssMnBaR1sNNTnrTHsHSn4U/wXqimuLjrI5cmpLZT+5cSMMWYVYGSu59gi27vysn5GKf6j/6AQCOJbDLs6pipF8500dSEiZEqzOMf0kf44FR86eGuVCXJqSyozkHdnvieCC7eW2R+MoB9MGZL8XjbUtVB2X+7kxKSmWvZ/lLr1cSEsZsSJxOP+Sbfl6rzGjUeHp+hhuQWx9NZVPcZn8492jdp"}
|
||||
32
services/mtproto_page/xk9m2p4q7/encrypt.py
Normal file
32
services/mtproto_page/xk9m2p4q7/encrypt.py
Normal file
@ -0,0 +1,32 @@
|
||||
#!/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
|
||||
from pathlib import Path
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
|
||||
here = Path(__file__).parent
|
||||
plaintext = here.joinpath('data.json').read_bytes()
|
||||
password = getpass.getpass('Password: ').encode()
|
||||
|
||||
salt = os.urandom(16)
|
||||
kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=salt, iterations=100000)
|
||||
key = kdf.derive(password)
|
||||
|
||||
iv = os.urandom(12)
|
||||
ciphertext = AESGCM(key).encrypt(iv, plaintext, None)
|
||||
|
||||
here.joinpath('data.enc').write_text(json.dumps({
|
||||
'salt': base64.b64encode(salt).decode(),
|
||||
'iv': base64.b64encode(iv).decode(),
|
||||
'ct': base64.b64encode(ciphertext).decode(),
|
||||
}))
|
||||
|
||||
print('data.enc written.')
|
||||
@ -108,7 +108,6 @@
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
/* ── auth wall ── */
|
||||
#auth-wall {
|
||||
width: 100%; max-width: 360px;
|
||||
display: flex; flex-direction: column; gap: 1.5rem;
|
||||
@ -120,9 +119,7 @@
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.auth-label span { color: var(--accent); }
|
||||
.input-wrap {
|
||||
position: relative;
|
||||
}
|
||||
.input-wrap { position: relative; }
|
||||
.input-wrap input {
|
||||
width: 100%;
|
||||
background: var(--surface);
|
||||
@ -172,14 +169,12 @@
|
||||
.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);
|
||||
@ -270,13 +265,10 @@
|
||||
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; }
|
||||
.proxy-card:nth-child(6) { animation-delay: 0.25s; }
|
||||
.proxy-card:nth-child(7) { animation-delay: 0.30s; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -299,7 +291,7 @@
|
||||
<p class="auth-label">Введи <span>пароль</span></p>
|
||||
<div class="input-wrap">
|
||||
<input type="password" id="pass-input" placeholder="Введите пароль" autocomplete="off" />
|
||||
<button onclick="toggleVis()" id="eye-btn">👁</button>
|
||||
<button onclick="toggleVis()">👁</button>
|
||||
</div>
|
||||
<p class="err-msg" id="err-msg">Неправильный пароль. Ты знаешь кому писать.</p>
|
||||
</div>
|
||||
@ -319,6 +311,19 @@
|
||||
<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;
|
||||
@ -327,26 +332,40 @@
|
||||
btn.disabled = true;
|
||||
btn.textContent = '...';
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await fetch('/data.json', {
|
||||
headers: { 'Authorization': 'Basic ' + btoa('pvlx:' + password) }
|
||||
});
|
||||
} catch {
|
||||
showError();
|
||||
btn.disabled = false;
|
||||
btn.textContent = '→ Нажать';
|
||||
return;
|
||||
}
|
||||
const res = await fetch('./data.enc');
|
||||
const { salt, iv, ct } = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
const proxies = 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';
|
||||
} else {
|
||||
|
||||
} catch {
|
||||
showError();
|
||||
btn.disabled = false;
|
||||
btn.textContent = '→ Нажать';
|
||||
@ -359,21 +378,12 @@
|
||||
input.classList.add('error');
|
||||
msg.classList.add('show');
|
||||
input.animate(
|
||||
[{transform:'translateX(-6px)'},{transform:'translateX(6px)'},{transform:'translateX(0)'}],
|
||||
{duration: 200, iterations: 3}
|
||||
[{ transform: 'translateX(-6px)' }, { transform: 'translateX(6px)' }, { transform: 'translateX(0)' }],
|
||||
{ duration: 200, iterations: 3 }
|
||||
);
|
||||
setTimeout(() => { input.classList.remove('error'); msg.classList.remove('show'); }, 3000);
|
||||
}
|
||||
|
||||
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 tgUrl(p) {
|
||||
return `tg://proxy?server=${encodeURIComponent(p.server)}&port=${encodeURIComponent(p.port)}&secret=${encodeURIComponent(p.secret)}`;
|
||||
}
|
||||
@ -386,8 +396,8 @@
|
||||
const card = document.createElement('div');
|
||||
card.className = 'proxy-card';
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'card-header';
|
||||
const hdr = document.createElement('div');
|
||||
hdr.className = 'card-header';
|
||||
|
||||
const idx = document.createElement('span');
|
||||
idx.className = 'node-index';
|
||||
@ -401,7 +411,7 @@
|
||||
flag.className = 'node-flag';
|
||||
flag.textContent = p.flag;
|
||||
|
||||
header.append(idx, name, flag);
|
||||
hdr.append(idx, name, flag);
|
||||
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'card-meta';
|
||||
@ -438,7 +448,7 @@
|
||||
});
|
||||
|
||||
actions.append(tgBtn, copyBtn);
|
||||
card.append(header, meta, actions);
|
||||
card.append(hdr, meta, actions);
|
||||
list.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user