change: security-check, memory-check [untest]

- fix high risk security issues
- fix high memory usage
- change adb to connection pool single instance

Signed-off-by: pakintada@gmail.com <Pakin>
This commit is contained in:
pakintada@gmail.com 2026-06-22 10:55:47 +07:00
parent f0619c5a10
commit 4f464a8513
23 changed files with 413 additions and 73 deletions

View file

@ -11,6 +11,7 @@
import { auth } from '$lib/core/stores/auth';
import { connectToWebsocket } from '$lib/core/stores/websocketStore';
import * as adb from '$lib/core/adb/adb';
import { adbConnectionStatus } from '$lib/core/stores/adbConnectionStore';
import { addNotification } from '$lib/core/stores/noti';
import { page } from '$app/stores';
import {
@ -117,7 +118,7 @@
if (adbReconnectTriedForUid !== currentUser.uid && !adb.getAdbInstance()) {
adbReconnectTriedForUid = currentUser.uid;
// void tryAutoConnect();
void tryAutoConnect();
}
});
</script>

View file

@ -10,7 +10,7 @@
import ArrowRight from '@lucide/svelte/icons/arrow-right';
import { permission as currentPermissions } from '$lib/core/stores/permissions';
import { get } from 'svelte/store';
import { onMount } from 'svelte';
import { onMount, onDestroy } from 'svelte';
import { referenceFromPage} from '$lib/core/stores/recipeStore';
let recipeModBtn = $state<HTMLElement | null>(null);
@ -23,7 +23,11 @@
let perms = $state<string[]>([]);
setInterval(() => {
// Wait for the goto-dashboard button to be mounted, then start a pulse
// animation once. Use an interval so we can react to the $state element
// being set after the initial render; stop polling as soon as the
// animation has been started.
const _pulseInterval = setInterval(() => {
if (gotoDashboardBtn && !animationPulseGoto) {
animationPulseGoto = animate(gotoDashboardBtn, {
opacity: [0.8, 1], // Slight pulse in opacity
@ -37,6 +41,10 @@
}
}, 1000);
onDestroy(() => {
clearInterval(_pulseInterval);
});
// let perms = get(currentPermissions);
onMount(() => {

View file

@ -9,6 +9,7 @@
import Checkbox from '$lib/components/ui/checkbox/checkbox.svelte';
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
import * as adb from '$lib/core/adb/adb';
import { adbConnectionStatus } from '$lib/core/stores/adbConnectionStore';
import { addNotification } from '$lib/core/stores/noti';
import { referenceFromPage } from '$lib/core/stores/recipeStore';
import type { Material } from '$lib/models/material.model';

View file

@ -9,6 +9,7 @@
import Checkbox from '$lib/components/ui/checkbox/checkbox.svelte';
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
import * as adb from '$lib/core/adb/adb';
import { adbConnectionStatus } from '$lib/core/stores/adbConnectionStore';
import { addNotification } from '$lib/core/stores/noti';
import { referenceFromPage } from '$lib/core/stores/recipeStore';

View file

@ -7,6 +7,7 @@
import { onMount, onDestroy } from 'svelte';
import * as adb from '$lib/core/adb/adb';
import { adbConnectionStatus } from '$lib/core/stores/adbConnectionStore';
import { addNotification } from '$lib/core/stores/noti';
import { columns, type RecipeOverview } from '../../recipe/overview/columns';
import {
@ -72,7 +73,7 @@
// clear out event
GlobalEventBus.on('recipe-event', (d: any) => {
const unsubRecipeEvent = GlobalEventBus.on('recipe-event', (d: any) => {
logger.info('[recipe-ev] get event: ', d);
if (d?.type == 'load-recipe' && d?.status == 'end') {
addNotification('INFO:Get data, waiting for reloading ...');
@ -829,6 +830,7 @@
});
onDestroy(() => {
unsubRecipeEvent();
clearOnMenuSavedCallback();
});

View file

@ -10,6 +10,7 @@
import { goto } from '$app/navigation';
import * as adb from '$lib/core/adb/adb';
import { adbConnectionStatus } from '$lib/core/stores/adbConnectionStore';
import { addNotification } from '$lib/core/stores/noti';
import { referenceFromPage } from '$lib/core/stores/recipeStore';
import { env } from '$env/dynamic/public';
@ -295,21 +296,12 @@
async function connectAdb() {
try {
if (adb.getAdbInstance()) {
const connected = await adb.ensureAdbConnection();
if (connected) {
if (!isAdbWriterAvailable()) {
await adb.reconnectAndroidRecipeMenuServer();
}
await loadRecipeFromMachine();
return;
}
if (!('usb' in navigator)) {
throw new Error('WebUSB not supported');
}
await adb.connectRecipeMenuViaWebUSB();
if (adb.getAdbInstance()) {
await loadRecipeFromMachine();
}
} catch (e: any) {
addNotification(`ERROR:${e}`);

View file

@ -2,12 +2,16 @@ import { logger } from '$lib/core/utils/logger';
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { env } from '$env/dynamic/public';
import { verifyAuthToken } from '$lib/server/auth';
// Method 2: forward a machine-generated sync_1.file to the adv FTP server.
const ADV_API_BASE = env.PUBLIC_POST_IMAGE;
export const POST: RequestHandler = async ({ request }) => {
try {
// Verify authentication
await verifyAuthToken(request);
const formData = await request.formData();
const country = formData.get('country') as string;

View file

@ -2,12 +2,21 @@ import { logger } from '$lib/core/utils/logger';
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { env } from '$env/dynamic/public';
import { verifyAuthToken } from '$lib/server/auth';
// Adv videos are served by the same taobin_image service as menu images.
const ADV_API_BASE = env.PUBLIC_POST_IMAGE;
const ALLOWED_MIME_TYPES = [
'video/mp4', 'video/webm', 'video/ogg',
'image/jpeg', 'image/png', 'image/gif', 'image/webp'
];
export const POST: RequestHandler = async ({ request }) => {
try {
// Verify authentication
await verifyAuthToken(request);
const formData = await request.formData();
const country = formData.get('country') as string;
@ -22,6 +31,11 @@ export const POST: RequestHandler = async ({ request }) => {
throw error(400, 'Missing required fields');
}
// File type validation (size limit removed — upstream handles it)
if (!ALLOWED_MIME_TYPES.includes(file.type)) {
throw error(400, `File type "${file.type}" not allowed. Allowed: ${ALLOWED_MIME_TYPES.join(', ')}`);
}
const endpoint =
`${ADV_API_BASE}/adv/upload/${encodeURIComponent(country)}/${encodeURIComponent(uid)}/${encodeURIComponent(displayName)}/${encodeURIComponent(email)}` +
`?regenerate=${encodeURIComponent(regenerate)}`;

View file

@ -2,11 +2,17 @@ import { logger } from '$lib/core/utils/logger';
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { env } from '$env/dynamic/public';
import { verifyAuthToken } from '$lib/server/auth';
const IMAGE_API_BASE = env.PUBLIC_POST_IMAGE;
const ALLOWED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif'];
export const POST: RequestHandler = async ({ request }) => {
try {
// Verify authentication
await verifyAuthToken(request);
const formData = await request.formData();
const country = formData.get('country') as string;
@ -19,6 +25,11 @@ export const POST: RequestHandler = async ({ request }) => {
if (!folder || !uid || !displayName || !email || !file) {
throw error(400, 'Missing required fields');
}
// File type validation (size limit removed — upstream handles it)
if (!ALLOWED_MIME_TYPES.includes(file.type)) {
throw error(400, `File type "${file.type}" not allowed. Allowed: ${ALLOWED_MIME_TYPES.join(', ')}`);
}
// Build the upload endpoint

View file

@ -1,21 +1,50 @@
import { logger } from '$lib/core/utils/logger';
import { json } from '@sveltejs/kit';
import { error, json } from '@sveltejs/kit';
import type { RequestEvent } from '@sveltejs/kit';
import { verifyAuthToken } from '$lib/server/auth';
// In-memory store for streamed catalog data
// Format: { batchId: { chunks: [...], status: 'collecting'|'complete'|'error' } }
const streamCache = new Map<string, any>();
const MAX_CACHE_SIZE = 200; // max number of batches in cache
const MAX_CHUNKS_PER_BATCH = 500;
export async function POST({ request }) {
export async function POST({ request }: RequestEvent) {
try {
await verifyAuthToken(request);
const data = await request.json();
const { batch_id, msg, content, current_chunk, total_chunks } = data.payload;
const { batch_id, msg, content, current_chunk, total_chunks } = data.payload ?? {};
// Input validation
if (!batch_id || typeof batch_id !== 'string') {
throw error(400, 'Missing or invalid batch_id');
}
if (!msg || typeof msg !== 'string') {
throw error(400, 'Missing or invalid msg');
}
if (content !== undefined && content !== null && typeof content !== 'string') {
throw error(400, 'content must be a string');
}
// Enforce cache size limit — evict oldest batches when over capacity
if (!streamCache.has(batch_id) && streamCache.size >= MAX_CACHE_SIZE) {
const oldestKey = streamCache.keys().next().value;
if (oldestKey !== undefined) {
streamCache.delete(oldestKey);
logger.warn(`[API] Evicted oldest batch "${oldestKey}" — cache full`);
}
}
// Initialize or update batch
if (!streamCache.has(batch_id)) {
const cappedTotal = typeof total_chunks === 'number' && total_chunks > 0
? Math.min(total_chunks, MAX_CHUNKS_PER_BATCH)
: MAX_CHUNKS_PER_BATCH;
streamCache.set(batch_id, {
chunks: [],
status: 'collecting',
total_chunks,
total_chunks: cappedTotal,
createdAt: Date.now()
});
}
@ -26,6 +55,10 @@ export async function POST({ request }) {
if (msg === 'start') {
logger.info(`[API] Stream started for batch ${batch_id}`);
} else if (msg === 'chunk') {
if (batch.chunks.length >= MAX_CHUNKS_PER_BATCH) {
logger.warn(`[API] Max chunks reached for batch ${batch_id}, ignoring chunk`);
return json({ status: 'received', batch_id, warning: 'max_chunks_reached' });
}
batch.chunks.push(content);
logger.info(`[API] Received chunk ${current_chunk}/${total_chunks} for batch ${batch_id}`);
} else if (msg === 'end') {
@ -44,7 +77,10 @@ export async function POST({ request }) {
}
}
export function GET({ url }) {
export async function GET({ request, url }: RequestEvent) {
// Verify authentication
await verifyAuthToken(request);
const batchId = url.searchParams.get('batch_id');
// Clean up old cache entries (older than 5 minutes)