feat: use common function to check for encryption and load api keys from existing configs

This commit is contained in:
Viren070
2025-02-25 20:41:22 +00:00
parent 1af5f745c9
commit 459e6f67a0
5 changed files with 92 additions and 55 deletions
+56 -25
View File
@@ -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,
+12 -12
View File
@@ -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}` : ''}`;
+8 -6
View File
@@ -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;
@@ -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<HTMLInputElement>;
}
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<CredentialInputProps> = ({
credential,
@@ -25,18 +19,20 @@ const CredentialInput: React.FC<CredentialInputProps> = ({
<input
type={showPassword ? 'text' : 'password'}
value={
isEncrypted(credential) ? '••••••••••••••••••••••••' : credential
isValueEncrypted(credential) ? '••••••••••••••••••••••••' : credential
}
onChange={(e) => 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) && (
<button
className={styles.showHideButton}
onClick={() => {
if (!isEncrypted(credential)) {
if (!isValueEncrypted(credential)) {
setShowPassword(!showPassword);
}
}}
@@ -123,7 +119,7 @@ const CredentialInput: React.FC<CredentialInputProps> = ({
</button>
)}
{isEncrypted(credential) && (
{isValueEncrypted(credential) && (
<button
className={styles.resetCredentialButton}
onClick={() => setCredential('')}
+8
View File
@@ -130,3 +130,11 @@ export function getTextHash(text: string): string {
hash.update(text);
return hash.digest('hex');
}
export function isValueEncrypted(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;
}