Usar clave SSH global del monitor

This commit is contained in:
2026-06-04 17:52:24 +02:00
parent e29df62f7a
commit d7fe855ec0
5 changed files with 228 additions and 37 deletions
+28 -11
View File
@@ -118,20 +118,36 @@ Usuario SSH.
### `password`
Password SSH. Se guarda en `config.json`, por lo que el archivo debe permanecer protegido. Si `privateKeyPath` esta configurado, la clave privada tiene prioridad y no se usa `password`.
Password SSH. Se guarda en `config.json`, por lo que el archivo debe permanecer protegido. Si el dispositivo usa `authMethod: "key"`, no se usa `password`.
### `authMethod`
Metodo de autenticacion SSH por dispositivo. Valores recomendados:
- `password`: usa `username` y `password`.
- `key`: usa `username` y la clave privada global configurada en `sshPrivateKeyPath`.
### `privateKeyPath`
Ruta de la clave privada SSH dentro del contenedor o del host donde corre el monitor. Debe apuntar a una clave privada legible por el proceso del monitor. Para uso en Docker, monta la clave o una carpeta `.ssh` dentro del contenedor. Las claves con passphrase requieren agente SSH disponible; para monitorizacion unattended suele usarse una clave sin passphrase protegida por permisos de archivo.
Campo heredado para configuraciones antiguas con clave privada por dispositivo. La interfaz nueva usa la clave global del monitor y deja este campo vacio.
## Autenticacion SSH por Clave Privada
El monitor soporta dos metodos de autenticacion por dispositivo:
- Password SSH: usar `password` y dejar `privateKeyPath` vacio.
- Clave privada: dejar `password` vacio y configurar `privateKeyPath`.
- Clave privada global: dejar `password` vacio y seleccionar `authMethod: "key"`.
Cuando `privateKeyPath` tiene valor, el backend usa clave privada y no usa el password.
Cuando un dispositivo tiene `authMethod: "key"`, el backend usa la clave privada global configurada en `sshPrivateKeyPath`. El campo `privateKeyPath` por dispositivo queda solo como compatibilidad con configuraciones antiguas.
### Configuracion global de claves
```json
"sshPrivateKeyPath": "/ssh/monitor_rpi_ed25519",
"sshPublicKeyPath": "/ssh/monitor_rpi_ed25519.pub"
```
La clave privada se queda solo en el monitor. La clave publica se muestra en el configurador y puede copiarse o descargarse para instalarla en las RPi remotas.
### 1. Guardar la clave privada en el volumen del monitor
@@ -144,15 +160,15 @@ En la RPi donde corre el contenedor, guarda las claves en:
Ejemplo:
```text
/home/yamaray/docker/monitorRPi/ssh/carabanes_monitor_ed25519
/home/yamaray/docker/monitorRPi/ssh/carabanes_monitor_ed25519.pub
/home/yamaray/docker/monitorRPi/ssh/monitor_rpi_ed25519
/home/yamaray/docker/monitorRPi/ssh/monitor_rpi_ed25519.pub
```
La clave privada debe quedar protegida:
```bash
chmod 700 /home/yamaray/docker/monitorRPi/ssh
chmod 600 /home/yamaray/docker/monitorRPi/ssh/carabanes_monitor_ed25519
chmod 600 /home/yamaray/docker/monitorRPi/ssh/monitor_rpi_ed25519
```
El `docker-compose.yml` debe montar esa carpeta dentro del contenedor:
@@ -167,8 +183,9 @@ volumes:
Dentro de la configuracion del dispositivo, la ruta debe ser la ruta vista por el contenedor:
```json
"authMethod": "key",
"password": "",
"privateKeyPath": "/ssh/carabanes_monitor_ed25519"
"privateKeyPath": ""
```
### 2. Instalar la clave publica en la Raspberry destino
@@ -187,7 +204,7 @@ chmod 600 ~/.ssh/authorized_keys
En `authorized_keys` pega el contenido de la clave publica:
```text
carabanes_monitor_ed25519.pub
monitor_rpi_ed25519.pub
```
El archivo correcto en Linux/OpenSSH es:
@@ -203,13 +220,13 @@ No es `authorised_keys`.
Una vez montada la clave, puedes probar desde la RPi host:
```bash
docker exec -it monitor-rpi ssh -i /ssh/carabanes_monitor_ed25519 -o IdentitiesOnly=yes pi@HOST_REMOTO
docker exec -it monitor-rpi ssh -i /ssh/monitor_rpi_ed25519 -o IdentitiesOnly=yes pi@HOST_REMOTO
```
Si el dispositivo usa un puerto distinto:
```bash
docker exec -it monitor-rpi ssh -i /ssh/carabanes_monitor_ed25519 -o IdentitiesOnly=yes -p 34000 pi@192.168.0.1
docker exec -it monitor-rpi ssh -i /ssh/monitor_rpi_ed25519 -o IdentitiesOnly=yes -p 34000 pi@192.168.0.1
```
Si la conexion funciona sin pedir password, el monitor tambien podra consultar ese dispositivo.
+10
View File
@@ -3,6 +3,8 @@
"refreshIntervalSeconds": 30,
"idleScanIntervalSeconds": 300,
"sshTimeoutSeconds": 8,
"sshPrivateKeyPath": "/ssh/monitor_rpi_ed25519",
"sshPublicKeyPath": "/ssh/monitor_rpi_ed25519.pub",
"temperatureThresholdsC": {
"warning": 60,
"hot": 70,
@@ -37,6 +39,7 @@
"host": "192.168.0.46",
"username": "pi",
"password": "",
"authMethod": "password",
"privateKeyPath": "",
"role": "Home Assistant Local",
"port": 22,
@@ -50,6 +53,7 @@
"host": "192.168.0.57",
"username": "pi",
"password": "",
"authMethod": "password",
"privateKeyPath": "",
"role": "Home Assistant Cabaña",
"port": 22,
@@ -63,6 +67,7 @@
"host": "192.168.0.53",
"username": "yamaray",
"password": "",
"authMethod": "password",
"privateKeyPath": "",
"role": "Docker",
"port": 22,
@@ -76,6 +81,7 @@
"host": "192.168.0.130",
"username": "pi",
"password": "",
"authMethod": "password",
"privateKeyPath": "",
"role": "Docker - Daniel",
"port": 22,
@@ -89,6 +95,7 @@
"host": "192.168.0.254",
"username": "yamaray",
"password": "",
"authMethod": "password",
"privateKeyPath": "",
"role": "nginx - wireguard ",
"port": 22,
@@ -102,6 +109,7 @@
"host": "192.168.0.37",
"username": "pi",
"password": "",
"authMethod": "password",
"privateKeyPath": "",
"role": "MQTT - varios",
"port": 22,
@@ -115,6 +123,7 @@
"host": "192.168.0.60",
"username": "yamaray",
"password": "",
"authMethod": "password",
"privateKeyPath": "",
"role": "Meshcore-Interface",
"port": 22,
@@ -128,6 +137,7 @@
"host": "192.168.1.47",
"username": "pi",
"password": "",
"authMethod": "password",
"privateKeyPath": "",
"role": "Reserva",
"port": 22,
+115 -12
View File
@@ -154,6 +154,45 @@
gap: 10px;
}
.ssh-key-panel {
margin-top: 16px;
display: grid;
gap: 12px;
padding: 12px;
border: 1px solid var(--line);
border-radius: 16px;
background: var(--panel-2);
}
.ssh-key-fields {
display: grid;
grid-template-columns: repeat(2, minmax(220px, 1fr));
gap: 10px;
}
.public-key-box {
display: grid;
gap: 8px;
}
.public-key-box textarea {
width: 100%;
min-height: 78px;
resize: vertical;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--input);
color: var(--text);
padding: 10px;
font: 13px ui-monospace, "Cascadia Code", Consolas, monospace;
}
.public-key-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.threshold-row {
display: grid;
grid-template-columns: 110px repeat(3, minmax(120px, 1fr));
@@ -198,9 +237,8 @@
.field-port { grid-column: span 1; }
.field-username { grid-column: span 2; }
.field-auth { grid-column: span 2; }
.password-credential, .key-credential { grid-column: span 4; }
.password-credential { grid-column: span 4; }
.field-delete { grid-column: span 2; }
.device[data-auth-method="password"] .key-credential { display: none; }
.device[data-auth-method="key"] .password-credential { display: none; }
.credential-note {
@@ -262,6 +300,17 @@
}
button:hover, a:hover { border-color: var(--info); }
.secondary-action {
background: var(--panel);
border-color: var(--info);
color: var(--info);
font-weight: 650;
}
.secondary-action:hover {
background: color-mix(in srgb, var(--info) 18%, var(--panel));
}
.danger {
background: var(--offline-bg);
border-color: var(--offline-line);
@@ -396,11 +445,12 @@
@media (max-width: 1180px) {
.device { grid-template-columns: repeat(6, minmax(0, 1fr)); align-items: start; }
.field-name, .field-role, .field-host, .password-credential, .key-credential { grid-column: span 3; }
.field-name, .field-role, .field-host, .password-credential { grid-column: span 3; }
.field-id, .field-model, .field-location, .field-username, .field-auth, .field-active, .field-delete { grid-column: span 2; }
.field-port { grid-column: span 1; }
.credential-note { grid-column: span 4; }
.top-grid { grid-template-columns: repeat(2, 1fr); }
.ssh-key-fields { grid-template-columns: 1fr; }
.threshold-row { grid-template-columns: 90px repeat(3, minmax(100px, 1fr)); }
}
@@ -410,7 +460,7 @@
.top-grid, .device { grid-template-columns: 1fr; }
.field-active, .field-name, .field-id, .field-role, .field-model, .field-location,
.field-host, .field-port, .field-username, .field-auth, .password-credential,
.key-credential, .credential-note, .field-delete { grid-column: span 1; }
.credential-note, .field-delete { grid-column: span 1; }
.threshold-row { grid-template-columns: 1fr; }
}
</style>
@@ -452,6 +502,26 @@
<input id="sshTimeoutSeconds" type="number" min="2" step="1">
</label>
</div>
<div class="ssh-key-panel">
<h2>Clave SSH del monitor</h2>
<div class="ssh-key-fields">
<label title="Ruta de la clave privada dentro del contenedor. Esta clave se queda solo en el monitor.">Clave privada global
<input id="sshPrivateKeyPath" value="/ssh/monitor_rpi_ed25519">
</label>
<label title="Ruta de la clave publica dentro del contenedor. Esta es la clave que se copia a las RPi remotas.">Clave publica global
<input id="sshPublicKeyPath" value="/ssh/monitor_rpi_ed25519.pub">
</label>
</div>
<div class="public-key-box">
<label title="Copia esta clave publica en ~/.ssh/authorized_keys del usuario SSH de la RPi remota.">Clave publica para instalar en las RPi
<textarea id="sshPublicKey" readonly placeholder="Cargando clave publica..."></textarea>
</label>
<div class="public-key-actions">
<button class="secondary-action" id="copyPublicKey" type="button">Copiar clave publica</button>
<a class="secondary-action" id="downloadPublicKey" href="/api/ssh-public-key/download">Descargar .pub</a>
</div>
</div>
</div>
<div class="threshold-groups">
<div class="threshold-row">
<div class="threshold-title">Temperatura</div>
@@ -588,9 +658,10 @@
const devicesEl = document.querySelector("#devices");
const statusEl = document.querySelector("#status");
const dashboardLink = document.querySelector("#dashboardLink");
const publicKeyEl = document.querySelector("#sshPublicKey");
function deviceTemplate(device, index) {
const authMethod = device.privateKeyPath ? "key" : "password";
const authMethod = device.authMethod || (device.privateKeyPath ? "key" : "password");
return `
<div class="device" data-index="${index}" data-auth-method="${authMethod}">
<label class="field-name" title="Nombre visible de la tarjeta en el dashboard.">Nombre
@@ -628,11 +699,8 @@
<label class="password-credential" title="Password SSH. Se guarda en config.json dentro del volumen persistente. Si eliges clave privada, se guardara vacio.">Password
<input data-field="password" type="password" value="${device.password || ""}">
</label>
<label class="key-credential" title="Ruta de la clave privada SSH dentro del contenedor. Ejemplo: /ssh/carabanes_monitor_ed25519. Si eliges password, se guardara vacia.">Clave privada
<input data-field="privateKeyPath" value="${device.privateKeyPath || ""}">
</label>
<div class="credential-note">
Metodo activo: <strong>${authMethod === "key" ? "Clave privada" : "Password"}</strong>
Metodo activo: <strong>${authMethod === "key" ? "Clave privada global del monitor" : "Password"}</strong>
</div>
<label class="field-active" title="Si esta activo, este dispositivo se consulta por SSH. Si esta inactivo, se muestra deshabilitado y no se escanea.">Activo
<input data-field="active" type="checkbox" ${device.active ? "checked" : ""}>
@@ -648,6 +716,8 @@
document.querySelector("#refreshIntervalSeconds").value = state.config.refreshIntervalSeconds || 30;
document.querySelector("#idleScanIntervalSeconds").value = state.config.idleScanIntervalSeconds || 300;
document.querySelector("#sshTimeoutSeconds").value = state.config.sshTimeoutSeconds || 8;
document.querySelector("#sshPrivateKeyPath").value = state.config.sshPrivateKeyPath || "/ssh/monitor_rpi_ed25519";
document.querySelector("#sshPublicKeyPath").value = state.config.sshPublicKeyPath || `${document.querySelector("#sshPrivateKeyPath").value}.pub`;
document.querySelector("#warningTemp").value = state.config.temperatureThresholdsC?.warning || 60;
document.querySelector("#hotTemp").value = state.config.temperatureThresholdsC?.hot || 70;
document.querySelector("#criticalTemp").value = state.config.temperatureThresholdsC?.critical || 80;
@@ -671,6 +741,8 @@
state.config.refreshIntervalSeconds = Number(document.querySelector("#refreshIntervalSeconds").value || 30);
state.config.idleScanIntervalSeconds = Number(document.querySelector("#idleScanIntervalSeconds").value || 300);
state.config.sshTimeoutSeconds = Number(document.querySelector("#sshTimeoutSeconds").value || 8);
state.config.sshPrivateKeyPath = document.querySelector("#sshPrivateKeyPath").value || "/ssh/monitor_rpi_ed25519";
state.config.sshPublicKeyPath = document.querySelector("#sshPublicKeyPath").value || `${state.config.sshPrivateKeyPath}.pub`;
state.config.temperatureThresholdsC = {
...(state.config.temperatureThresholdsC || {}),
warning: Number(document.querySelector("#warningTemp").value || 60),
@@ -709,8 +781,9 @@
else device[field] = input.value;
});
const authMethod = row.querySelector("[data-auth-method]")?.value || "password";
if (authMethod === "key") device.password = "";
else device.privateKeyPath = "";
device.authMethod = authMethod === "key" ? "key" : "password";
device.privateKeyPath = "";
if (device.authMethod === "key") device.password = "";
return device;
});
}
@@ -719,6 +792,18 @@
const res = await fetch("/api/config", { cache: "no-store" });
state.config = await res.json();
render();
loadPublicKey().catch((error) => {
publicKeyEl.value = error.message;
});
}
async function loadPublicKey() {
publicKeyEl.value = "Cargando clave publica...";
const res = await fetch("/api/ssh-public-key", { cache: "no-store" });
const payload = await res.json();
if (!res.ok) throw new Error(payload.error || `HTTP ${res.status}`);
publicKeyEl.value = payload.publicKey || "";
document.querySelector("#sshPublicKeyPath").value = payload.publicKeyPath || document.querySelector("#sshPublicKeyPath").value;
}
async function save() {
@@ -762,6 +847,7 @@
username: "pi",
password: "",
privateKeyPath: "",
authMethod: "password",
model: "RPi 4",
role: "",
location: "Rack",
@@ -797,7 +883,24 @@
const row = selector.closest(".device");
row.dataset.authMethod = selector.value;
const note = row.querySelector(".credential-note strong");
if (note) note.textContent = selector.value === "key" ? "Clave privada" : "Password";
if (note) note.textContent = selector.value === "key" ? "Clave privada global del monitor" : "Password";
});
document.querySelector("#copyPublicKey").addEventListener("click", async () => {
const button = document.querySelector("#copyPublicKey");
try {
await navigator.clipboard.writeText(publicKeyEl.value);
button.classList.add("action-ok");
button.textContent = "Copiada";
setTimeout(() => {
button.classList.remove("action-ok");
button.textContent = "Copiar clave publica";
}, 1200);
} catch {
publicKeyEl.select();
statusEl.className = "status save-error";
statusEl.textContent = "No se pudo copiar automaticamente. Selecciona la clave y copiala manualmente.";
}
});
load().catch((error) => {
+10 -8
View File
@@ -86,26 +86,28 @@
<li><code>name</code>: nombre visible.</li>
<li><code>host</code>: IP o DNS.</li>
<li><code>port</code>: puerto SSH.</li>
<li><code>username/password</code> o <code>privateKeyPath</code>: credenciales SSH. Si hay clave privada configurada, tiene prioridad sobre el password.</li>
<li><code>username/password</code> o <code>authMethod: key</code>: credenciales SSH. Con clave, usa la clave privada global del monitor.</li>
<li><code>model</code>: modelo visible.</li>
<li><code>role</code>: funcion del equipo.</li>
<li><code>location</code>: ubicacion.</li>
</ul>
<h2>Autenticacion SSH por clave privada</h2>
<p>Por dispositivo se puede usar password o clave privada. Si <code>privateKeyPath</code> tiene valor, el backend usa la clave privada y no usa el password.</p>
<p>Por dispositivo se puede usar password o la clave privada global del monitor. Si el dispositivo usa <code>authMethod: key</code>, el backend usa <code>sshPrivateKeyPath</code> y no usa el password.</p>
<h3>Clave en el contenedor</h3>
<p>Guarda la clave privada en la RPi donde corre el monitor:</p>
<pre><code>/home/yamaray/docker/monitorRPi/ssh/carabanes_monitor_ed25519</code></pre>
<pre><code>/home/yamaray/docker/monitorRPi/ssh/monitor_rpi_ed25519
/home/yamaray/docker/monitorRPi/ssh/monitor_rpi_ed25519.pub</code></pre>
<p>Protege permisos:</p>
<pre><code>chmod 700 /home/yamaray/docker/monitorRPi/ssh
chmod 600 /home/yamaray/docker/monitorRPi/ssh/carabanes_monitor_ed25519</code></pre>
chmod 600 /home/yamaray/docker/monitorRPi/ssh/monitor_rpi_ed25519</code></pre>
<p>El compose debe montar la carpeta como solo lectura:</p>
<pre><code>- /home/yamaray/docker/monitorRPi/ssh:/ssh:ro</code></pre>
<p>En el configurador usa la ruta vista desde el contenedor:</p>
<pre><code>password: ""
privateKeyPath: /ssh/carabanes_monitor_ed25519</code></pre>
<p>En el configurador general usa las rutas vistas desde el contenedor:</p>
<pre><code>sshPrivateKeyPath: /ssh/monitor_rpi_ed25519
sshPublicKeyPath: /ssh/monitor_rpi_ed25519.pub</code></pre>
<p>En cada dispositivo selecciona <code>Auth SSH = Clave privada</code>. El dispositivo solo necesita IP/host, puerto y usuario SSH.</p>
<h3>Clave publica en la Raspberry destino</h3>
<p>En el usuario SSH remoto, pega la clave publica en:</p>
@@ -118,7 +120,7 @@ chmod 600 ~/.ssh/authorized_keys</code></pre>
<p>El nombre correcto del archivo es <code>authorized_keys</code>, no <code>authorised_keys</code>.</p>
<h3>Prueba manual</h3>
<pre><code>docker exec -it monitor-rpi ssh -i /ssh/carabanes_monitor_ed25519 -o IdentitiesOnly=yes -p 34000 pi@192.168.0.1</code></pre>
<pre><code>docker exec -it monitor-rpi ssh -i /ssh/monitor_rpi_ed25519 -o IdentitiesOnly=yes -p 34000 pi@192.168.0.1</code></pre>
<p>Si entra sin pedir password, el monitor podra usar esa clave para el scan.</p>
<h2>API</h2>
+65 -6
View File
@@ -10,6 +10,8 @@ const DEFAULT_CONFIG_PATH = path.join(ROOT, "config.json");
const PORT = Number(process.env.PORT || 8787);
const AUTH_USERNAME = process.env.MONITOR_USERNAME || "";
const AUTH_PASSWORD = process.env.MONITOR_PASSWORD || "";
const DEFAULT_SSH_PRIVATE_KEY_PATH = "/ssh/monitor_rpi_ed25519";
const DEFAULT_SSH_PUBLIC_KEY_PATH = "/ssh/monitor_rpi_ed25519.pub";
let config = null;
let latestStatus = null;
@@ -42,21 +44,26 @@ async function readConfig() {
parsed.refreshIntervalSeconds = Number(parsed.refreshIntervalSeconds || 30);
parsed.idleScanIntervalSeconds = Number(parsed.idleScanIntervalSeconds || 300);
parsed.sshTimeoutSeconds = Number(parsed.sshTimeoutSeconds || 8);
parsed.sshPrivateKeyPath = String(parsed.sshPrivateKeyPath || DEFAULT_SSH_PRIVATE_KEY_PATH);
parsed.sshPublicKeyPath = String(parsed.sshPublicKeyPath || `${parsed.sshPrivateKeyPath}.pub` || DEFAULT_SSH_PUBLIC_KEY_PATH);
parsed.temperatureThresholdsC = normalizeTemperatureThresholds(parsed.temperatureThresholdsC);
parsed.metricThresholdsPercent = normalizeMetricThresholds(parsed.metricThresholdsPercent);
parsed.devices = Array.isArray(parsed.devices) ? parsed.devices : [];
parsed.devices = Array.isArray(parsed.devices) ? parsed.devices.map(normalizeDeviceConfig) : [];
return parsed;
}
async function writeConfig(nextConfig) {
const sshPrivateKeyPath = String(nextConfig.sshPrivateKeyPath || DEFAULT_SSH_PRIVATE_KEY_PATH);
const normalized = {
...nextConfig,
refreshIntervalSeconds: Number(nextConfig.refreshIntervalSeconds || 30),
idleScanIntervalSeconds: Number(nextConfig.idleScanIntervalSeconds || 300),
sshTimeoutSeconds: Number(nextConfig.sshTimeoutSeconds || 8),
sshPrivateKeyPath,
sshPublicKeyPath: String(nextConfig.sshPublicKeyPath || `${sshPrivateKeyPath}.pub` || DEFAULT_SSH_PUBLIC_KEY_PATH),
temperatureThresholdsC: normalizeTemperatureThresholds(nextConfig.temperatureThresholdsC),
metricThresholdsPercent: normalizeMetricThresholds(nextConfig.metricThresholdsPercent),
devices: Array.isArray(nextConfig.devices) ? nextConfig.devices : []
devices: Array.isArray(nextConfig.devices) ? nextConfig.devices.map(normalizeDeviceConfig) : []
};
await fs.mkdir(path.dirname(CONFIG_PATH), { recursive: true });
await fs.writeFile(CONFIG_PATH, JSON.stringify(normalized, null, 2) + "\n", "utf8");
@@ -64,6 +71,15 @@ async function writeConfig(nextConfig) {
restartScanner();
}
function normalizeDeviceConfig(device = {}) {
const authMethod = device.authMethod || (device.privateKeyPath ? "key" : "password");
return {
...device,
authMethod: authMethod === "key" ? "key" : "password",
privateKeyPath: device.privateKeyPath || ""
};
}
function classifyTemp(tempC) {
const thresholds = config.temperatureThresholdsC || {};
if (tempC >= (thresholds.critical || 80)) return "critical";
@@ -364,8 +380,12 @@ function runCommandWithInput(command, args, input, timeoutMs) {
}
async function sshMetrics(device) {
const hasPrivateKey = Boolean(device.privateKeyPath);
if (!hasPrivateKey && !device.password) {
const usesPrivateKey = device.authMethod === "key" || Boolean(device.privateKeyPath);
const privateKeyPath = device.privateKeyPath || config.sshPrivateKeyPath;
if (usesPrivateKey && !privateKeyPath) {
throw new Error("Clave privada SSH global no configurada");
}
if (!usesPrivateKey && !device.password) {
throw new Error("Credenciales SSH no configuradas");
}
@@ -388,10 +408,10 @@ async function sshMetrics(device) {
let command = "ssh";
let args = sshArgs;
if (hasPrivateKey) {
if (usesPrivateKey) {
args = [
"-i",
device.privateKeyPath,
privateKeyPath,
"-o",
"BatchMode=yes",
"-o",
@@ -627,6 +647,21 @@ function sendJson(res, statusCode, payload) {
res.end(body);
}
function sendText(res, statusCode, body, headers = {}) {
res.writeHead(statusCode, {
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": "no-store",
...headers
});
res.end(body);
}
async function readSshPublicKey() {
const publicKeyPath = config.sshPublicKeyPath || `${config.sshPrivateKeyPath || DEFAULT_SSH_PRIVATE_KEY_PATH}.pub`;
const publicKey = (await fs.readFile(publicKeyPath, "utf8")).trim();
return { publicKeyPath, publicKey };
}
function isAuthEnabled() {
return Boolean(AUTH_USERNAME && AUTH_PASSWORD);
}
@@ -737,6 +772,30 @@ async function handleRequest(req, res) {
return;
}
if (req.method === "GET" && pathname === "/api/ssh-public-key") {
try {
sendJson(res, 200, await readSshPublicKey());
} catch (error) {
sendJson(res, 404, {
error: `No se pudo leer la clave publica SSH: ${error.message}`,
publicKeyPath: config.sshPublicKeyPath || `${config.sshPrivateKeyPath || DEFAULT_SSH_PRIVATE_KEY_PATH}.pub`
});
}
return;
}
if (req.method === "GET" && pathname === "/api/ssh-public-key/download") {
try {
const { publicKeyPath, publicKey } = await readSshPublicKey();
sendText(res, 200, `${publicKey}\n`, {
"Content-Disposition": `attachment; filename="${path.basename(publicKeyPath)}"`
});
} catch (error) {
sendText(res, 404, `No se pudo leer la clave publica SSH: ${error.message}\n`);
}
return;
}
if (req.method === "POST" && pathname === "/api/config") {
const body = await readBody(req);
await writeConfig(JSON.parse(body));