bazarr, tautulli: read the app's own key through its container
Moved out of their containers, their code runs as the node runtime's account, which may not read the app's config directory: it belongs to the account the app runs as on that machine, mode 0700. Read the file directly when allowed, else through the app's own container, which owns it.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
// bazarr's own API key, found where bazarr keeps it. Shared by the client (tools, events) and the
|
||||
// Servarr step, and kept apart from client.ts so the step and its test load it without the client.
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
/**
|
||||
@@ -10,12 +11,21 @@ import { readFileSync } from "node:fs";
|
||||
* stays right if the operator regenerates the key in bazarr's settings screen. The file is read, never
|
||||
* written. Undefined when the file or the key is not there.
|
||||
*/
|
||||
export function apiKeyFromConfigDir(configDir?: string): string | undefined {
|
||||
if (!configDir) return undefined;
|
||||
let text: string;
|
||||
try { text = readFileSync(`${configDir.replace(/\/$/, "")}/config/config.yaml`, "utf8"); }
|
||||
catch { return undefined; }
|
||||
return apiKeyFromConfigYaml(text);
|
||||
export function apiKeyFromConfigDir(configDir?: string, container?: string): string | undefined {
|
||||
if (!configDir && !container) return undefined;
|
||||
let text: string | undefined;
|
||||
if (configDir) {
|
||||
try { text = readFileSync(`${configDir.replace(/\/$/, "")}/config/config.yaml`, "utf8"); }
|
||||
catch { text = undefined; }
|
||||
}
|
||||
// **Through bazarr's own container when this machine's account may not read its directory.** The
|
||||
// directory belongs to the account bazarr runs as here, mode 0700; the node's runtime is another
|
||||
// account. The container is bazarr, and reads its own file.
|
||||
if (text === undefined && container) {
|
||||
try { text = execFileSync("docker", ["exec", container, "cat", "/config/config/config.yaml"], { encoding: "utf8", timeout: 10_000 }); }
|
||||
catch { return undefined; }
|
||||
}
|
||||
return text === undefined ? undefined : apiKeyFromConfigYaml(text);
|
||||
}
|
||||
|
||||
/** `auth.apikey` from the text of bazarr's config.yaml — a top-level `auth:` mapping, one level deep. */
|
||||
|
||||
@@ -70,7 +70,7 @@ export class BazarrClient {
|
||||
const url = cfg.url ?? env.MESH_BAZARR_URL;
|
||||
const apiKey =
|
||||
cfg.apiKey ??
|
||||
apiKeyFromConfigDir(env.MESH_BAZARR_CONFIG_DIR) ??
|
||||
apiKeyFromConfigDir(env.MESH_BAZARR_CONFIG_DIR, env.MESH_BAZARR_CONTAINER) ??
|
||||
readSecret(env.MESH_BAZARR_API_KEY_FILE) ??
|
||||
env.MESH_BAZARR_API_KEY;
|
||||
if (!url) throw new Error("no Bazarr URL — set MESH_BAZARR_URL");
|
||||
|
||||
@@ -104,7 +104,8 @@
|
||||
"env": {
|
||||
"MESH_BAZARR_URL": "http://127.0.0.1:${port:6767}",
|
||||
"MESH_BAZARR_CONFIG_DIR": "${dir:config}",
|
||||
"MESH_SERVARR_DIR": "${dir:state}"
|
||||
"MESH_SERVARR_DIR": "${dir:state}",
|
||||
"MESH_BAZARR_CONTAINER": "bazarr"
|
||||
},
|
||||
"restart-on": [
|
||||
"bound-sonarr-api",
|
||||
@@ -152,7 +153,8 @@
|
||||
"env": {
|
||||
"MESH_BAZARR_URL": "http://127.0.0.1:${port:6767}",
|
||||
"MESH_BAZARR_CONFIG_FILE": "${dir:state}/config.json",
|
||||
"MESH_BAZARR_CONFIG_DIR": "${dir:config}"
|
||||
"MESH_BAZARR_CONFIG_DIR": "${dir:config}",
|
||||
"MESH_BAZARR_CONTAINER": "bazarr"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -31,7 +31,7 @@ if (!bazarrUp) {
|
||||
}
|
||||
|
||||
// Read after bazarr answers: on a first start bazarr writes its config.yaml, key included, as it boots.
|
||||
const apiKey = apiKeyFromConfigDir(process.env.MESH_BAZARR_CONFIG_DIR);
|
||||
const apiKey = apiKeyFromConfigDir(process.env.MESH_BAZARR_CONFIG_DIR, process.env.MESH_BAZARR_CONTAINER);
|
||||
if (!apiKey) {
|
||||
console.error("[bazarr-servarr] no bazarr API key in bazarr's config/config.yaml under MESH_BAZARR_CONFIG_DIR");
|
||||
process.exit(1);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// { response: { result: "success" | "error", message, data } }. This client unwraps that envelope
|
||||
// and hands back only the data.
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
export interface TautulliSession {
|
||||
@@ -50,11 +51,20 @@ function meshConfig(file?: string): Record<string, string> {
|
||||
* again on the next start. Undefined when there is no file or no key yet (a fresh install whose
|
||||
* setup wizard has not run).
|
||||
*/
|
||||
export function keyOfTautulli(dir?: string): string | undefined {
|
||||
if (!dir) return undefined;
|
||||
let ini: string;
|
||||
try { ini = readFileSync(`${dir.replace(/\/$/, "")}/config.ini`, "utf8"); }
|
||||
catch { return undefined; }
|
||||
export function keyOfTautulli(dir?: string, container?: string): string | undefined {
|
||||
if (!dir && !container) return undefined;
|
||||
let ini: string | undefined;
|
||||
if (dir) {
|
||||
try { ini = readFileSync(`${dir.replace(/\/$/, "")}/config.ini`, "utf8"); }
|
||||
catch { ini = undefined; }
|
||||
}
|
||||
// Through Tautulli's own container when this machine's account may not read its directory: it
|
||||
// belongs to the account Tautulli runs as here, mode 0700, and the node's runtime is another.
|
||||
if (ini === undefined && container) {
|
||||
try { ini = execFileSync("docker", ["exec", container, "cat", "/config/config.ini"], { encoding: "utf8", timeout: 10_000 }); }
|
||||
catch { return undefined; }
|
||||
}
|
||||
if (ini === undefined) return undefined;
|
||||
let section = "";
|
||||
for (const raw of ini.split(/\r?\n/)) {
|
||||
const line = raw.trim();
|
||||
@@ -86,7 +96,7 @@ export class TautulliClient {
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): TautulliClient {
|
||||
const cfg = meshConfig(env.MESH_TAUTULLI_CONFIG_FILE);
|
||||
const url = cfg.url ?? (env.MESH_TAUTULLI_URL ?? `http://127.0.0.1:${env.TAUTULLI_PORT ?? "8181"}`);
|
||||
const apiKey = cfg.apiKey ?? env.MESH_TAUTULLI_APIKEY ?? keyOfTautulli(env.MESH_TAUTULLI_CONFIG_DIR);
|
||||
const apiKey = cfg.apiKey ?? env.MESH_TAUTULLI_APIKEY ?? keyOfTautulli(env.MESH_TAUTULLI_CONFIG_DIR, env.MESH_TAUTULLI_CONTAINER);
|
||||
if (!apiKey) throw new Error("no Tautulli API key — Tautulli's config.ini has none yet (finish its setup and enable the API)");
|
||||
return new TautulliClient(url, apiKey);
|
||||
}
|
||||
|
||||
@@ -106,7 +106,8 @@
|
||||
"env": {
|
||||
"MESH_TAUTULLI_URL": "http://127.0.0.1:${port:8181}",
|
||||
"MESH_TAUTULLI_CONFIG_DIR": "${dir:config}",
|
||||
"MESH_PLEX_DIR": "${dir:state}"
|
||||
"MESH_PLEX_DIR": "${dir:state}",
|
||||
"MESH_TAUTULLI_CONTAINER": "tautulli"
|
||||
},
|
||||
"restart-on": [
|
||||
"server",
|
||||
@@ -150,7 +151,8 @@
|
||||
"env": {
|
||||
"MESH_TAUTULLI_URL": "http://127.0.0.1:${port:8181}",
|
||||
"MESH_TAUTULLI_CONFIG_FILE": "${dir:mesh-state}/config.json",
|
||||
"MESH_TAUTULLI_CONFIG_DIR": "${dir:config}"
|
||||
"MESH_TAUTULLI_CONFIG_DIR": "${dir:config}",
|
||||
"MESH_TAUTULLI_CONTAINER": "tautulli"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -19,7 +19,7 @@ const waitSeconds = Number(process.env.MESH_TAUTULLI_WAIT_SECONDS ?? "180");
|
||||
const http: Http = { fetch: (u, init) => fetch(u, init) };
|
||||
const read = (path: string) => readFile(path, "utf8").catch(() => undefined);
|
||||
|
||||
const apiKey = keyOfTautulli(process.env.MESH_TAUTULLI_CONFIG_DIR);
|
||||
const apiKey = keyOfTautulli(process.env.MESH_TAUTULLI_CONFIG_DIR, process.env.MESH_TAUTULLI_CONTAINER);
|
||||
if (!apiKey) {
|
||||
console.error("[tautulli-plex] Tautulli's config.ini holds no API key yet — it writes one on its first start");
|
||||
process.exit(1);
|
||||
|
||||
Reference in New Issue
Block a user