From 95b0e7a8a64e908480ef799ab0cf4058faf27b5b Mon Sep 17 00:00:00 2001 From: Florian <52180080+florianjs@users.noreply.github.com> Date: Thu, 8 Jan 2026 14:37:07 +0100 Subject: [PATCH] Fix Torznab API: accept 32-char passkeys and return proper XML error responses --- package.json | 2 +- scripts/install.sh | 10 +++++----- server/api/torznab/download.get.ts | 29 ++++++++++++++++------------- server/api/torznab/index.get.ts | 14 +++++++++++++- server/api/torznab/utils/auth.ts | 27 ++++++++++++++++----------- 5 files changed, 51 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index 6979b89..8604f8a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "trackarr", - "version": "0.5.3", + "version": "0.5.4", "type": "module", "private": true, "scripts": { diff --git a/scripts/install.sh b/scripts/install.sh index 3c05bbd..777b6f6 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -726,11 +726,11 @@ main() { echo -e "${GREEN}${BOLD}" echo " ┌──────────────────────────────────────────────────────────────────────────┐" echo " │ │" - echo " │ ____ ____ _____ _ _ _____ ____ _ ____ _ _______ ____ │" - echo " │ / __ \| _ \| ____| \ | |_ _| _ \ / \ / ___| |/ / ____| _ \ │" - echo " │ | | | | |_) | _| | \| | | | | |_) | / _ \| | | ' /| _| | |_) | │" - echo " │ | |__| | __/| |___| |\ | | | | _ < / ___ \ |___| . \| |___| _ < │" - echo " │ \____/|_| |_____|_| \_| |_| |_| \_/_/ \_\____|_|\_\_____|_| \_\ │" + echo " │ _____ ____ _ ____ _ __ _ ____ ____ │" + echo " │ |_ _| _ \ / \ / ___| |/ / / \ | _ \| _ \ │" + echo " │ | | | |_) | / _ \| | | ' / / _ \ | |_) | |_) | │" + echo " │ | | | _ < / ___ \ |___| . \ / ___ \| _ <| _ < │" + echo " │ |_| |_| \_\/_/ \_\____|_|\_\/_/ \_\_| \_\_| \_\ │" echo " │ │" echo " │ Production Server Installation Script │" echo " │ │" diff --git a/server/api/torznab/download.get.ts b/server/api/torznab/download.get.ts index 8e7d59e..b8a14f4 100644 --- a/server/api/torznab/download.get.ts +++ b/server/api/torznab/download.get.ts @@ -29,28 +29,31 @@ export default defineEventHandler(async (event) => { const enabled = await getTorznabEnabled(); if (!enabled) { setHeader(event, 'Content-Type', 'application/xml; charset=utf-8'); - throw createError({ - statusCode: 503, - message: 'Torznab API is currently disabled', - data: buildErrorXml({ - code: 900, - description: 'Torznab API is currently disabled', - }), + return buildErrorXml({ + code: 900, + description: 'Torznab API is currently disabled', }); } // Authenticate via passkey - const user = await authenticateTorznab(event); + let user: Awaited>; + try { + user = await authenticateTorznab(event); + } catch (error: any) { + // If it's a Torznab error, return the XML directly + if (error.isTorznab && error.data) { + setHeader(event, 'Content-Type', 'application/xml; charset=utf-8'); + setResponseStatus(event, error.statusCode || 400); + return error.data; + } + throw error; + } // Check if user is blocked from Torznab API const blockStatus = await isTorznabUserBlocked(user.passkey); if (blockStatus.blocked) { setHeader(event, 'Content-Type', 'application/xml; charset=utf-8'); - throw createError({ - statusCode: 403, - message: blockStatus.reason || 'Access denied', - data: buildErrorXml(TORZNAB_ERRORS.ACCOUNT_SUSPENDED), - }); + return buildErrorXml(TORZNAB_ERRORS.ACCOUNT_SUSPENDED); } // Apply dynamic rate limiting diff --git a/server/api/torznab/index.get.ts b/server/api/torznab/index.get.ts index a2792cf..075984d 100644 --- a/server/api/torznab/index.get.ts +++ b/server/api/torznab/index.get.ts @@ -83,7 +83,19 @@ export default defineEventHandler(async (event) => { } // All other functions require authentication - const user = await authenticateTorznab(event); + // Wrap in try/catch to handle Torznab XML errors properly + let user: { passkey: string }; + try { + user = await authenticateTorznab(event); + } catch (error: any) { + // If it's a Torznab error, return the XML directly + if (error.isTorznab && error.data) { + setHeader(event, 'Content-Type', 'application/xml; charset=utf-8'); + setResponseStatus(event, error.statusCode || 400); + return error.data; + } + throw error; + } // Check if user is blocked from Torznab API const blockStatus = await isTorznabUserBlocked(user.passkey); diff --git a/server/api/torznab/utils/auth.ts b/server/api/torznab/utils/auth.ts index b03a6a5..7f373f5 100644 --- a/server/api/torznab/utils/auth.ts +++ b/server/api/torznab/utils/auth.ts @@ -35,12 +35,12 @@ export async function authenticateTorznab( ); } - // Validate passkey format (40 hex chars) - if (!/^[a-f0-9]{40}$/i.test(apikey)) { + // Validate passkey format (32 or 40 hex chars - supports legacy and new passkeys) + if (!/^[a-f0-9]{32}$/i.test(apikey) && !/^[a-f0-9]{40}$/i.test(apikey)) { throw createTorznabError( event, TORZNAB_ERRORS.INCORRECT_CREDENTIALS, - `Invalid API key format. Expected 40 hex characters (your passkey), got ${apikey.length} characters` + `Invalid API key format. Expected 32 or 40 hex characters (your passkey), got ${apikey.length} characters` ); } @@ -73,24 +73,29 @@ export async function authenticateTorznab( /** * Create a Torznab-compliant error response + * The error contains XML in the 'data' field which should be extracted by the caller */ export function createTorznabError( event: H3Event, error: { code: number; description: string }, customMessage?: string -): Error { - setHeader(event, 'Content-Type', 'application/xml; charset=utf-8'); - +): never { const httpStatus = error.code === 100 || error.code === 101 ? 401 : 400; + const xml = buildErrorXml({ + code: error.code, + description: customMessage ?? error.description, + }); - throw createError({ + const err = createError({ statusCode: httpStatus, + statusMessage: 'Torznab Error', message: customMessage ?? error.description, - data: buildErrorXml({ - code: error.code, - description: customMessage ?? error.description, - }), + data: xml, }); + // Mark this as a Torznab error so we can handle it specially + (err as any).isTorznab = true; + + throw err; } /**