- Introduced a new Lua-based REST API for darktable, allowing external applications to interact with the darktable library. - Implemented core functionalities including image listing, retrieval, and preview serving. - Added support for OAuth 2.0 authentication with token generation. - Created a simple HTTP router to handle API requests. - Included JSON encoding/decoding utilities for API responses. - Developed a script to launch the API server alongside darktable, ensuring seamless integration. - Added necessary configuration and setup for the API server, including CORS support and error handling.
97 lines
3.5 KiB
Lua
97 lines
3.5 KiB
Lua
-- OAuth 2.0 Client Credentials grant + HS256 JWT
|
|
-- Requires: LuaJIT FFI with libcrypto (OpenSSL)
|
|
local ffi = require "ffi"
|
|
local base64 = require "base64"
|
|
local json = require "json"
|
|
local config = require "config"
|
|
|
|
-- ── OpenSSL HMAC-SHA256 via FFI ───────────────────────────────────────────────
|
|
|
|
ffi.cdef[[
|
|
void *EVP_sha256(void);
|
|
unsigned char *HMAC(const void *evp_md, const void *key, int key_len,
|
|
const unsigned char *data, size_t data_len,
|
|
unsigned char *md, unsigned int *md_len);
|
|
]]
|
|
|
|
-- libcrypto ships as part of OpenSSL; path varies by distro but the soname is standard
|
|
local crypto = ffi.load("crypto")
|
|
|
|
local function hmac_sha256(key, data)
|
|
local digest = ffi.new("unsigned char[32]")
|
|
local dlen = ffi.new("unsigned int[1]")
|
|
crypto.HMAC(crypto.EVP_sha256(), key, #key, data, #data, digest, dlen)
|
|
return ffi.string(digest, 32)
|
|
end
|
|
|
|
-- ── JWT helpers ───────────────────────────────────────────────────────────────
|
|
|
|
local HEADER_B64 = base64.urlencode(json.encode({ alg = "HS256", typ = "JWT" }))
|
|
|
|
local function jwt_sign(payload_tbl)
|
|
local payload = base64.urlencode(json.encode(payload_tbl))
|
|
local signing = HEADER_B64 .. "." .. payload
|
|
local sig = base64.urlencode(hmac_sha256(config.jwt_secret, signing))
|
|
return signing .. "." .. sig
|
|
end
|
|
|
|
local function jwt_verify(token)
|
|
local h, p, s = token:match("^([^.]+)%.([^.]+)%.([^.]+)$")
|
|
if not h then return nil, "malformed token" end
|
|
|
|
local expected = base64.urlencode(hmac_sha256(config.jwt_secret, h .. "." .. p))
|
|
if s ~= expected then return nil, "invalid signature" end
|
|
|
|
local payload, err = json.decode(base64.urldecode(p))
|
|
if not payload then return nil, "invalid payload: " .. (err or "?") end
|
|
|
|
if os.time() > (payload.exp or 0) then return nil, "token expired" end
|
|
|
|
return payload
|
|
end
|
|
|
|
-- ── Public API ────────────────────────────────────────────────────────────────
|
|
|
|
local M = {}
|
|
|
|
-- Validate client_id / client_secret pair; returns true + scopes or false + error.
|
|
function M.validate_client(client_id, client_secret)
|
|
local client = config.clients[client_id]
|
|
if not client then return false, "unknown client" end
|
|
if client.secret ~= client_secret then return false, "invalid client secret" end
|
|
return true, client.scopes
|
|
end
|
|
|
|
-- Issue an access token for a validated client.
|
|
function M.issue_token(client_id, scopes)
|
|
local now = os.time()
|
|
return jwt_sign({
|
|
iss = "darktable-api",
|
|
sub = client_id,
|
|
scope = table.concat(scopes, " "),
|
|
iat = now,
|
|
exp = now + config.token_ttl,
|
|
})
|
|
end
|
|
|
|
-- Extract and verify a Bearer token from a request.
|
|
-- Returns payload table on success, or nil + error string on failure.
|
|
function M.authenticate(req)
|
|
local auth = (req.headers or {})["authorization"] or ""
|
|
local token = auth:match("^[Bb]earer%s+(.+)$")
|
|
if not token then return nil, "missing Bearer token" end
|
|
return jwt_verify(token)
|
|
end
|
|
|
|
-- Check that the authenticated payload contains a required scope.
|
|
function M.require_scope(payload, scope)
|
|
if not payload then return false end
|
|
local scopes = payload.scope or ""
|
|
for s in scopes:gmatch("%S+") do
|
|
if s == scope then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
return M
|