fix(tunnel): one-liner curl|sh; keypair generated on VPS; on-page copy fallback

aarch64-k3.10 Entware lacks sshpass, cronie, and openssh-keygen. Generate the
ed25519 keypair on the VPS (where ssh-keygen is always present) and ship the
private key to the router inside the install script.

The router only sees a short curl|sh one-liner — easy to copy, immune to
markdown auto-linkification of .sh paths that mangled previous attempts.

- _gen_ed25519_keypair / _add_pubkey_to_authorized_keys helpers
- /tunnel-cmd: generate keypair on first call, store priv+pub on router record,
  add pub to ~/.ssh/authorized_keys, return one-liner
- /tunnel-script: returns full install script with private key embedded
  (text/plain), token-authenticated, single-use
- removed /tunnel-register-key endpoint (router no longer generates the key)
- copy button: select on-page text + execCommand fallback for non-HTTPS;
  if both fail, leave selected so user can Cmd+C manually

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Андрей Бобырев
2026-04-27 22:12:15 +03:00
parent b140cdac43
commit 61d80ebc22
2 changed files with 176 additions and 152 deletions

View File

@@ -434,30 +434,26 @@ function closeTunnelModal(){
_tunnelRouterId=null;
}
function copyTunnelCmd(){
const txt=document.getElementById('tunnel-cmd-text').textContent;
const el=document.getElementById('tunnel-cmd-text');
const txt=el.textContent;
const btn=(typeof event!=='undefined'&&event&&event.target)?event.target:document.querySelector('button[onclick="copyTunnelCmd()"]');
const flash=()=>{if(btn){const o=btn.textContent;btn.textContent='✓ Скопировано';setTimeout(()=>{btn.textContent=o;},2000);}};
const fail=()=>{
const ta=document.getElementById('tunnel-cmd-text');
const r=document.createRange();r.selectNodeContents(ta);
const s=window.getSelection();s.removeAllRanges();s.addRange(r);
alert('Не получилось автоматически. Команда выделена — нажми Cmd/Ctrl+C, затем вставь в SSH роутера.');
};
// navigator.clipboard работает только в HTTPS / localhost. На http://IP:port — нет.
const flash=(t)=>{if(btn){const o=btn.textContent;btn.textContent=t||'✓ Скопировано';setTimeout(()=>{btn.textContent=o;},1800);}};
// 1) HTTPS: современный API
if(window.isSecureContext&&navigator.clipboard){
navigator.clipboard.writeText(txt).then(flash).catch(()=>execCopy(txt,flash,fail));
} else {
execCopy(txt,flash,fail);
navigator.clipboard.writeText(txt).then(()=>flash()).catch(selectFallback);
return;
}
selectFallback();
function selectFallback(){
// 2) Выделить on-page контент и попытаться execCommand('copy')
const r=document.createRange();r.selectNodeContents(el);
const s=window.getSelection();s.removeAllRanges();s.addRange(r);
let ok=false;
try{ok=document.execCommand('copy');}catch(e){ok=false;}
if(ok){flash();setTimeout(()=>s.removeAllRanges(),1500);return;}
// 3) Не получилось — оставить выделенным, дать пользователю Cmd+C
flash('Выдели команду и нажми Cmd/Ctrl+C');
}
}
function execCopy(txt,ok,fail){
const ta=document.createElement('textarea');
ta.value=txt;ta.style.position='fixed';ta.style.top='0';ta.style.left='0';ta.style.opacity='0';
document.body.appendChild(ta);ta.focus();ta.select();
let okv=false;
try{okv=document.execCommand('copy');}catch(e){okv=false;}
document.body.removeChild(ta);
okv?ok():fail();
}
async function applyTunnelUrl(){
if(!_tunnelRouterId)return;