diff --git a/.gitignore b/.gitignore index f6d03df..cde4ac0 100644 --- a/.gitignore +++ b/.gitignore @@ -24,10 +24,3 @@ build/ Thumbs.db .last_rollback data/admin-events.log -data/*.json -data/backups/* -users.json -giveaways.json -promo.json -env.json -analytics*.json diff --git a/deploy.sh b/deploy.sh index c08825d..aa33a6a 100755 --- a/deploy.sh +++ b/deploy.sh @@ -189,17 +189,8 @@ NPM_CMD="npm install --omit=dev" NPM_OUT="" if NPM_OUT=$(${NPM_CMD} 2>&1); then say "Dependencies installed." - mkdir -p "$PROJECT_DIR/data" "$PROJECT_DIR/data/backups" "$PROJECT_DIR/logs" + mkdir -p "$PROJECT_DIR/data" "$PROJECT_DIR/logs" touch "$PROJECT_DIR/data/sshv-sessions.json" "$PROJECT_DIR/data/admin-events.log" - - if id -u "$APP_NAME" >/dev/null 2>&1; then - chown -R "$APP_NAME:$APP_NAME" "$PROJECT_DIR/data" "$PROJECT_DIR/logs" || true - [[ -f "$PROJECT_DIR/.env" ]] && chown "$APP_NAME:$APP_NAME" "$PROJECT_DIR/.env" || true - fi - - chmod 0750 "$PROJECT_DIR/data" "$PROJECT_DIR/data/backups" "$PROJECT_DIR/logs" || true - chmod 0640 "$PROJECT_DIR/data/sshv-sessions.json" "$PROJECT_DIR/data/admin-events.log" || true - [[ -f "$PROJECT_DIR/.env" ]] && chmod 0600 "$PROJECT_DIR/.env" || true else warn "npm install failed — restarting bot on existing node_modules" warn "npm output: $NPM_OUT" diff --git a/index.js b/index.js index 671b602..73251c4 100644 --- a/index.js +++ b/index.js @@ -926,60 +926,6 @@ async function sendCommandError(ctx, { command, reason, howToUse, example }) { } - -function resolveTlsCertPathIfAllowed(inputPath) { - const allowedDirs = [ - PROJECT_DIR, - path.join(PROJECT_DIR, 'certs'), - '/etc/ssl', - '/etc/letsencrypt', - ]; - for (const baseDir of allowedDirs) { - try { - return validateSafePath(inputPath, baseDir); - } catch (_) { - // try next allowed directory - } - } - throw new Error('TLS path is outside allowed directories'); -} - -function isHealthHttpsConfigured() { - const tlsKeyPath = process.env.HTTPS_KEY_PATH; - const tlsCertPath = process.env.HTTPS_CERT_PATH; - if (!tlsKeyPath || !tlsCertPath) return false; - try { - resolveTlsCertPathIfAllowed(tlsKeyPath); - resolveTlsCertPathIfAllowed(tlsCertPath); - return true; - } catch (_) { - return false; - } -} - -function requestHealthPayload() { - const port = Number(process.env.PORT || 3000); - const useHttps = isHealthHttpsConfigured(); - const client = useHttps ? require('https') : require('http'); - return new Promise((resolve, reject) => { - const req = client.request({ - hostname: '127.0.0.1', - port, - path: '/health', - method: 'GET', - timeout: 5000, - ...(useHttps ? { rejectUnauthorized: false } : {}), - }, (res) => { - let data = ''; - res.on('data', (chunk) => { data += chunk; }); - res.on('end', () => resolve({ data, statusCode: res.statusCode, protocol: useHttps ? 'https' : 'http' })); - }); - req.on('error', reject); - req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); }); - req.end(); - }); -} - function normalizeSshvBufferLines(bufferValue) { if (Array.isArray(bufferValue)) { return bufferValue.map((line) => escapeMarkdownFull(String(line))).slice(-SSHV_MAX_BUFFER_LINES); @@ -1256,25 +1202,17 @@ function commandNeedsConfirmation(commandText) { const text = String(commandText || ''); const patterns = [ /\b(rm|mv|dd|shred|mkfs|chown|chmod|sudo|shutdown|reboot|kill)\b/i, - /\b(curl|wget)\b[^\n]*\|[^\n]*\b(sh|bash|zsh)\b/i, - /\b(rm|dd|shred|mkfs)\b[^\n]*\|/i, - /(^|\s)>>?\s*\S+/i, - /(^|[^|])\|([^|]|$)/, - /[;`$(){}]/, + /\b(curl|wget)\b[^\n]*\|[^\n]*\bsh\b/i, + />{1,2}/, + /[;&`$(){}<>]/, + /\|/, ]; return patterns.some((re) => re.test(text)); } function commandBlocked(commandText) { const text = String(commandText || ''); - const blockedPatterns = [ - /(^|\s)&(\s|$)/, - /&&/, - /\|\|/, - /\bnohup\b/i, - /\bbg\b/i, - /\b(curl|wget)\b[^\n]*\|[^\n]*\b(sh|bash|zsh)\b/i, - ]; + const blockedPatterns = [/(^|\s)&(\s|$)/, /&&/, /\|\|/, /\bnohup\b/i, /\bbg\b/i]; return blockedPatterns.some((re) => re.test(text)); } @@ -3506,36 +3444,6 @@ bot.command('sshv', async (ctx) => { await renderSshvConsole(ctx, session, note); }); -bot.action('admin_cmd_announce_start', async (ctx) => { - if (!requireAdmin(ctx)) return; - const user = getUser(ctx); - clearPendingAction(user); - user.pendingAction = { type: 'await_announcement_text' }; - await ctx.answerCbQuery('Send announcement text'); - await replaceCallbackPanel(ctx, '📣 *Announcement Composer*\n\nSend the announcement text now. You can use normal text or HTML.', { - parse_mode: 'Markdown', - ...Markup.inlineKeyboard([[Markup.button.callback('❌ Cancel', 'open_admin_dashboard')]]), - }); -}); - -bot.action('admin_cmd_tips_dashboard', async (ctx) => { - if (!requireAdmin(ctx)) return; - await ctx.answerCbQuery(); - await sendTipsDashboard(ctx); -}); - -bot.action('admin_cmd_tiptest', async (ctx) => { - if (!requireAdmin(ctx)) return; - await ctx.answerCbQuery(); - const enabled = tipsStore.tips.filter((t) => t.enabled); - if (!enabled.length) { - await ctx.reply('No enabled tips to preview.'); - return; - } - const tip = enabled[Math.floor(Math.random() * enabled.length)]; - await ctx.reply(`Here is a random 4-hour tip preview:\n\n${tip.text}`, { parse_mode: 'Markdown' }); -}); - // ========================= // Admin Announce Command — /A /a /announce // State machine: @@ -3897,8 +3805,18 @@ bot.command('refreshuser', safeAdminHandler('refreshuser', { usage: '/refreshuse bot.command('health', async (ctx) => { if (!requireAdmin(ctx)) return; try { - const { data, protocol } = await requestHealthPayload(); - await ctx.reply(`🩺 *Health Endpoint (${protocol.toUpperCase()})*\n\n\`\`\`\n${JSON.stringify(JSON.parse(data), null, 2)}\n\`\`\``, { parse_mode: 'Markdown' }); + const { data } = await new Promise((resolve, reject) => { + const opts = { hostname: '127.0.0.1', port, path: '/health', method: 'GET', timeout: 5000, rejectUnauthorized: false }; + const req = require('https').request(opts, (res) => { + let body = ''; + res.on('data', (c) => { body += c; }); + res.on('end', () => resolve({ status: res.statusCode, data: body })); + }); + req.on('error', reject); + req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); }); + req.end(); + }); + await ctx.reply(`🩺 *Health Endpoint*\n\n\`\`\`\n${JSON.stringify(JSON.parse(data), null, 2)}\n\`\`\``, { parse_mode: 'Markdown' }); } catch (e) { await ctx.reply(`❌ Health check failed: ${e.message}`); } @@ -4621,8 +4539,18 @@ bot.action('pamenu_tools_health', async (ctx) => { await ctx.answerCbQuery('Running health check...'); if (!requireAdmin(ctx)) return; try { - const { data, protocol } = await requestHealthPayload(); - await ctx.reply(`🩺 Health Check (${protocol.toUpperCase()}):\n\`\`\`\n${data.slice(0, 800)}\n\`\`\``, { parse_mode: 'Markdown' }); + const httpsClient = require('https'); + const port = Number(process.env.PORT || 3000); + const result = await new Promise((resolve, reject) => { + const req = httpsClient.get({ hostname: '127.0.0.1', port, path: '/health', rejectUnauthorized: false }, (res) => { + let data = ''; + res.on('data', (chunk) => { data += chunk; }); + res.on('end', () => resolve(data)); + }); + req.on('error', reject); + req.setTimeout(5000, () => { req.destroy(); reject(new Error('timeout')); }); + }); + await ctx.reply(`🩺 Health Check:\n\`\`\`\n${result.slice(0, 800)}\n\`\`\``, { parse_mode: 'Markdown' }); } catch (e) { await ctx.reply(`❌ Health check failed: ${e.message}`); } @@ -5714,20 +5642,14 @@ bot.action('sshv_editor_save', async (ctx) => { await ctx.answerCbQuery('Send edited file content first.'); return; } - try { - fs.writeFileSync(session.editorMode.filePath, draft, 'utf8'); - adminLog('sshv_editor_save', { adminId: user.id, filePath: session.editorMode.filePath, size: draft.length }); - session.buffer = `Saved ${session.editorMode.filePath}`; - session.editorMode = null; - user.pendingAction = { type: 'await_sshv_command' }; - persistSshvSessions(); - await ctx.answerCbQuery('Saved'); - await renderSshvConsole(ctx, session); - } catch (e) { - adminLog('sshv_editor_save_failed', { error: e.message, filePath: session.editorMode.filePath, by: user.id }); - await ctx.answerCbQuery('Save failed'); - await ctx.reply(`Save failed: ${e.message}`); - } + fs.writeFileSync(session.editorMode.filePath, draft, 'utf8'); + adminLog('sshv_editor_save', { adminId: user.id, filePath: session.editorMode.filePath, size: draft.length }); + session.buffer = `Saved ${session.editorMode.filePath}`; + session.editorMode = null; + user.pendingAction = { type: 'await_sshv_command' }; + persistSshvSessions(); + await ctx.answerCbQuery('Saved'); + await renderSshvConsole(ctx, session); }); bot.action('sshv_editor_cancel', async (ctx) => { @@ -7317,8 +7239,8 @@ function startTipsScheduler() { logEvent('warn', 'Skipping helpful message: missing permission in target group', { target: tipsStore.targetGroup }); return; } - await bot.telegram.sendMessage(tipsStore.targetGroup, formatTipForHtml(tip.text), { - parse_mode: 'HTML', + await bot.telegram.sendMessage(tipsStore.targetGroup, tip.text, { + parse_mode: 'Markdown', disable_notification: true, }); tipsStore.lastSentTipId = tip.id; @@ -8944,8 +8866,8 @@ function startHealthServer() { let cert = null; if (tlsKeyPath && tlsCertPath) { try { - key = fs.readFileSync(resolveTlsCertPathIfAllowed(tlsKeyPath)); - cert = fs.readFileSync(resolveTlsCertPathIfAllowed(tlsCertPath)); + key = fs.readFileSync(validateSafePath(tlsKeyPath, PROJECT_DIR)); + cert = fs.readFileSync(validateSafePath(tlsCertPath, PROJECT_DIR)); } catch (e) { _startupWarnings.push(`⚠️ Startup Warning TLS cert/key read failed: ${e.message} @@ -9697,7 +9619,7 @@ async function startBot() { } // Cleanup expired /sshv sessions every minute to keep memory usage bounded. -const sshvGcTimer = setInterval(() => { +setInterval(() => { const now = Date.now(); for (const [adminId, session] of sshvSessions.entries()) { if (now - (session.lastActivity || session.createdAt || 0) > SSHV_SESSION_TTL_MS) { @@ -9705,7 +9627,6 @@ const sshvGcTimer = setInterval(() => { } } }, 60 * 1000); -if (typeof sshvGcTimer.unref === 'function') sshvGcTimer.unref(); // Fallback for unmatched callback_data: always acknowledge and provide recovery path. bot.action(/.*/, async (ctx) => { diff --git a/prod-run.sh b/prod-run.sh index 1a3c9b3..49ef908 100755 --- a/prod-run.sh +++ b/prod-run.sh @@ -199,12 +199,6 @@ mkdir -p "$PROJECT_DIR/data" mkdir -p "$PROJECT_DIR/data/backups" touch "$MAIN_LOG" "$ERROR_LOG" "$ADMIN_EVENTS_LOG" "$SSHV_SESSIONS_FILE" # create if missing; safe on existing files -if id -u "$SERVICE_USER" >/dev/null 2>&1; then - chown -R "$SERVICE_USER:$SERVICE_GROUP" "$LOG_DIR" "$PROJECT_DIR/data" -fi -chmod 0750 "$PROJECT_DIR/data" "$PROJECT_DIR/data/backups" "$LOG_DIR" -chmod 0640 "$MAIN_LOG" "$ERROR_LOG" "$ADMIN_EVENTS_LOG" "$SSHV_SESSIONS_FILE" - say "Ensured runtime directories and log files exist" # --------------------------------------------------------- diff --git a/runewager.service b/runewager.service index ab0cc19..031386b 100644 --- a/runewager.service +++ b/runewager.service @@ -48,7 +48,6 @@ ProtectSystem=strict ProtectHome=true PrivateTmp=true NoNewPrivileges=true -UMask=0077 # File descriptor limit (for concurrent connections) LimitNOFILE=65536