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. - `playerctl`: read and control MPRIS players.
- `python3`: run the unified lyric-source adapter and dynamic lyric parser. - `python3`: run the unified lyric-source adapter and dynamic lyric parser.
- `cp`: preserve local MPRIS artwork in the plugin cache. - `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 ## 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 Spotify, Apple Music, Musixmatch, and Qishui credentials may therefore be stored
in plaintext in Noctalia's settings. The plugin never scans browser cookies, in plaintext in Noctalia's settings. The plugin never scans browser cookies,
never logs credential values, and deletes its temporary credential request file 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 ## Settings
+36 -21
View File
@@ -16,6 +16,8 @@ local coverInFlight = nil
local pluginDir = noctalia.pluginDir() or "/tmp" local pluginDir = noctalia.pluginDir() or "/tmp"
local cacheDir = pluginDir .. "/.cache" local cacheDir = pluginDir .. "/.cache"
noctalia.mkdirAll(cacheDir) noctalia.mkdirAll(cacheDir)
local requestDir = cacheDir .. "/requests"
noctalia.mkdirAll(requestDir)
local krcTmp = cacheDir .. "/krc.tmp" local krcTmp = cacheDir .. "/krc.tmp"
local lyricsSource = noctalia.getConfig("lyrics_source") or "auto" local lyricsSource = noctalia.getConfig("lyrics_source") or "auto"
local lyricsSources = noctalia.getConfig("lyrics_sources") or { local lyricsSources = noctalia.getConfig("lyrics_sources") or {
@@ -32,8 +34,8 @@ local currentEmbeddedLyrics = ""
local currentPlayerInstance = "" local currentPlayerInstance = ""
local currentArtUrl = "" local currentArtUrl = ""
for _, name in ipairs(noctalia.listDir(cacheDir) or {}) do for _, name in ipairs(noctalia.listDir(requestDir) or {}) do
if name:match("^source_request_.*%.json$") then noctalia.removeFile(cacheDir .. "/" .. name) end if name:match("^source_request_.*%.json$") then noctalia.removeFile(requestDir .. "/" .. name) end
end end
local function normalizePatterns(value) local function normalizePatterns(value)
@@ -279,7 +281,7 @@ local function fetchLyricsNetEase(track, embeddedLyrics)
if not ok then klyricStr = "" end if not ok then klyricStr = "" end
end end
if klyricStr ~= "" then 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) noctalia.runAsync(py, function(r3)
if inFlight ~= flight then return end if inFlight ~= flight then return end
if tk ~= lastTrackKey then inFlight = nil; return end if tk ~= lastTrackKey then inFlight = nil; return end
@@ -357,7 +359,7 @@ local function fetchLyricsNetEase(track, embeddedLyrics)
end end
local function runPy(script, cb) local function runPy(script, cb)
local py = 'python3 "' .. dir .. "/" .. script .. '" "' .. qTmp .. '"' local py = "python3 " .. shellQuote(dir .. "/" .. script) .. " " .. shellQuote(qTmp)
noctalia.runAsync(py, function(r) noctalia.runAsync(py, function(r)
if inFlight ~= flight then return end if inFlight ~= flight then return end
if tk ~= lastTrackKey then inFlight = nil; return end if tk ~= lastTrackKey then inFlight = nil; return end
@@ -436,7 +438,7 @@ local function fetchLyricsNetEase(track, embeddedLyrics)
return return
end end
local requestPath = cacheDir .. "/source_request_" .. tostring(fetchGeneration) .. ".json" local requestPath = requestDir .. "/source_request_" .. tostring(fetchGeneration) .. ".json"
local sources = normalizedSources() local sources = normalizedSources()
local request = { local request = {
track = track, track = track,
@@ -470,28 +472,41 @@ local function fetchLyricsNetEase(track, embeddedLyrics)
request.source = source request.source = source
request.credentials = credentialsFor(source) request.credentials = credentialsFor(source)
local encoded = noctalia.json.encode(request) local encoded = noctalia.json.encode(request)
if not encoded or not noctalia.writeFile(requestPath, encoded) then if not encoded then
trySource(index + 1) trySource(index + 1)
return return
end end
local command = "chmod 600 " .. shellQuote(requestPath) .. " && python3 "
.. shellQuote(pluginDir .. "/lyric_sources.py") .. " " .. shellQuote(requestPath) -- Secure the containing directory before any credentials are written.
local started = noctalia.runAsync(command, function(result) local secured = noctalia.runAsync("chmod 700 " .. shellQuote(requestDir), function(chmodResult)
noctalia.removeFile(requestPath)
if inFlight ~= flight or tk ~= lastTrackKey then return end if inFlight ~= flight or tk ~= lastTrackKey then return end
local parsed = noctalia.json.decode(result.stdout or "") if chmodResult.exitCode ~= 0 or not noctalia.writeFile(requestPath, encoded) then
if type(parsed) == "table" and parsed.type == "lyrics" and type(parsed.lines) == "table" and #parsed.lines > 0 then trySource(index + 1)
cache[tk] = parsed.lines return
evictCache() end
inFlight = nil
noctalia.state.set("lyrics", parsed.lines) local command = "python3 " .. shellQuote(pluginDir .. "/lyric_sources.py")
noctalia.state.set("lyrics_source_used", parsed.source or source) .. " " .. shellQuote(requestPath)
else 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) trySource(index + 1)
end end
end, 30000) end, 5000)
if not started then if not secured then
noctalia.removeFile(requestPath)
trySource(index + 1) trySource(index + 1)
end end
end end