(() => {
// Глобальные обработчики ошибок в renderer
window.addEventListener('error', (event) => {
console.error('Global error in renderer:', event.error);
});
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection in renderer:', event.reason);
});
let apiBase = window.config?.apiBase || "http://localhost:8080/api/v1";
const qs = (sel) => document.querySelector(sel);
const qsi = (sel) => document.querySelector(sel);
const qst = (sel) => document.querySelector(sel);
const yaml = window.jsyaml;
function setMode(mode) {
["inline", "yaml", "form"].forEach((m) => {
const el = qs(`#${m}-section`);
if (el) el.classList.toggle("hidden", m !== mode);
const encryptDecryptRow = qs("#encrypt-decrypt-row");
if (encryptDecryptRow) {
encryptDecryptRow.classList.toggle("hidden", mode !== "yaml");
}
});
}
function basicAuthHeader(password) {
const token = btoa(`knocker:${password}`);
return { Authorization: `Basic ${token}` };
}
function updateStatus(msg) {
const el = qs("#status");
if (el) {
el.textContent = msg;
setTimeout(() => {
el.textContent = "";
}, 5000); // Очищаем через 5 секунд
}
}
const targets = [{ protocol: "tcp", host: "127.0.0.1", port: 22, gateway: "" }];
function renderTargets() {
const list = qs("#targetsList");
if (!list) return;
list.innerHTML = "";
targets.forEach((t, idx) => {
const row = document.createElement("div");
row.className = "target-row";
row.innerHTML = `
`;
list.appendChild(row);
});
}
function serializeFormTargetsToInline() {
return targets
.map(
(t) =>
`${t.protocol}:${t.host}:${t.port}${t.gateway ? `:${t.gateway}` : ""}`
)
.join(";");
}
function convertInlineToYaml(targetsStr, delay, waitConnection) {
const entries = (targetsStr || "").split(";").filter(Boolean);
const config = {
targets: entries.map((e) => {
const parts = e.split(":");
const protocol = parts[0] || "tcp";
const host = parts[1] || "127.0.0.1";
const port = parseInt(parts[2] || "22", 10);
return {
protocol,
host,
ports: [port],
wait_connection: !!waitConnection,
};
}),
delay: delay || "1s",
};
return yaml.dump(config, { lineWidth: 120 });
}
function convertYamlToInline(yamlText) {
if (!yamlText.trim())
return {
targets: "tcp:127.0.0.1:22",
delay: "1s",
waitConnection: false,
};
const config = yaml.load(yamlText) || {};
const list = [];
(config.targets || []).forEach((t) => {
const protocol = t.protocol || "tcp";
const host = t.host || "127.0.0.1";
const ports = t.ports || [t.port] || [22];
(Array.isArray(ports) ? ports : [ports]).forEach((p) =>
list.push(`${protocol}:${host}:${p}`)
);
});
return {
targets: list.join(";"),
delay: config.delay || "1s",
waitConnection: !!config.targets?.[0]?.wait_connection,
};
}
function extractPathFromYaml(text) {
try {
const doc = yaml.load(text);
if (doc && typeof doc === "object" && typeof doc.path === "string")
return doc.path;
} catch { }
return "";
}
function patchYamlPath(text, newPath) {
try {
const doc = text.trim() ? yaml.load(text) : {};
if (doc && typeof doc === "object") {
doc.path = newPath || "";
return yaml.dump(doc, { lineWidth: 120 });
}
} catch { }
return text;
}
function isEncryptedYaml(text) {
return (text || "").trim().startsWith("ENCRYPTED:");
}
// Функция для обновления конфига из настроек
function updateConfigFromSettings() {
window.api.getConfig('apiBase')
.then((saved) => {
if (typeof saved === 'string' && saved.trim()) {
apiBase = saved;
if (qsi('#apiUrl')) {
qsi('#apiUrl').value = apiBase;
}
}
})
.catch(() => { });
window.api.getConfig('gateway')
.then((saved) => {
if (qsi('#gateway')) {
qsi('#gateway').value = saved || '';
}
})
.catch(() => { });
window.api.getConfig('inlineTargets')
.then((saved) => {
if (qsi('#targets')) {
qsi('#targets').value = saved || '';
}
})
.catch(() => { });
window.api.getConfig('delay')
.then((saved) => {
if (qsi('#delay')) {
qsi('#delay').value = saved || '';
}
})
.catch(() => { });
}
// событие возникающее когда загружается страница основная приложения
window.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll('input[name="mode"]').forEach((r) => {
r.addEventListener("change", (e) => setMode(e?.target?.value || ''));
});
// Инициализация/восстановление apiBase из конфига
window.api.getConfig('apiBase')
.then((saved) => {
if (typeof saved === 'string' && saved.trim()) {
apiBase = saved;
}
})
.catch(() => { })
.finally(() => {
if (qsi('#apiUrl')) {
qsi('#apiUrl').value = apiBase;
}
});
// Сохранение apiBase при изменении поля
qsi('#apiUrl')?.addEventListener('change', async () => {
const val = qsi('#apiUrl').value.trim();
if (!val) return;
apiBase = val;
try { await window.api.setConfig('apiBase', val); } catch { }
updateStatus('API URL сохранён');
});
// Инициализация/восстановление gateway из конфига
window.api.getConfig('gateway')
.then((saved) => {
if (qsi('#gateway')) {
qsi('#gateway').value = saved || '';
}
})
.catch(() => { });
// Сохранение Gateway при изменении поля
qsi('#gateway')?.addEventListener('change', async () => {
const val = qsi('#gateway').value.trim();
try { await window.api.setConfig('gateway', val); } catch { }
updateStatus('Gateway сохранён');
});
// Инициализация/восстановление inlineTargets из конфига
window.api.getConfig('inlineTargets')
.then((saved) => {
if (qsi('#targets')) {
qsi('#targets').value = saved || '';
}
})
.catch(() => { });
// Сохранение inlineTargets при изменении поля
qsi('#targets')?.addEventListener('change', async () => {
const val = qsi('#targets').value.trim();
try { await window.api.setConfig('inlineTargets', val); } catch { }
updateStatus('inlineTargets сохранёны');
});
// Инициализация/восстановление delay из конфига
window.api.getConfig('delay')
.then((saved) => {
if (qsi('#delay')) {
qsi('#delay').value = saved || '';
}
})
.catch(() => { });
// Сохранение delay при изменении поля
qsi('#delay')?.addEventListener('change', async () => {
const val = qsi('#delay').value.trim();
try { await window.api.setConfig('delay', val); } catch { }
updateStatus('Задержка сохранёна');
});
qsi("#addTarget")?.addEventListener("click", () => {
targets.push({ protocol: "tcp", host: "127.0.0.1", port: 22 });
renderTargets();
});
qs("#targetsList")?.addEventListener("input", (e) => {
const row = e.target.closest(".target-row");
if (!row) return;
const idx = Array.from(row.parentElement.children).indexOf(row);
const key = e.target.getAttribute("data-k");
if (idx >= 0 && key) {
const val =
e.target.type === "number" ? Number(e.target.value) : e.target.value;
targets[idx][key] = val;
}
});
qs("#targetsList")?.addEventListener("click", (e) => {
if (!e.target.classList.contains("remove")) {
return;
}
const row = e.target.closest(".target-row");
const idx = Array.from(row.parentElement.children).indexOf(row);
if (idx >= 0) {
targets.splice(idx, 1);
renderTargets();
}
});
qs("#openFile")?.addEventListener("click", async () => {
const res = await window.api.openFile();
if (!(!res.canceled && res.content !== undefined)) {
return;
}
qst("#configYAML").value = res.content;
const p = extractPathFromYaml(res.content);
qsi("#serverFilePath").value = p || "";
updateStatus(`Открыт файл: ${res.filePath}`);
});
qs("#saveFile")?.addEventListener("click", async () => {
const content = qst("#configYAML").value;
const suggested = content.trim().startsWith("ENCRYPTED:")
? "config.encrypted"
: "config.yaml";
const res = await window.api.saveAs({
suggestedName: suggested,
content,
});
if (!res.canceled && res.filePath) {
updateStatus(`Сохранено: ${res.filePath}`);
await window.api.revealInFolder(res.filePath);
}
});
qsi("#serverFilePath")?.addEventListener("input", () => {
const newPath = qsi("#serverFilePath").value;
const current = qst("#configYAML").value;
qst("#configYAML").value = patchYamlPath(current, newPath);
});
qs("#execute")?.addEventListener("click", async () => {
updateStatus("Выполнение…");
const password = qsi("#password").value;
const mode = document.querySelector('input[name="mode"]:checked')?.value || '';
// Проверяем, нужно ли использовать локальное простукивание
const useLocalKnock = !apiBase || apiBase.trim() === '' || apiBase === 'internal';
if (useLocalKnock) {
// Локальное простукивание через Node.js
try {
let targets = [];
let delay = qsi("#delay").value || '1s';
const verbose = qsi("#verbose").checked;
if (mode === "inline") {
targets = qsi("#targets").value.split(';').filter(t => t.trim());
} else if (mode === "form") {
targets = [serializeFormTargetsToInline()];
} else if (mode === "yaml") {
// Для YAML режима извлекаем targets из YAML
const yamlContent = qst("#configYAML").value;
try {
const config = yaml.load(yamlContent);
if (config?.targets && Array.isArray(config.targets)) {
targets = config.targets.map(t => {
const protocol = t.protocol || 'tcp';
const host = t.host || '127.0.0.1';
const ports = t.ports || [t.port] || [22];
return ports.map(port => `${protocol}:${host}:${port}`);
}).flat();
delay = config.delay || delay;
}
} catch (e) {
updateStatus(`Ошибка парсинга YAML: ${e.message}`);
return;
}
}
if (targets.length === 0) {
updateStatus("Нет целей для простукивания");
return;
}
// Получаем gateway из конфигурации или поля
const gateway = qsi('#gateway')?.value?.trim() || '';
const result = await window.api.localKnock({
targets,
delay,
verbose,
gateway
});
if (result?.success) {
const summary = result.summary;
updateStatus(`Локальное простукивание завершено: ${summary.successful}/${summary.total} успешно`);
// Логируем детальные результаты в консоль
if (verbose) {
console.log('Local knock results:', result.results);
}
} else {
const errorMsg = result?.error || 'Неизвестная ошибка локального простукивания';
updateStatus(`Ошибка локального простукивания: ${errorMsg}`);
console.error('Local knock failed:', result);
}
} catch (e) {
updateStatus(`Ошибка: ${e?.message || String(e)}`);
}
return;
}
// API простукивание через HTTP
const body = {};
if (mode === "yaml") {
body.config_yaml = qst("#configYAML").value;
} else if (mode === "inline") {
body.targets = qsi("#targets").value;
body.delay = qsi("#delay").value;
body.verbose = qsi("#verbose").checked;
body.waitConnection = qsi("#waitConnection").checked;
body.gateway = qsi("#gateway").value;
} else {
body.targets = serializeFormTargetsToInline();
body.delay = qsi("#delay").value;
body.verbose = qsi("#verbose").checked;
body.waitConnection = qsi("#waitConnection").checked;
}
let result;
try {
result = await fetch(`${apiBase}/knock-actions/execute`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...basicAuthHeader(password),
},
body: JSON.stringify(body),
});
if (result?.ok) {
updateStatus("Успешно простучали через API...");
} else {
updateStatus(`Ошибка API: ${result.statusText}`);
}
} catch (e) {
updateStatus(`Ошибка: ${e?.message || String(e)}`);
}
});
qs("#encrypt")?.addEventListener("click", async () => {
const password = qsi("#password").value;
const content = qst("#configYAML").value;
const pathFromYaml = extractPathFromYaml(content);
if (!content.trim()) return;
const url = `${apiBase}/knock-actions/encrypt`;
const payload = { yaml: content };
try {
const r = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
...basicAuthHeader(password),
},
body: JSON.stringify(payload),
});
const res = await r.json();
const encrypted = res?.encrypted || "";
qst("#configYAML").value = encrypted;
updateStatus("Зашифровано");
if (!pathFromYaml) {
await window.api.saveAs({
suggestedName: "config.encrypted",
content: encrypted,
});
}
} catch (e) {
updateStatus(`Ошибка: ${e?.message || String(e)}`);
}
});
qs("#decrypt")?.addEventListener("click", async () => {
const password = qsi("#password").value;
const content = qst("#configYAML").value;
if (!content.trim() || !isEncryptedYaml(content)) return;
try {
const r = await fetch(`${apiBase}/knock-actions/decrypt`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...basicAuthHeader(password),
},
body: JSON.stringify({ encrypted: content }),
});
const res = await r.json();
const plain = res?.yaml || "";
qst("#configYAML").value = plain;
const p = extractPathFromYaml(plain);
if (p) qsi("#serverFilePath").value = p;
updateStatus("Расшифровано");
} catch (e) {
updateStatus(`Ошибка: ${e?.message || String(e)}`);
}
});
renderTargets();
// Обновляем конфиг при фокусе окна (если настройки были изменены)
window.addEventListener('focus', updateConfigFromSettings);
// Диагностические функции
window.testNetworkInterfaces = async () => {
try {
const result = await window.api.getNetworkInterfaces();
if (result.success) {
console.log('Network interfaces:', result.interfaces);
updateStatus('Network interfaces logged to console');
} else {
updateStatus(`Error getting interfaces: ${result.error}`);
}
} catch (e) {
updateStatus(`Error: ${e.message}`);
}
};
window.testConnection = async () => {
try {
const gateway = qsi('#gateway')?.value?.trim();
if (!gateway) {
updateStatus('Please set gateway first');
return;
}
const result = await window.api.testConnection({
host: '192.168.89.1',
port: 2655,
localAddress: gateway
});
if (result.success) {
updateStatus(`Test connection successful: ${result.message}`);
console.log('Test connection result:', result);
} else {
updateStatus(`Test connection failed: ${result.error}`);
}
} catch (e) {
updateStatus(`Error: ${e.message}`);
}
};
// Добавляем диагностические кнопки в консоль
console.log('Diagnostic functions available:');
console.log('- window.testNetworkInterfaces() - Show network interfaces');
console.log('- window.testConnection() - Test connection with gateway');
});
})();