Add darktable REST API implementation
- 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.
This commit is contained in:
96
contrib/darktable-api/auth.lua
Normal file
96
contrib/darktable-api/auth.lua
Normal file
@@ -0,0 +1,96 @@
|
||||
-- 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
|
||||
Reference in New Issue
Block a user