From b4adfd2ffa69529fbc579b05ab70c96c4d8347ac Mon Sep 17 00:00:00 2001 From: fallenbagel <98979876+fallenbagel@users.noreply.github.com> Date: Wed, 20 Aug 2025 03:02:21 +0800 Subject: [PATCH] feat: dns caching manager (#1294) * feat(dns): implement dns caching * feat: simple implementation of dnscaching * feat: dynamic ttl which is revalidated while using stale dns cache This is done as tmdb ttl is very less like 40 seconds so to make sure any issues wont be caused due to cached dns (previously we were caching for 5 minutes no matter what ttl) * feat(dns): improve DNS cache with multi-strategy fallback system - multiple DNS resolution strategie - graceful fallbacks between IPv6 and IPv4 addresses - network error reporting in fetch fix - compatibility with cypress testing (I HOPE) * fix: typos * feat: dns cache stats in jobs & cache page (and cleanup) * feat(networksettings): cache dns off by default * feat: make dnsCache optional and enable-able through network settings * chore(i18n): extract translation keys * test(cypress): fix cypress testing * feat(dnscache): dns cache entries are now flushable * style(cypress): run prettier * chore(cypresssettings): git ignore cypress json settings * chore: ignore cypress/config/settings.json * fix(dnscache): use entry specific hits and misses not global * refactor: clean up console logs * fix(dnscache): fix miss counter * feat(dnscache): global stats * chore(i18n): extract translation keys * refactor: use date-fns for formatting age and remove useless code * refactor: remove cypress testing options in dnsCacheManager * refactor: remove console logs * refactor: removed useless condition when its always truthy * fix: remove FetchAPI-related code * fix: remove old ipv4first setting * refactor: use our own dns-caching package instead * fix: correct dns-caching module configuration * fix: correct dns-caching module configuration * fix: remove useless lru-cache dependency * fix: update dns-caching to v0.2.0 * fix: add env variable for min/max ttl & update dns-caching * fix: update dns-caching package * fix: add force min/max TTL in network settings * docs: add docs for dns caching --------- Co-authored-by: Gauthier --- .prettierignore | 1 + .prettierrc.js | 6 + cypress/config/settings.cypress.json | 22 +- docs/using-jellyseerr/settings/dns-caching.md | 16 ++ docs/using-jellyseerr/settings/jobs&cache.md | 1 + jellyseerr-api.yml | 121 +++++++++- package.json | 1 + pnpm-lock.yaml | 214 +++++++++--------- server/index.ts | 9 + server/interfaces/api/settingsInterfaces.ts | 30 +++ server/lib/settings/index.ts | 34 ++- server/routes/settings/index.ts | 22 ++ server/utils/dnsCache.ts | 28 +++ .../Settings/SettingsJobsCache/index.tsx | 134 +++++++++++ .../Settings/SettingsNetwork/index.tsx | 99 ++++++++ src/i18n/locale/en.json | 21 ++ 16 files changed, 641 insertions(+), 118 deletions(-) create mode 100644 docs/using-jellyseerr/settings/dns-caching.md create mode 100644 server/utils/dnsCache.ts diff --git a/.prettierignore b/.prettierignore index c2e778c1d..736c402cb 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,6 +4,7 @@ dist/ config/ CHANGELOG.md pnpm-lock.yaml +cypress/config/settings.cypress.json # assets src/assets/ diff --git a/.prettierrc.js b/.prettierrc.js index 10da08eb1..25b39dd3b 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -21,5 +21,11 @@ module.exports = { rangeEnd: 0, // default: Infinity }, }, + { + files: 'cypress/config/settings.cypress.json', + options: { + rangeEnd: 0, + }, + }, ], }; diff --git a/cypress/config/settings.cypress.json b/cypress/config/settings.cypress.json index d10a107fd..e30dde861 100644 --- a/cypress/config/settings.cypress.json +++ b/cypress/config/settings.cypress.json @@ -6,7 +6,6 @@ "apiKey": "testkey", "applicationTitle": "Jellyseerr", "applicationUrl": "", - "csrfProtection": false, "cacheImages": false, "defaultPermissions": 32, "defaultQuotas": { @@ -180,5 +179,26 @@ "image-cache-cleanup": { "schedule": "0 0 5 * * *" } + }, + "network": { + "csrfProtection": false, + "trustProxy": false, + "forceIpv4First": false, + "dnsServers": "", + "proxy": { + "enabled": false, + "hostname": "", + "port": 8080, + "useSsl": false, + "user": "", + "password": "", + "bypassFilter": "", + "bypassLocalAddresses": true + }, + "dnsCache": { + "enabled": false, + "forceMinTtl": 0, + "forceMaxTtl": -1 + } } } diff --git a/docs/using-jellyseerr/settings/dns-caching.md b/docs/using-jellyseerr/settings/dns-caching.md new file mode 100644 index 000000000..ad4e9597c --- /dev/null +++ b/docs/using-jellyseerr/settings/dns-caching.md @@ -0,0 +1,16 @@ +--- +title: DNS Caching +description: Configure DNS caching settings. +sidebar_position: 7 +--- + +# DNS Caching + +Jellyseerr uses DNS caching to improve performance and reduce the number of DNS lookups required for external API calls. This can help speed up response times and reduce load on DNS servers, when something like a Pi-hole is used as a DNS resolver. + +## Configuration + +You can enable the DNS caching settings in the Network tab of the Jellyseerr settings. The default values follow the standard DNS caching behavior. + +- **Force Minimum TTL**: Set a minimum time-to-live (TTL) in seconds for DNS cache entries. This ensures that frequently accessed DNS records are cached for a longer period, reducing the need for repeated lookups. Default is 0. +- **Force Maximum TTL**: Set a maximum time-to-live (TTL) in seconds for DNS cache entries. This prevents infrequently accessed DNS records from being cached indefinitely, allowing for more up-to-date information to be retrieved. Default is -1 (unlimited). diff --git a/docs/using-jellyseerr/settings/jobs&cache.md b/docs/using-jellyseerr/settings/jobs&cache.md index b68e6d911..ef8ca4871 100644 --- a/docs/using-jellyseerr/settings/jobs&cache.md +++ b/docs/using-jellyseerr/settings/jobs&cache.md @@ -1,6 +1,7 @@ --- title: Jobs & Cache description: Configure jobs and cache settings. +sidebar_position: 6 --- # Jobs & Cache diff --git a/jellyseerr-api.yml b/jellyseerr-api.yml index 60f16e51c..4254ba43e 100644 --- a/jellyseerr-api.yml +++ b/jellyseerr-api.yml @@ -260,9 +260,51 @@ components: csrfProtection: type: boolean example: false + forceIpv4First: + type: boolean + example: false trustProxy: type: boolean - example: true + example: false + proxy: + type: object + properties: + enabled: + type: boolean + example: false + hostname: + type: string + example: '' + port: + type: number + example: 8080 + useSsl: + type: boolean + example: false + user: + type: string + example: '' + password: + type: string + example: '' + bypassFilter: + type: string + example: '' + bypassLocalAddresses: + type: boolean + example: true + dnsCache: + type: object + properties: + enabled: + type: boolean + example: false + forceMinTtl: + type: number + example: 0 + forceMaxTtl: + type: number + example: -1 PlexLibrary: type: object properties: @@ -2967,6 +3009,68 @@ paths: imageCount: type: number example: 123 + dnsCache: + type: object + properties: + stats: + type: object + properties: + size: + type: number + example: 1 + maxSize: + type: number + example: 500 + hits: + type: number + example: 19 + misses: + type: number + example: 1 + failures: + type: number + example: 0 + ipv4Fallbacks: + type: number + example: 0 + hitRate: + type: number + example: 0.95 + entries: + type: array + additionalProperties: + type: object + properties: + addresses: + type: object + properties: + ipv4: + type: number + example: 1 + ipv6: + type: number + example: 1 + activeAddress: + type: string + example: 127.0.0.1 + family: + type: number + example: 4 + age: + type: number + example: 10 + ttl: + type: number + example: 10 + networkErrors: + type: number + example: 0 + hits: + type: number + example: 1 + misses: + type: number + example: 1 apiCaches: type: array items: @@ -3006,6 +3110,21 @@ paths: responses: '204': description: 'Flushed cache' + /settings/cache/dns/{dnsEntry}/flush: + post: + summary: Flush a specific DNS cache entry + description: Flushes a specific DNS cache entry + tags: + - settings + parameters: + - in: path + name: dnsEntry + required: true + schema: + type: string + responses: + '204': + description: 'Flushed dns cache' /settings/logs: get: summary: Returns logs diff --git a/package.json b/package.json index 52ed3feb5..b47b8f2dc 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "cronstrue": "2.23.0", "date-fns": "2.29.3", "dayjs": "1.11.7", + "dns-caching": "^0.2.4", "email-templates": "12.0.1", "email-validator": "2.0.4", "express": "4.21.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07051e1a6..4012b0350 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,6 +83,9 @@ importers: dayjs: specifier: 1.11.7 version: 1.11.7 + dns-caching: + specifier: ^0.2.4 + version: 0.2.4 email-templates: specifier: 12.0.1 version: 12.0.1(@babel/core@7.24.7)(encoding@0.1.13)(handlebars@4.7.8)(mustache@4.2.0)(pug@3.0.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.7) @@ -688,8 +691,8 @@ packages: resolution: {integrity: sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.27.6': - resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} + '@babel/helpers@7.28.2': + resolution: {integrity: sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==} engines: {node: '>=6.9.0'} '@babel/highlight@7.24.7': @@ -1455,8 +1458,8 @@ packages: resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} engines: {node: '>=6.9.0'} - '@babel/runtime@7.27.6': - resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} + '@babel/runtime@7.28.2': + resolution: {integrity: sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==} engines: {node: '>=6.9.0'} '@babel/template@7.24.7': @@ -1483,8 +1486,8 @@ packages: resolution: {integrity: sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==} engines: {node: '>=6.9.0'} - '@babel/types@7.28.1': - resolution: {integrity: sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==} + '@babel/types@7.28.2': + resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} engines: {node: '>=6.9.0'} '@codedependant/semantic-release-docker@5.1.0': @@ -1975,8 +1978,8 @@ packages: resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jridgewell/gen-mapping@0.3.12': - resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} '@jridgewell/gen-mapping@0.3.5': resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} @@ -1990,20 +1993,20 @@ packages: resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} - '@jridgewell/source-map@0.3.10': - resolution: {integrity: sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==} + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} '@jridgewell/sourcemap-codec@1.4.15': resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - '@jridgewell/sourcemap-codec@1.5.4': - resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@jridgewell/trace-mapping@0.3.29': - resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + '@jridgewell/trace-mapping@0.3.30': + resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} @@ -3229,8 +3232,8 @@ packages: '@swc/helpers@0.5.5': resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} - '@swc/types@0.1.23': - resolution: {integrity: sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==} + '@swc/types@0.1.24': + resolution: {integrity: sha512-tjTMh3V4vAORHtdTprLlfoMptu1WfTZG9Rsca6yOKyNYsRr+MUXutKmliB17orgSZk5DpnDxs8GUdd/qwYxOng==} '@tailwindcss/aspect-ratio@0.4.2': resolution: {integrity: sha512-8QPrypskfBa7QIMuKHg2TA7BqES6vhBrDLOv8Unb6FcFyd3TjKbc6lcmb9UPQHxfl24sXoJ41ux/H7qQQvfaSQ==} @@ -3391,8 +3394,8 @@ packages: '@types/node@17.0.45': resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} - '@types/node@18.19.118': - resolution: {integrity: sha512-hIPK0hSrrcaoAu/gJMzN3QClXE4QdCdFvaenJ0JsjIbExP1JFFVH+RHcBt25c9n8bx5dkIfqKE+uw6BmBns7ug==} + '@types/node@18.19.122': + resolution: {integrity: sha512-yzegtT82dwTNEe/9y+CM8cgb42WrUfMMCg2QqSddzO1J6uPmBD7qKCZ7dOHZP2Yrpm/kb0eqdNMn2MUyEiqBmA==} '@types/node@20.5.1': resolution: {integrity: sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg==} @@ -4087,11 +4090,6 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - browserslist@4.24.3: - resolution: {integrity: sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - browserslist@4.25.1: resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -4196,8 +4194,8 @@ packages: caniuse-lite@1.0.30001700: resolution: {integrity: sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==} - caniuse-lite@1.0.30001727: - resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==} + caniuse-lite@1.0.30001734: + resolution: {integrity: sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==} caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} @@ -4426,8 +4424,8 @@ packages: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} engines: {node: '>= 0.6'} - compression@1.8.0: - resolution: {integrity: sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==} + compression@1.8.1: + resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} engines: {node: '>= 0.8.0'} computed-style@0.1.4: @@ -4547,8 +4545,8 @@ packages: core-js-compat@3.37.1: resolution: {integrity: sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==} - core-js-compat@3.44.0: - resolution: {integrity: sha512-JepmAj2zfl6ogy34qfWtcE7nHKAJnKsQFRn++scjVS2bZFllwptzw61BZcZFYBPpUznLfAvh0LGhxKppk04ClA==} + core-js-compat@3.45.0: + resolution: {integrity: sha512-gRoVMBawZg0OnxaVv3zpqLLxaHmsubEGyTnqdpI/CEBvX4JadI1dMSHxagThprYRtSVbuQxvi6iUatdPxohHpA==} core-util-is@1.0.2: resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} @@ -4866,6 +4864,9 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dns-caching@0.2.4: + resolution: {integrity: sha512-J48CLnMScOAtWIdExkz+522A0nPUwG5o+w7vVsXBJDipVLugCnps5AVJMn9bOkqQm4GarHtutMHYJEryCTeMjA==} + doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -4937,8 +4938,8 @@ packages: electron-to-chromium@1.4.810: resolution: {integrity: sha512-Kaxhu4T7SJGpRQx99tq216gCq2nMxJo+uuT6uzz9l8TVN2stL7M06MIIXAtr9jsrLs2Glflgf2vMQRepxawOdQ==} - electron-to-chromium@1.5.182: - resolution: {integrity: sha512-Lv65Btwv9W4J9pyODI6EWpdnhfvrve/us5h1WspW8B2Fb0366REPtY3hX7ounk1CkV/TBjWCEvCBBbYbmV0qCA==} + electron-to-chromium@1.5.200: + resolution: {integrity: sha512-rFCxROw7aOe4uPTfIAx+rXv9cEcGx+buAF4npnhtTqCJk5KDFRnh3+KYj7rdVh6lsFt5/aPs+Irj9rZ33WMA7w==} email-templates@12.0.1: resolution: {integrity: sha512-849pjBFVUAWWTa3HqhDjxlXHaSWmxf4CZOlZ9iVkrSAbQ8YCYi+7KiKqt35L6F20WhSViWX7lmMjno6zBv2rNQ==} @@ -5506,8 +5507,8 @@ packages: flow-enums-runtime@0.0.6: resolution: {integrity: sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==} - flow-parser@0.275.0: - resolution: {integrity: sha512-fHNwawoA2LM7FsxhU/1lTRGq9n6/Q8k861eHgN7GKtamYt9Qrxpg/ZSrev8o1WX7fQ2D3Gg3+uvYN15PmsG7Yw==} + flow-parser@0.278.0: + resolution: {integrity: sha512-9oUcYDHf9n+E/t0FXndgBqGbaUsGEcmWqIr1ldqCzTzctsJV5E/bHusOj4ThB72Ss2mqWpLFNz0+o2c1O8J6+A==} engines: {node: '>=0.4.0'} fn.name@1.1.0: @@ -6744,6 +6745,10 @@ packages: resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} engines: {node: 14 || >=16.14} + lru-cache@11.1.0: + resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -7534,6 +7539,10 @@ packages: resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} engines: {node: '>= 0.8'} + on-headers@1.1.0: + resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} + engines: {node: '>= 0.8'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -8838,9 +8847,9 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - source-map@0.7.4: - resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} - engines: {node: '>= 8'} + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -9893,8 +9902,8 @@ packages: engines: {node: '>= 14'} hasBin: true - yaml@2.8.0: - resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} engines: {node: '>= 14.6'} hasBin: true @@ -10038,11 +10047,11 @@ snapshots: '@babel/generator': 7.28.0 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) - '@babel/helpers': 7.27.6 + '@babel/helpers': 7.28.2 '@babel/parser': 7.28.0 '@babel/template': 7.27.2 '@babel/traverse': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 convert-source-map: 2.0.0 debug: 4.4.1 gensync: 1.0.0-beta.2 @@ -10061,9 +10070,9 @@ snapshots: '@babel/generator@7.28.0': dependencies: '@babel/parser': 7.28.0 - '@babel/types': 7.28.1 - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 + '@babel/types': 7.28.2 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 jsesc: 3.1.0 '@babel/helper-annotate-as-pure@7.24.7': @@ -10072,7 +10081,7 @@ snapshots: '@babel/helper-annotate-as-pure@7.27.3': dependencies: - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 '@babel/helper-builder-binary-assignment-operator-visitor@7.24.7': dependencies: @@ -10093,7 +10102,7 @@ snapshots: dependencies: '@babel/compat-data': 7.28.0 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.24.3 + browserslist: 4.25.1 lru-cache: 5.1.1 semver: 6.3.1 @@ -10199,7 +10208,7 @@ snapshots: '@babel/helper-member-expression-to-functions@7.27.1': dependencies: '@babel/traverse': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color @@ -10213,7 +10222,7 @@ snapshots: '@babel/helper-module-imports@7.27.1': dependencies: '@babel/traverse': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color @@ -10252,7 +10261,7 @@ snapshots: '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 '@babel/helper-plugin-utils@7.24.7': {} @@ -10320,7 +10329,7 @@ snapshots: '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: '@babel/traverse': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color @@ -10357,7 +10366,7 @@ snapshots: dependencies: '@babel/template': 7.27.2 '@babel/traverse': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color @@ -10366,10 +10375,10 @@ snapshots: '@babel/template': 7.24.7 '@babel/types': 7.24.7 - '@babel/helpers@7.27.6': + '@babel/helpers@7.28.2': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 '@babel/highlight@7.24.7': dependencies: @@ -10388,7 +10397,7 @@ snapshots: '@babel/parser@7.28.0': dependencies: - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.7(@babel/core@7.24.7)': dependencies: @@ -11080,7 +11089,7 @@ snapshots: '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.24.7) - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color @@ -11374,7 +11383,7 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 - '@babel/runtime@7.27.6': {} + '@babel/runtime@7.28.2': {} '@babel/template@7.24.7': dependencies: @@ -11386,7 +11395,7 @@ snapshots: dependencies: '@babel/code-frame': 7.27.1 '@babel/parser': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 '@babel/traverse@7.24.7': dependencies: @@ -11410,7 +11419,7 @@ snapshots: '@babel/helper-globals': 7.28.0 '@babel/parser': 7.28.0 '@babel/template': 7.27.2 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -11426,7 +11435,7 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 - '@babel/types@7.28.1': + '@babel/types@7.28.2': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 @@ -12091,10 +12100,10 @@ snapshots: '@types/yargs': 17.0.33 chalk: 4.1.2 - '@jridgewell/gen-mapping@0.3.12': + '@jridgewell/gen-mapping@0.3.13': dependencies: - '@jridgewell/sourcemap-codec': 1.5.4 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.30 '@jridgewell/gen-mapping@0.3.5': dependencies: @@ -12106,24 +12115,24 @@ snapshots: '@jridgewell/set-array@1.2.1': {} - '@jridgewell/source-map@0.3.10': + '@jridgewell/source-map@0.3.11': dependencies: - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 '@jridgewell/sourcemap-codec@1.4.15': {} - '@jridgewell/sourcemap-codec@1.5.4': {} + '@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping@0.3.29': + '@jridgewell/trace-mapping@0.3.30': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping@0.3.9': dependencies: @@ -12871,7 +12880,7 @@ snapshots: semver: 7.7.1 strip-ansi: 5.2.0 wcwidth: 1.0.1 - yaml: 2.8.0 + yaml: 2.8.1 transitivePeerDependencies: - encoding @@ -12916,7 +12925,7 @@ snapshots: dependencies: '@react-native-community/cli-debugger-ui': 13.6.8 '@react-native-community/cli-tools': 13.6.8(encoding@0.1.13) - compression: 1.8.0 + compression: 1.8.1 connect: 3.7.0 errorhandler: 1.5.1 nocache: 3.0.4 @@ -13392,7 +13401,7 @@ snapshots: '@react-three/fiber@8.16.8(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)(three@0.165.0)': dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.2 '@types/react-reconciler': 0.26.7 '@types/webxr': 0.5.22 base64-js: 1.5.1 @@ -13547,7 +13556,7 @@ snapshots: '@rnx-kit/chromium-edge-launcher@1.0.0': dependencies: - '@types/node': 18.19.118 + '@types/node': 18.19.122 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 lighthouse-logger: 1.4.2 @@ -13810,7 +13819,7 @@ snapshots: '@swc/core@1.6.5(@swc/helpers@0.5.11)': dependencies: '@swc/counter': 0.1.3 - '@swc/types': 0.1.23 + '@swc/types': 0.1.24 optionalDependencies: '@swc/core-darwin-arm64': 1.6.5 '@swc/core-darwin-x64': 1.6.5 @@ -13835,7 +13844,7 @@ snapshots: '@swc/counter': 0.1.3 tslib: 2.8.1 - '@swc/types@0.1.23': + '@swc/types@0.1.24': dependencies: '@swc/counter': 0.1.3 @@ -14013,7 +14022,7 @@ snapshots: '@types/node@17.0.45': {} - '@types/node@18.19.118': + '@types/node@18.19.122': dependencies: undici-types: 5.26.5 @@ -14689,7 +14698,7 @@ snapshots: dependencies: '@babel/core': 7.24.7 '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.24.7) - core-js-compat: 3.44.0 + core-js-compat: 3.45.0 transitivePeerDependencies: - supports-color @@ -14804,17 +14813,10 @@ snapshots: node-releases: 2.0.14 update-browserslist-db: 1.0.16(browserslist@4.23.1) - browserslist@4.24.3: - dependencies: - caniuse-lite: 1.0.30001727 - electron-to-chromium: 1.5.182 - node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.24.3) - browserslist@4.25.1: dependencies: - caniuse-lite: 1.0.30001727 - electron-to-chromium: 1.5.182 + caniuse-lite: 1.0.30001734 + electron-to-chromium: 1.5.200 node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.1) @@ -14945,7 +14947,7 @@ snapshots: caniuse-lite@1.0.30001700: {} - caniuse-lite@1.0.30001727: {} + caniuse-lite@1.0.30001734: {} caseless@0.12.0: {} @@ -15195,13 +15197,13 @@ snapshots: dependencies: mime-db: 1.54.0 - compression@1.8.0: + compression@1.8.1: dependencies: bytes: 3.1.2 compressible: 2.0.18 debug: 2.6.9 negotiator: 0.6.4 - on-headers: 1.0.2 + on-headers: 1.1.0 safe-buffer: 5.2.1 vary: 1.1.2 transitivePeerDependencies: @@ -15332,7 +15334,7 @@ snapshots: dependencies: browserslist: 4.23.1 - core-js-compat@3.44.0: + core-js-compat@3.45.0: dependencies: browserslist: 4.25.1 @@ -15698,6 +15700,10 @@ snapshots: dlv@1.1.3: {} + dns-caching@0.2.4: + dependencies: + lru-cache: 11.1.0 + doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -15782,7 +15788,7 @@ snapshots: electron-to-chromium@1.4.810: {} - electron-to-chromium@1.5.182: {} + electron-to-chromium@1.5.200: {} email-templates@12.0.1(@babel/core@7.24.7)(encoding@0.1.13)(handlebars@4.7.8)(mustache@4.2.0)(pug@3.0.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.7): dependencies: @@ -16702,7 +16708,7 @@ snapshots: flow-enums-runtime@0.0.6: {} - flow-parser@0.275.0: {} + flow-parser@0.278.0: {} fn.name@1.1.0: {} @@ -17092,7 +17098,7 @@ snapshots: hermes-profile-transformer@0.0.6: dependencies: - source-map: 0.7.4 + source-map: 0.7.6 highlight.js@10.7.3: {} @@ -17685,7 +17691,7 @@ snapshots: '@babel/register': 7.27.1(@babel/core@7.28.0) babel-core: 7.0.0-bridge.0(@babel/core@7.28.0) chalk: 4.1.2 - flow-parser: 0.275.0 + flow-parser: 0.278.0 graceful-fs: 4.2.11 micromatch: 4.0.8 neo-async: 2.6.2 @@ -18036,6 +18042,8 @@ snapshots: lru-cache@10.2.2: {} + lru-cache@11.1.0: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -18307,13 +18315,13 @@ snapshots: metro-runtime@0.80.12: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.2 flow-enums-runtime: 0.0.6 metro-source-map@0.80.12: dependencies: '@babel/traverse': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 flow-enums-runtime: 0.0.6 invariant: 2.2.4 metro-symbolicate: 0.80.12 @@ -18352,7 +18360,7 @@ snapshots: '@babel/core': 7.28.0 '@babel/generator': 7.28.0 '@babel/parser': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 flow-enums-runtime: 0.0.6 metro: 0.80.12 metro-babel-transformer: 0.80.12 @@ -18375,7 +18383,7 @@ snapshots: '@babel/parser': 7.28.0 '@babel/template': 7.27.2 '@babel/traverse': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 accepts: 1.3.8 chalk: 4.1.2 ci-info: 2.0.0 @@ -19002,6 +19010,8 @@ snapshots: on-headers@1.0.2: {} + on-headers@1.1.0: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -20534,7 +20544,7 @@ snapshots: source-map@0.6.1: {} - source-map@0.7.4: {} + source-map@0.7.6: {} space-separated-tokens@2.0.2: {} @@ -20871,7 +20881,7 @@ snapshots: terser@5.43.1: dependencies: - '@jridgewell/source-map': 0.3.10 + '@jridgewell/source-map': 0.3.11 acorn: 8.15.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -21298,12 +21308,6 @@ snapshots: escalade: 3.1.2 picocolors: 1.0.1 - update-browserslist-db@1.1.3(browserslist@4.24.3): - dependencies: - browserslist: 4.24.3 - escalade: 3.2.0 - picocolors: 1.1.1 - update-browserslist-db@1.1.3(browserslist@4.25.1): dependencies: browserslist: 4.25.1 @@ -21594,7 +21598,7 @@ snapshots: yaml@2.4.5: {} - yaml@2.8.0: {} + yaml@2.8.1: {} yamljs@0.3.0: dependencies: diff --git a/server/index.ts b/server/index.ts index e4d46b8a1..24b7f2250 100644 --- a/server/index.ts +++ b/server/index.ts @@ -25,6 +25,7 @@ import imageproxy from '@server/routes/imageproxy'; import { appDataPermissions } from '@server/utils/appDataVolume'; import { getAppVersion } from '@server/utils/appVersion'; import createCustomProxyAgent from '@server/utils/customProxyAgent'; +import { initializeDnsCache } from '@server/utils/dnsCache'; import restartFlag from '@server/utils/restartFlag'; import { getClientIp } from '@supercharge/request-ip'; import axios from 'axios'; @@ -80,6 +81,14 @@ app axios.defaults.httpsAgent = new https.Agent({ family: 4 }); } + // Add DNS caching + if (settings.network.dnsCache) { + initializeDnsCache({ + forceMinTtl: settings.network.dnsCache.forceMinTtl, + forceMaxTtl: settings.network.dnsCache.forceMaxTtl, + }); + } + // Register HTTP proxy if (settings.network.proxy.enabled) { await createCustomProxyAgent(settings.network.proxy); diff --git a/server/interfaces/api/settingsInterfaces.ts b/server/interfaces/api/settingsInterfaces.ts index 9393370dc..3fee84be8 100644 --- a/server/interfaces/api/settingsInterfaces.ts +++ b/server/interfaces/api/settingsInterfaces.ts @@ -61,9 +61,39 @@ export interface CacheItem { }; } +export interface DNSAddresses { + ipv4: number; + ipv6: number; +} + +export interface DNSRecord { + addresses: DNSAddresses; + activeAddress: string; + family: number; + age: number; + ttl: number; + networkErrors: number; + hits: number; + misses: number; +} + +export interface DNSStats { + size: number; + maxSize: number; + hits: number; + misses: number; + failures: number; + ipv4Fallbacks: number; + hitRate: number; +} + export interface CacheResponse { apiCaches: CacheItem[]; imageCache: Record<'tmdb' | 'avatar', { size: number; imageCount: number }>; + dnsCache: { + entries: Record; + stats: DNSStats; + }; } export interface StatusResponse { diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index 115f224f8..52272c74d 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -100,17 +100,6 @@ interface Quota { quotaDays?: number; } -export interface ProxySettings { - enabled: boolean; - hostname: string; - port: number; - useSsl: boolean; - user: string; - password: string; - bypassFilter: string; - bypassLocalAddresses: boolean; -} - export interface MainSettings { apiKey: string; applicationTitle: string; @@ -138,11 +127,29 @@ export interface MainSettings { youtubeUrl: string; } +export interface ProxySettings { + enabled: boolean; + hostname: string; + port: number; + useSsl: boolean; + user: string; + password: string; + bypassFilter: string; + bypassLocalAddresses: boolean; +} + +export interface DnsCacheSettings { + enabled: boolean; + forceMinTtl?: number; + forceMaxTtl?: number; +} + export interface NetworkSettings { csrfProtection: boolean; forceIpv4First: boolean; trustProxy: boolean; proxy: ProxySettings; + dnsCache: DnsCacheSettings; } interface PublicSettings { @@ -542,6 +549,11 @@ class Settings { bypassFilter: '', bypassLocalAddresses: true, }, + dnsCache: { + enabled: false, + forceMinTtl: 0, + forceMaxTtl: -1, + }, }, }; if (initialSettings) { diff --git a/server/routes/settings/index.ts b/server/routes/settings/index.ts index d74d328f3..db31b937e 100644 --- a/server/routes/settings/index.ts +++ b/server/routes/settings/index.ts @@ -28,6 +28,7 @@ import discoverSettingRoutes from '@server/routes/settings/discover'; import { ApiError } from '@server/types/error'; import { appDataPath } from '@server/utils/appDataVolume'; import { getAppVersion } from '@server/utils/appVersion'; +import dnsCache from '@server/utils/dnsCache'; import { getHostname } from '@server/utils/getHostname'; import { Router } from 'express'; import rateLimit from 'express-rate-limit'; @@ -755,12 +756,19 @@ settingsRoutes.get('/cache', async (_req, res) => { const tmdbImageCache = await ImageProxy.getImageStats('tmdb'); const avatarImageCache = await ImageProxy.getImageStats('avatar'); + const stats = dnsCache?.getStats(); + const entries = dnsCache?.getCacheEntries(); + return res.status(200).json({ apiCaches, imageCache: { tmdb: tmdbImageCache, avatar: avatarImageCache, }, + dnsCache: { + stats, + entries, + }, }); }); @@ -778,6 +786,20 @@ settingsRoutes.post<{ cacheId: AvailableCacheIds }>( } ); +settingsRoutes.post<{ dnsEntry: string }>( + '/cache/dns/:dnsEntry/flush', + (req, res, next) => { + const dnsEntry = req.params.dnsEntry; + + if (dnsCache) { + dnsCache.clear(dnsEntry); + return res.status(204).send(); + } + + next({ status: 404, message: 'Cache not found.' }); + } +); + settingsRoutes.post( '/initialize', isAuthenticated(Permission.ADMIN), diff --git a/server/utils/dnsCache.ts b/server/utils/dnsCache.ts new file mode 100644 index 000000000..253b9291a --- /dev/null +++ b/server/utils/dnsCache.ts @@ -0,0 +1,28 @@ +import logger from '@server/logger'; +import { DnsCacheManager } from 'dns-caching'; + +let dnsCache: DnsCacheManager | undefined; + +export function initializeDnsCache({ + forceMinTtl, + forceMaxTtl, +}: { + forceMinTtl?: number; + forceMaxTtl?: number; +}) { + if (dnsCache) { + logger.warn('DNS Cache is already initialized', { label: 'DNS Cache' }); + return; + } + + logger.info('Initializing DNS Cache', { label: 'DNS Cache' }); + + dnsCache = new DnsCacheManager({ + logger, + forceMinTtl: typeof forceMinTtl === 'number' ? forceMinTtl * 1000 : 0, + forceMaxTtl: typeof forceMaxTtl === 'number' ? forceMaxTtl * 1000 : -1, + }); + dnsCache.initialize(); +} + +export default dnsCache; diff --git a/src/components/Settings/SettingsJobsCache/index.tsx b/src/components/Settings/SettingsJobsCache/index.tsx index 0f951293a..5a6ebefea 100644 --- a/src/components/Settings/SettingsJobsCache/index.tsx +++ b/src/components/Settings/SettingsJobsCache/index.tsx @@ -22,6 +22,7 @@ import type { import type { JobId } from '@server/lib/settings'; import axios from 'axios'; import cronstrue from 'cronstrue/i18n'; +import { formatDuration, intervalToDuration } from 'date-fns'; import { Fragment, useReducer, useState } from 'react'; import type { MessageDescriptor } from 'react-intl'; import { FormattedRelativeTime, useIntl } from 'react-intl'; @@ -55,6 +56,26 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages( cacheksize: 'Key Size', cachevsize: 'Value Size', flushcache: 'Flush Cache', + dnsCache: 'DNS Cache', + dnsCacheDescription: + 'Jellyseerr caches DNS lookups to optimize performance and avoid making unnecessary API calls.', + dnscacheflushed: '{hostname} dns cache flushed.', + dnscachename: 'Hostname', + dnscacheactiveaddress: 'Active Address', + dnscachehits: 'Hits', + dnscachemisses: 'Misses', + dnscacheage: 'Age', + dnscachenetworkerrors: 'Network Errors', + flushdnscache: 'Flush DNS Cache', + dnsCacheGlobalStats: 'Global DNS Cache Stats', + dnsCacheGlobalStatsDescription: + 'These stats are aggregated across all DNS cache entries.', + size: 'Size', + hits: 'Hits', + misses: 'Misses', + failures: 'Failures', + ipv4Fallbacks: 'IPv4 Fallbacks', + hitRate: 'Hit Rate', unknownJob: 'Unknown Job', 'plex-recently-added-scan': 'Plex Recently Added Scan', 'plex-full-scan': 'Plex Full Library Scan', @@ -242,6 +263,18 @@ const SettingsJobs = () => { cacheRevalidate(); }; + const flushDnsCache = async (hostname: string) => { + await axios.post(`/api/v1/settings/cache/dns/${hostname}/flush`); + addToast( + intl.formatMessage(messages.dnscacheflushed, { hostname: hostname }), + { + appearance: 'success', + autoDismiss: true, + } + ); + cacheRevalidate(); + }; + const scheduleJob = async () => { const jobScheduleCron = ['0', '0', '*', '*', '*', '*']; @@ -285,6 +318,18 @@ const SettingsJobs = () => { } }; + const formatAge = (milliseconds: number): string => { + const duration = intervalToDuration({ + start: 0, + end: milliseconds, + }); + + return formatDuration(duration, { + format: ['minutes', 'seconds'], + zero: false, + }); + }; + return ( <> { +
+

{intl.formatMessage(messages.dnsCache)}

+

+ {intl.formatMessage(messages.dnsCacheDescription)} +

+
+
+ + + + {intl.formatMessage(messages.dnscachename)} + + {intl.formatMessage(messages.dnscacheactiveaddress)} + + {intl.formatMessage(messages.dnscachehits)} + {intl.formatMessage(messages.dnscachemisses)} + {intl.formatMessage(messages.dnscacheage)} + + {intl.formatMessage(messages.dnscachenetworkerrors)} + + + + + + {Object.entries(cacheData?.dnsCache.entries || {}).map( + ([hostname, data]) => ( + + {hostname} + {data.activeAddress} + {intl.formatNumber(data.hits)} + {intl.formatNumber(data.misses)} + {formatAge(data.age)} + {intl.formatNumber(data.networkErrors)} + + + + + ) + )} + +
+
+
+

+ {intl.formatMessage(messages.dnsCacheGlobalStats)} +

+

+ {intl.formatMessage(messages.dnsCacheGlobalStatsDescription)} +

+
+
+ + + + {Object.entries(cacheData?.dnsCache.stats || {}) + .filter(([statName]) => statName !== 'maxSize') + .map(([statName]) => ( + + {messages[statName] + ? intl.formatMessage(messages[statName]) + : statName} + + ))} + + + + + {Object.entries(cacheData?.dnsCache.stats || {}) + .filter(([statName]) => statName !== 'maxSize') + .map(([statName, statValue]) => ( + + {statName === 'hitRate' + ? intl.formatNumber(statValue, { + style: 'percent', + maximumFractionDigits: 2, + }) + : intl.formatNumber(statValue)} + + ))} + + +
+

{intl.formatMessage(messages.imagecache)}

diff --git a/src/components/Settings/SettingsNetwork/index.tsx b/src/components/Settings/SettingsNetwork/index.tsx index e02b7bfc6..f0ac1f13a 100644 --- a/src/components/Settings/SettingsNetwork/index.tsx +++ b/src/components/Settings/SettingsNetwork/index.tsx @@ -45,6 +45,13 @@ const messages = defineMessages('components.Settings.SettingsNetwork', { forceIpv4First: 'Force IPv4 Resolution First', forceIpv4FirstTip: 'Force Jellyseerr to resolve IPv4 addresses first instead of IPv6', + dnsCache: 'DNS Cache', + dnsCacheTip: + 'Enable caching of DNS lookups to optimize performance and avoid making unnecessary API calls', + dnsCacheHoverTip: + 'Do NOT enable this if you are experiencing issues with DNS lookups', + dnsCacheForceMinTtl: 'DNS Cache Minimum TTL', + dnsCacheForceMaxTtl: 'DNS Cache Maximum TTL', }); const SettingsNetwork = () => { @@ -90,6 +97,9 @@ const SettingsNetwork = () => { initialValues={{ csrfProtection: data?.csrfProtection, forceIpv4First: data?.forceIpv4First, + dnsCacheEnabled: data?.dnsCache.enabled, + dnsCacheForceMinTtl: data?.dnsCache.forceMinTtl, + dnsCacheForceMaxTtl: data?.dnsCache.forceMaxTtl, trustProxy: data?.trustProxy, proxyEnabled: data?.proxy?.enabled, proxyHostname: data?.proxy?.hostname, @@ -108,6 +118,11 @@ const SettingsNetwork = () => { csrfProtection: values.csrfProtection, forceIpv4First: values.forceIpv4First, trustProxy: values.trustProxy, + dnsCache: { + enabled: values.dnsCacheEnabled, + forceMinTtl: values.dnsCacheForceMinTtl, + forceMaxTtl: values.dnsCacheForceMaxTtl, + }, proxy: { enabled: values.proxyEnabled, hostname: values.proxyHostname, @@ -221,6 +236,90 @@ const SettingsNetwork = () => { />

+
+ +
+ + { + setFieldValue( + 'dnsCacheEnabled', + !values.dnsCacheEnabled + ); + }} + /> + +
+
+ {values.dnsCacheEnabled && ( + <> +
+
+ +
+
+ +
+ {errors.dnsCacheForceMinTtl && + touched.dnsCacheForceMinTtl && + typeof errors.dnsCacheForceMinTtl === 'string' && ( +
+ {errors.dnsCacheForceMinTtl} +
+ )} +
+
+
+ +
+
+ +
+ {errors.dnsCacheForceMaxTtl && + touched.dnsCacheForceMaxTtl && + typeof errors.dnsCacheForceMaxTtl === 'string' && ( +
+ {errors.dnsCacheForceMaxTtl} +
+ )} +
+
+
+ + )}