diff --git a/packages/addon/src/config.ts b/packages/addon/src/config.ts index 0e06e2b9..e79af1e6 100644 --- a/packages/addon/src/config.ts +++ b/packages/addon/src/config.ts @@ -1,6 +1,8 @@ import { AddonDetail, Config } from '@aiostreams/types'; import { addonDetails, + isValueEncrypted, + parseAndDecryptString, serviceDetails, Settings, unminifyConfig, @@ -98,20 +100,41 @@ export function validateConfig(config: Config): { ); } // check for apiKey if Settings.API_KEY is set - if (Settings.API_KEY && !config.apiKey) { - return createResponse( - false, - 'missingApiKey', - 'The AIOStreams API key is required' - ); - } else if (Settings.API_KEY && config.apiKey !== Settings.API_KEY) { - return createResponse( - false, - 'invalidApiKey', - 'Invalid AIOStreams API key. Please use the one defined in your environment variables' - ); + if (Settings.API_KEY) { + const { apiKey } = config; + if (!apiKey) { + return createResponse( + false, + 'missingApiKey', + 'The AIOStreams API key is required' + ); + } + let decryptedApiKey = apiKey; + if (isValueEncrypted(apiKey)) { + const decryptionResult = parseAndDecryptString(apiKey); + if (decryptionResult === null) { + return createResponse( + false, + 'decryptionFailed', + 'Failed to decrypt the AIOStreams API key' + ); + } else if (decryptionResult === '') { + return createResponse( + false, + 'emptyDecryption', + 'Decrypted API key is empty' + ); + } + decryptedApiKey = decryptionResult; + } + if (decryptedApiKey !== Settings.API_KEY) { + return createResponse( + false, + 'invalidApiKey', + 'Invalid AIOStreams API key. Please use the one defined in your environment variables' + ); + } } - // check for any duplicate addons where both the ID and options are the same const duplicateAddons = config.addons.filter( (addon, index) => config.addons.findIndex( @@ -182,9 +205,23 @@ export function validateConfig(config: Config): { option.id.toLowerCase().includes('url') && addon.options[option.id] ) { + const url = parseAndDecryptString(addon.options[option.id] ?? ''); + if (url === null) { + return createResponse( + false, + 'decryptionFailed', + `Failed to decrypt URL for ${option.label}` + ); + } else if (url === '') { + return createResponse( + false, + 'emptyDecryption', + `Decrypted URL for ${option.label} is empty` + ); + } if ( Settings.DISABLE_TORRENTIO && - addon.options[option.id]?.match(/torrentio\.strem\.fun/) !== null + url.match(/torrentio\.strem\.fun/) !== null ) { // if torrentio is disabled, don't allow the user to set URLs with torrentio.strem.fun return createResponse( @@ -194,14 +231,13 @@ export function validateConfig(config: Config): { ); } else if ( Settings.DISABLE_TORRENTIO && - addon.options[option.id]?.match(/stremthru\.elfhosted\.com/) !== - null + url.match(/stremthru\.elfhosted\.com/) !== null ) { // if torrentio is disabled, we need to inspect the stremthru URL to see if it's using torrentio try { - const url = new URL(addon.options[option.id] as string); + const parsedUrl = new URL(url); // get the component before manifest.json - const pathComponents = url.pathname.split('/'); + const pathComponents = parsedUrl.pathname.split('/'); if (pathComponents.includes('manifest.json')) { const index = pathComponents.indexOf('manifest.json'); const componentBeforeManifest = pathComponents[index - 1]; @@ -219,14 +255,9 @@ export function validateConfig(config: Config): { } catch (_) { // ignore } - } else if ( - addon.options[option.id]?.match( - /^E-[0-9a-fA-F]{32}-[0-9a-fA-F]+$/ - ) === null && - addon.options[option.id]?.match(/^E2-[^-]+-[^-]+$/) === null - ) { + } else { try { - new URL(addon.options[option.id] as string); + new URL(url); } catch (_) { return createResponse( false, diff --git a/packages/addon/src/server.ts b/packages/addon/src/server.ts index cfe989ce..b75c89d9 100644 --- a/packages/addon/src/server.ts +++ b/packages/addon/src/server.ts @@ -22,6 +22,7 @@ import { loadSecretKey, createLogger, getTimeTakenSincePoint, + isValueEncrypted, } from '@aiostreams/utils'; const logger = createLogger('server'); @@ -130,7 +131,7 @@ app.get('/:config/configure', (req, res) => { } try { let configJson = extractJsonConfig(config); - if (config.startsWith('E-') || config.startsWith('E2-')) { + if (isValueEncrypted(config)) { logger.info(`Encrypted config detected, encrypting credentials`); configJson = encryptInfoInConfig(configJson); } @@ -360,11 +361,10 @@ app.listen(Settings.PORT, () => { function extractJsonConfig(config: string): Config { if ( - config.startsWith('E-') || config.startsWith('eyJ') || config.startsWith('eyI') || - config.startsWith('E2-') || - config.startsWith('B-') + config.startsWith('B-') || + isValueEncrypted(config) ) { return extractEncryptedOrEncodedConfig(config, 'Config'); } @@ -459,6 +459,10 @@ function decryptEncryptedInfoFromConfig(config: Config): Config { decryptMediaFlowConfig(config.mediaFlowConfig); } + if (config.apiKey) { + config.apiKey = decryptValue(config.apiKey, 'aioStreams apiKey'); + } + if (config.addons) { config.addons.forEach((addon) => { if (addon.options) { @@ -512,6 +516,10 @@ function encryptInfoInConfig(config: Config): Config { encryptMediaFlowConfig(config.mediaFlowConfig); } + if (config.apiKey) { + config.apiKey = encryptValue(config.apiKey, 'aioStreams apiKey'); + } + if (config.addons) { config.addons.forEach((addon) => { if (addon.options) { @@ -597,13 +605,5 @@ function decryptValue(value: any, label: string): any { } } -function isValueEncrypted(value: string | undefined): boolean { - if (!value) return false; - const tests = - /^E2-[^-]+-[^-]+$/.test(value) || - /^E-[0-9a-fA-F]{32}-[0-9a-fA-F]+$/.test(value); - return tests; -} - const rootUrl = (req: Request) => `${req.protocol}://${req.hostname}${req.hostname === 'localhost' ? `:${Settings.PORT}` : ''}`; diff --git a/packages/frontend/src/app/configure/page.tsx b/packages/frontend/src/app/configure/page.tsx index 84305129..15e3d99d 100644 --- a/packages/frontend/src/app/configure/page.tsx +++ b/packages/frontend/src/app/configure/page.tsx @@ -28,7 +28,12 @@ import { allowedLanguages, validateConfig, } from '@aiostreams/config'; -import { addonDetails, serviceDetails, Settings } from '@aiostreams/utils'; +import { + addonDetails, + isValueEncrypted, + serviceDetails, + Settings, +} from '@aiostreams/utils'; import Slider from '@/components/Slider'; import CredentialInput from '@/components/CredentialInput'; @@ -478,11 +483,7 @@ export default function Configure() { useEffect(() => { async function decodeConfig(config: string) { let decodedConfig: Config; - if ( - config.startsWith('E-') || - config.startsWith('E2-') || - config.startsWith('B-') - ) { + if (isValueEncrypted(config) || config.startsWith('B-')) { throw new Error('Encrypted Config Not Supported'); } else { decodedConfig = JSON.parse(atob(decodeURIComponent(config))); @@ -573,6 +574,7 @@ export default function Configure() { setMediaFlowProxiedServices( decodedConfig.mediaFlowConfig?.proxiedServices || null ); + setApiKey(decodedConfig.apiKey || ''); } const path = window.location.pathname; diff --git a/packages/frontend/src/components/CredentialInput.tsx b/packages/frontend/src/components/CredentialInput.tsx index 86220713..53ed0487 100644 --- a/packages/frontend/src/components/CredentialInput.tsx +++ b/packages/frontend/src/components/CredentialInput.tsx @@ -1,18 +1,12 @@ import React from 'react'; import styles from './CredentialInput.module.css'; +import { isValueEncrypted } from '@aiostreams/utils'; interface CredentialInputProps { credential: string; setCredential: (credential: string) => void; inputProps?: React.InputHTMLAttributes; } -function isEncrypted(value: string): boolean { - if (!value) return false; - const tests = - /^E2-[^-]+-[^-]+$/.test(value) || - /^E-[0-9a-fA-F]{32}-[0-9a-fA-F]+$/.test(value); - return tests; -} const CredentialInput: React.FC = ({ credential, @@ -25,18 +19,20 @@ const CredentialInput: React.FC = ({ setCredential(e.target.value.trim())} className={styles.credentialInput} {...inputProps} - disabled={isEncrypted(credential) ? true : inputProps.disabled || false} + disabled={ + isValueEncrypted(credential) ? true : inputProps.disabled || false + } /> - {!isEncrypted(credential) && ( + {!isValueEncrypted(credential) && ( )} - {isEncrypted(credential) && ( + {isValueEncrypted(credential) && (