Compare commits

...

2 Commits

Author SHA1 Message Date
yamaray bc50948455 Aclarar autenticacion por clave asimetrica 2026-06-04 18:31:08 +02:00
yamaray d7fe855ec0 Usar clave SSH global del monitor 2026-06-04 17:52:24 +02:00
5 changed files with 233 additions and 42 deletions
+30 -13
View File
@@ -12,7 +12,7 @@ Navegador
v
Contenedor monitor-rpi
|
| SSH con usuario/password o clave privada
| SSH con usuario/password o clave asimetrica
v
Raspberry Pi activas
```
@@ -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`. Operativamente, instala la clave publica del monitor en la RPi.
### `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
## Autenticacion SSH por Clave Asimetrica
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 asimetrica: dejar `password` vacio, seleccionar `authMethod: "key"` y copiar la clave publica del monitor en la RPi.
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,
+117 -14
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
@@ -622,17 +693,14 @@
<label class="field-auth" title="Metodo de autenticacion SSH que usara el monitor para este dispositivo.">Auth SSH
<select data-auth-method>
<option value="password" ${authMethod === "password" ? "selected" : ""}>Password</option>
<option value="key" ${authMethod === "key" ? "selected" : ""}>Clave privada</option>
<option value="key" ${authMethod === "key" ? "selected" : ""}>Clave asimetrica</option>
</select>
</label>
<label class="password-credential" title="Password SSH. Se guarda en config.json dentro del volumen persistente. Si eliges clave privada, se guardara vacio.">Password
<label class="password-credential" title="Password SSH. Se guarda en config.json dentro del volumen persistente. Si eliges clave asimetrica, 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 asimetrica: copiar clave publica del monitor en la RPi" : "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 asimetrica: copiar clave publica del monitor en la RPi" : "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) => {
+11 -9
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 asimetrica, copia la publica del monitor en la RPi.</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>
<h2>Autenticacion SSH por clave asimetrica</h2>
<p>Por dispositivo se puede usar password o clave asimetrica. Si el dispositivo usa <code>authMethod: key</code>, el backend usa la clave privada global del monitor 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 asimetrica</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));