fix(lyrics): secure credential request handling

This commit is contained in:
h465855hgg
2026-07-18 18:46:17 +08:00
parent fc3d3447d0
commit 2c42b32101
2 changed files with 40 additions and 23 deletions
+4 -2
View File
@@ -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
+36 -21
View File
@@ -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