From 43d66da47378fa27040d407449710ab239d6b8e4 Mon Sep 17 00:00:00 2001 From: Max Malm Date: Sun, 23 Jun 2024 15:27:41 +0200 Subject: [PATCH] initial commit --- .dockerignore | 3 + .github/workflows/docker-publish.yml | 27 +++++++ .gitignore | 44 ++++++++++++ .prettierrc | 4 ++ Dockerfile | 11 +++ README.md | 1 + bun.lockb | Bin 0 -> 4516 bytes package.json | 19 +++++ src/config.ts | 65 +++++++++++++++++ src/index.ts | 51 +++++++++++++ tsconfig.json | 103 +++++++++++++++++++++++++++ 11 files changed, 328 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker-publish.yml create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 Dockerfile create mode 100644 README.md create mode 100755 bun.lockb create mode 100644 package.json create mode 100644 src/config.ts create mode 100644 src/index.ts create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ecd08d5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +node_modules +README.md +./config \ No newline at end of file diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..e860a8b --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,27 @@ +name: Build and Publish Docker Image + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Docker image + run: docker build -t ghcr.io/${{ github.repository }}/prowlarr-proxy:latest . + + - name: Push Docker image + run: docker push ghcr.io/${{ github.repository }}/prowlarr-proxy:latest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bdb241f --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel + +**/*.trace +**/*.zip +**/*.tar.gz +**/*.tgz +**/*.log +package-lock.json +**/*.bun + +config/ \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..222861c --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 2, + "useTabs": false +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6c42167 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM oven/bun + +EXPOSE 3000 + +COPY ./package.json ./bun.lockb ./ +RUN bun install + +ENV NODE_ENV=production + +COPY . . +CMD ["bun", "run", "src/index.ts"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..13f854d --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# prowlerr-proxy diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..0eb0cee4b1c563a4314aa248973758193a72d9a2 GIT binary patch literal 4516 zcmeHK2~ZPP7~X_<5RU*N6ptD~M38JkhzJbeebs7PFH}4TVZoJT6L%8?tQ9<{#}ve? zRzyK*z3~{eTJWeSqLnFDXvGUFRXnkt6)*a~WFN*0a5|l-GyP|F|M&L2@B9DvzhmF; z)=_TaSa}4^$SEdP8ew9@@W`}!O-vN6rDRchR;M<~VijT`K@hp6ny=+XXQGpB>cx;v zp7ycK6W8o{(_9q$xFI&G!YPjS&;?afRvN@9S^a`Jp@Tt{1+c-CMIefH}z0EGKZoa%riTpK=9WsO$RSr8Vr`?P$n(J`G70mr3K~Y2kVrjz>nqe2!niRh+zB}DD(n6!h8tNyZXNZ@Hl>m z1M{MCeS&OZd)6AQ12GSNlSX628~0U>VE+#9bIJ3D2AB7}UIlyt!oq7!EgIKux*#re z(dm+nQ#T~}&%Rteu1m7hg2z{;^=gw^HTsv(TYJJMKMqREtYHl(5TVrD}n+yP&tC z!ac@qh1g})LwRWjyR>MxQ!{_-DNH!sV%d^j54RjYuQ8a~r#&nDWZvCBViw&jj=f>U zi`Rj99=j*Y2F;v2vo&+cBfD&=+)#dha@K=1yDGoU=Z+n`GyAgVtnC?jyLUSqE)L$D zHzMYGoAc+zvm7j=;Ka2h?`ZT{8ttk_mwKR{fI*J z6T<5k{YS8f4u<8l=#Wx!QFJ=u{-&)ROM`Zvv=4N3b}iT+ZB8aY9~D#!L-NO;Yc ztIkaw_vq|~=_B0}QX1c0zDeRb#(r>@VctKVojt>|SyHun%Z7z_at6&zUAzo<8!Bku z;5z_ZN=V&guA$KPrr$e+PZa_AM-E-J$)3{vqhR zM`trSN70#w&L_m#ljqVlkP~4LAL2#)=sZGvh!^oAJIHIqpUHFXhYLLtaZs4jZ@kGL zM($VZv(M`zNB4ew|86SOs*Mz9BnX@#;2XWLwgsOb_|7k^{Tbsd17`(7!31AY=}Y<( zIDf$ReP~gTz6w%B;7kK&2GHU|`jAS3z_|#{9fSe}G^k)?N8v$UGdTZ1 zElNQ766aAw0JGXX(Yxy$D#B3^5-jaTDo6#+ad1vzW&=|&Yb^l;g7wSGi=%Y06ifDt zGg2(0)($taG!q>n^?4C&|Jq>t^Gwb83=Y;3%!|)krv)X6#&`q8O)?q{T%cUeQqeSL zWaCN30B1I-XQSm1ZMh7+WtJe5#^{7Px}#}>{z%D`q>AiTPk+k-0EDF=Q;>e7QiVW0 zIhxUE)if(dE=K6%qSTyGHpN7-@!%o^K}YHIGzZP{$v MJX*_M`tQB}28hjTdjJ3c literal 0 HcmV?d00001 diff --git a/package.json b/package.json new file mode 100644 index 0000000..128cd11 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "prowlarr-proxy", + "version": "1.0.50", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "bun run --watch src/index.ts", + "docker:build": "docker build -t prowlar-proxy .", + "docker:run": "docker run -p 3000:3000 -v $(pwd)/config:/config -it prowlar-proxy" + }, + "dependencies": { + "elysia": "latest", + "yaml": "^2.4.5", + "zod": "^3.23.8" + }, + "devDependencies": { + "bun-types": "latest" + }, + "module": "src/index.js" +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..1117a16 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,65 @@ +import { parse, stringify } from "yaml"; + +import { z } from "zod"; + +const pathSchema = z.object({ + label: z.string(), + path: z.string(), + baseUrl: z.string().url(), +}); + +const configSchema = z.object({ + suffix: z.string().default(""), + paths: z.array(pathSchema), +}); + +type Config = z.infer; + +const defaultConfig: Config = { + suffix: "🦊", + paths: [ + { + label: "radarr", + path: "/radarr/*", + baseUrl: "http://radarr:7878", + }, + { + label: "sonarr", + path: "/sonarr/*", + baseUrl: "http://sonarr:8989", + }, + ], +}; + +export async function getsertConfig() { + const configPath = + process.env.NODE_ENV !== "production" + ? "./config/config.yml" + : "/config/config.yml"; + console.log("🦊 Config path:", configPath); + + let config: Config; + const file = Bun.file(configPath); + + const exists = await file.exists(); + if (exists) { + const text = await file.text(); + console.log("🦊 Loading config file:"); + console.log(text); + const json = parse(text); + const maybeConfig = configSchema.safeParse(json); + if (maybeConfig.success) { + config = maybeConfig.data; + } else { + console.error("🦊 Invalid config file:"); + console.error(maybeConfig.error.errors); + process.exit(1); + } + } else { + console.log("🦊 Config file not found, creating one with default values."); + await Bun.write(configPath, stringify(defaultConfig)); + config = defaultConfig; + } + + return config; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..ac85255 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,51 @@ +import { Elysia } from "elysia"; +import { getsertConfig } from "./config"; + +async function main() { + const app = new Elysia(); + const { paths, suffix } = await getsertConfig(); + + for (const { label, path, baseUrl } of paths) { + app.all(path, async ({ body, path, request, set }) => { + console.log(`[${label}] incoming: ${path}`); + if (isBodyWithName(body)) { + console.log(`[${label}] name: ${body.name}`); + body.name = body.name.replace(" (Prowlarr)", suffix); + } + const res = await fetch(`${baseUrl}${path}`, { + method: request.method, + headers: request.headers, + body: body ? JSON.stringify(body) : undefined, + }); + const data = await res.json(); + const version = res.headers.get("X-Application-Version"); + if (version) { + set.headers["X-Application-Version"] = version; + } + console.log(`[${label}] done: ${path}`); + return data; + }); + console.log(`🦊 ${label} is proxied at ${path} (${baseUrl})`); + } + + app.listen(3000); + console.log( + `🦊 prowlarr-proxy is running at ${app.server?.hostname}:${app.server?.port}` + ); +} + +void main(); + +function isBodyWithName(body: unknown): body is { name: string } { + return ( + typeof body === "object" && + body !== null && + "name" in body && + typeof (body as { name: unknown }).name === "string" + ); +} + +process.on("SIGINT", () => { + console.log("🦊 good bye"); + process.exit(); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ca2350 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,103 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "ES2022", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + "types": ["bun-types"], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +}