diff --git a/lyrics/README.md b/lyrics/README.md index 103dc82..7be0e02 100644 --- a/lyrics/README.md +++ b/lyrics/README.md @@ -18,7 +18,8 @@ Install these commands on `PATH`: - `playerctl`: read and control MPRIS players. - `python3`: run the unified lyric-source adapter and dynamic lyric parser. - `cp`: preserve local MPRIS artwork in the plugin cache. -- `chmod`: restrict temporary credential request files to the current user. +- `chmod`: secures the temporary request directory before credentials are + written. ## Usage @@ -106,7 +107,8 @@ Noctalia currently exposes these as normal string settings, not secret fields. Spotify, Apple Music, Musixmatch, and Qishui credentials may therefore be stored in plaintext in Noctalia's settings. The plugin never scans browser cookies, never logs credential values, and deletes its temporary credential request file -as soon as the source adapter reads it. +as soon as the source adapter reads it. The service applies mode `0700` to the +request directory before writing any credential-bearing file. ## Settings diff --git a/lyrics/lyrics_service.luau b/lyrics/lyrics_service.luau index e1690f1..a52f057 100644 --- a/lyrics/lyrics_service.luau +++ b/lyrics/lyrics_service.luau @@ -16,6 +16,8 @@ local coverInFlight = nil local pluginDir = noctalia.pluginDir() or "/tmp" local cacheDir = pluginDir .. "/.cache" noctalia.mkdirAll(cacheDir) +local requestDir = cacheDir .. "/requests" +noctalia.mkdirAll(requestDir) local krcTmp = cacheDir .. "/krc.tmp" local lyricsSource = noctalia.getConfig("lyrics_source") or "auto" local lyricsSources = noctalia.getConfig("lyrics_sources") or { @@ -32,8 +34,8 @@ local currentEmbeddedLyrics = "" local currentPlayerInstance = "" local currentArtUrl = "" -for _, name in ipairs(noctalia.listDir(cacheDir) or {}) do - if name:match("^source_request_.*%.json$") then noctalia.removeFile(cacheDir .. "/" .. name) end +for _, name in ipairs(noctalia.listDir(requestDir) or {}) do + if name:match("^source_request_.*%.json$") then noctalia.removeFile(requestDir .. "/" .. name) end end local function normalizePatterns(value) @@ -279,7 +281,7 @@ local function fetchLyricsNetEase(track, embeddedLyrics) if not ok then klyricStr = "" end end if klyricStr ~= "" then - local py = 'python3 "' .. noctalia.pluginDir() .. '/krc_decode.py" "' .. krcTmp .. '"' + local py = "python3 " .. shellQuote(pluginDir .. "/krc_decode.py") .. " " .. shellQuote(krcTmp) noctalia.runAsync(py, function(r3) if inFlight ~= flight then return end if tk ~= lastTrackKey then inFlight = nil; return end @@ -357,7 +359,7 @@ local function fetchLyricsNetEase(track, embeddedLyrics) end local function runPy(script, cb) - local py = 'python3 "' .. dir .. "/" .. script .. '" "' .. qTmp .. '"' + local py = "python3 " .. shellQuote(dir .. "/" .. script) .. " " .. shellQuote(qTmp) noctalia.runAsync(py, function(r) if inFlight ~= flight then return end if tk ~= lastTrackKey then inFlight = nil; return end @@ -436,7 +438,7 @@ local function fetchLyricsNetEase(track, embeddedLyrics) return end - local requestPath = cacheDir .. "/source_request_" .. tostring(fetchGeneration) .. ".json" + local requestPath = requestDir .. "/source_request_" .. tostring(fetchGeneration) .. ".json" local sources = normalizedSources() local request = { track = track, @@ -470,28 +472,41 @@ local function fetchLyricsNetEase(track, embeddedLyrics) request.source = source request.credentials = credentialsFor(source) local encoded = noctalia.json.encode(request) - if not encoded or not noctalia.writeFile(requestPath, encoded) then + if not encoded then trySource(index + 1) return end - local command = "chmod 600 " .. shellQuote(requestPath) .. " && python3 " - .. shellQuote(pluginDir .. "/lyric_sources.py") .. " " .. shellQuote(requestPath) - local started = noctalia.runAsync(command, function(result) - noctalia.removeFile(requestPath) + + -- Secure the containing directory before any credentials are written. + local secured = noctalia.runAsync("chmod 700 " .. shellQuote(requestDir), function(chmodResult) if inFlight ~= flight or tk ~= lastTrackKey then return end - local parsed = noctalia.json.decode(result.stdout or "") - if type(parsed) == "table" and parsed.type == "lyrics" and type(parsed.lines) == "table" and #parsed.lines > 0 then - cache[tk] = parsed.lines - evictCache() - inFlight = nil - noctalia.state.set("lyrics", parsed.lines) - noctalia.state.set("lyrics_source_used", parsed.source or source) - else + if chmodResult.exitCode ~= 0 or not noctalia.writeFile(requestPath, encoded) then + trySource(index + 1) + return + end + + local command = "python3 " .. shellQuote(pluginDir .. "/lyric_sources.py") + .. " " .. shellQuote(requestPath) + local started = noctalia.runAsync(command, function(result) + noctalia.removeFile(requestPath) + if inFlight ~= flight or tk ~= lastTrackKey then return end + local parsed = noctalia.json.decode(result.stdout or "") + if type(parsed) == "table" and parsed.type == "lyrics" and type(parsed.lines) == "table" and #parsed.lines > 0 then + cache[tk] = parsed.lines + evictCache() + inFlight = nil + noctalia.state.set("lyrics", parsed.lines) + noctalia.state.set("lyrics_source_used", parsed.source or source) + else + trySource(index + 1) + end + end, 30000) + if not started then + noctalia.removeFile(requestPath) trySource(index + 1) end - end, 30000) - if not started then - noctalia.removeFile(requestPath) + end, 5000) + if not secured then trySource(index + 1) end end