optimize serving with compressions and more, fix to work when compiled

This commit is contained in:
Egor 2026-01-21 00:56:56 -08:00
parent 49d36e0150
commit 27b6b83ce2
13 changed files with 144 additions and 60 deletions

View file

@ -8,3 +8,5 @@ export const SIGNAL_CLI_DATA_DIR = `${HOME}/.local/share/signal-cli`;
export const SIGNAL_CLI_DATA = `${HOME}/.local/share/signal-cli/data`;
export const SUP_DB = `${HOME}/.local/share/sup/store.db`;
export const PUBLIC_DIR = (await Bun.file('public/favicon.webp').exists()) ? 'public' : '/public';

View file

@ -1,8 +1,10 @@
import { API_KEY, BRIDGE_IMAP_PASSWORD, BRIDGE_IMAP_USERNAME, PORT } from '@/constants/config';
import { PUBLIC_DIR } from '@/constants/paths';
import { cleanupDaemon, initSignal } from '@/modules/signal';
import { adminRoutes } from '@/routes/admin';
import { ntfyRoutes } from '@/routes/ntfy';
import { unifiedPushRoutes } from '@/routes/unifiedpush';
import { maybeCompress } from '@/utils/compress';
import { getLanIP } from '@/utils/ip';
import { logError, logInfo, logVerbose, logWarn } from '@/utils/log';
@ -29,13 +31,20 @@ if (BRIDGE_IMAP_USERNAME && BRIDGE_IMAP_PASSWORD) {
const server = Bun.serve({
port: PORT,
idleTimeout: 60,
maxRequestBodySize: 1024 * 1024, // 1MB limit
error(error) {
logError('Unhandled server error:', error);
return new Response('Internal Server Error', { status: 500 });
},
routes: {
'/favicon.png': {
GET: () => new Response(Bun.file('public/favicon.png')),
'/favicon.webp': {
GET: () => new Response(Bun.file(`${PUBLIC_DIR}/favicon.webp`)),
},
'/htmx.js': {
GET: () => new Response(Bun.file('public/htmx.min.js')),
GET: async (req) =>
maybeCompress(req, await Bun.file(`${PUBLIC_DIR}/htmx.min.js`).text(), 'text/javascript'),
},
...adminRoutes,

View file

@ -6,8 +6,8 @@ import {
PROTON_BRIDGE_PORT,
SUP_TOPIC,
} from '@/constants/config';
import { createGroup, hasValidAccount, sendGroupMessage } from '@/modules/signal';
import { getGroupId, register } from '@/modules/store';
import { hasValidAccount, sendGroupMessage } from '@/modules/signal';
import { getOrCreateGroup } from '@/modules/store';
import { logError, logInfo, logSuccess, logVerbose, logWarn } from '@/utils/log';
let imapConnected = false;
@ -41,26 +41,21 @@ export async function startProtonMonitor() {
keepalive: true,
});
async function sendNotification(title: string, message: string) {
async function sendNotification(from: string, subject: string) {
if (!(await hasValidAccount())) {
logVerbose('Skipping notification (Signal not linked)');
return;
}
try {
const topicKey = `proton-${SUP_TOPIC}`;
const groupId = getGroupId(topicKey) ?? (await createGroup(SUP_TOPIC));
const groupId = await getOrCreateGroup(`proton-${SUP_TOPIC}`, SUP_TOPIC);
if (!getGroupId(topicKey)) {
register(topicKey, groupId, SUP_TOPIC);
}
await sendGroupMessage(groupId, message, {
await sendGroupMessage(groupId, subject, {
androidPackage: 'ch.protonmail.android',
title,
title: from,
});
logVerbose(`Notification sent: ${title}`);
logVerbose(`Email from ${from}: ${subject}`);
} catch (error) {
logError('Failed to send notification:', error);
}
@ -93,9 +88,9 @@ export async function startProtonMonitor() {
const subject = header.subject?.[0] || 'No subject';
const nameMatch = rawFrom.match(/^"?([^"<]+)"?\s*<?/);
const from = nameMatch ? nameMatch[1]?.trim() : rawFrom;
const from = nameMatch?.[1]?.trim() || rawFrom;
sendNotification(`From: ${from}`, subject);
sendNotification(from, subject);
});
});
});

View file

@ -1,5 +1,6 @@
import { Database } from 'bun:sqlite';
import { SUP_DB } from '@/constants/paths';
import { createGroup } from './signal';
interface EndpointMapping {
endpoint: string;
@ -45,3 +46,12 @@ export const getAllMappings = () =>
export const remove = (endpoint: string) => {
db.run('DELETE FROM mappings WHERE endpoint = ?', [endpoint]);
};
export const getOrCreateGroup = async (key: string, name: string) => {
const existingGroupId = getGroupId(key);
if (existingGroupId) return existingGroupId;
const groupId = await createGroup(name);
register(key, groupId, name);
return groupId;
};

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<title>SUP Admin</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="/favicon.png">
<link rel="icon" type="image/webp" href="/favicon.webp">
<link rel="stylesheet" href="/admin.css">
<script src="/htmx.js"></script>
</head>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

BIN
server/public/favicon.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 B

View file

@ -1,5 +1,5 @@
import { DEVICE_NAME } from '@/constants/config';
import { SIGNAL_CLI_DATA } from '@/constants/paths';
import { PUBLIC_DIR, SIGNAL_CLI_DATA } from '@/constants/paths';
import { isImapConnected } from '@/modules/protonmail';
import {
checkSignalCli,
@ -13,6 +13,7 @@ import {
} from '@/modules/signal';
import { getAllMappings, remove } from '@/modules/store';
import { withAuth } from '@/utils/auth';
import { maybeCompress } from '@/utils/compress';
export const handleHealthFragment = async () => {
const signalOk = await checkSignalCli();
@ -174,11 +175,15 @@ export const handleDeleteEndpoint = async (req: Request) => {
export const adminRoutes = {
'/': {
GET: () => new Response(Bun.file('public/admin.html')),
GET: withAuth(async (req: Request) =>
maybeCompress(req, await Bun.file(`${PUBLIC_DIR}/admin.html`).text()),
),
},
'/admin.css': {
GET: () => new Response(Bun.file('public/admin.css')),
GET: withAuth(async (req: Request) =>
maybeCompress(req, await Bun.file(`${PUBLIC_DIR}/admin.css`).text(), 'text/css'),
),
},
'/health/fragment': {

View file

@ -1,5 +1,5 @@
import { createGroup, sendGroupMessage } from '@/modules/signal';
import { getGroupId, register } from '@/modules/store';
import { sendGroupMessage } from '@/modules/signal';
import { getOrCreateGroup } from '@/modules/store';
import { withAuth } from '@/utils/auth';
import { logError, logVerbose } from '@/utils/log';
@ -18,25 +18,24 @@ const handleNtfyPublish = async (req: Request) => {
}
const contentType = req.headers.get('content-type') || '';
let title = req.headers.get('X-Title') || req.headers.get('Title') || req.headers.get('t');
let androidPackage =
req.headers.get('X-Package') || req.headers.get('Package') || req.headers.get('p');
let title: string | undefined =
req.headers.get('X-Title') || req.headers.get('Title') || req.headers.get('t') || undefined;
let androidPackage: string | undefined =
req.headers.get('X-Package') ||
req.headers.get('Package') ||
req.headers.get('p') ||
undefined;
if (contentType.includes('application/x-www-form-urlencoded')) {
const params = new URLSearchParams(message);
message = params.get('message') || message;
title = title || params.get('title') || params.get('t');
androidPackage = androidPackage || params.get('package') || params.get('p');
title = title || params.get('title') || params.get('t') || undefined;
androidPackage = androidPackage || params.get('package') || params.get('p') || undefined;
}
const topicKey = `ntfy-${topic}`;
let groupId = getGroupId(topicKey);
if (title === topic) title = undefined;
if (!groupId) {
groupId = await createGroup(topic);
register(topicKey, groupId, topic);
logVerbose(`Created new group for ntfy topic: ${topic}`);
}
const groupId = await getOrCreateGroup(`ntfy-${topic}`, topic);
await sendGroupMessage(groupId, message, {
androidPackage: androidPackage || undefined,

View file

@ -1,5 +1,5 @@
import { createGroup, sendGroupMessage } from '@/modules/signal';
import { getGroupId, register, remove } from '@/modules/store';
import { sendGroupMessage } from '@/modules/signal';
import { getGroupId, getOrCreateGroup, remove } from '@/modules/store';
import { formatAsSignalMessage, parseUnifiedPushRequest } from '@/modules/unifiedpush';
const handleMatrixNotify = async (req: Request) => {
@ -24,11 +24,7 @@ const handleRegister = async (req: Request) => {
token?: string;
};
const groupId: string = getGroupId(endpointId) ?? (await createGroup(appName));
if (!getGroupId(endpointId)) {
register(endpointId, groupId, appName);
}
await getOrCreateGroup(endpointId, appName);
const proto = req.headers.get('x-forwarded-proto') || 'http';
const host = req.headers.get('host') || 'localhost:8080';

View file

@ -1,20 +1,16 @@
import { timingSafeEqual } from 'node:crypto';
import { ALLOW_INSECURE_HTTP, API_KEY } from '@/constants/config';
import { getClientIP, isLocalIP } from '@/utils/ip';
import { logWarn } from '@/utils/log';
// Rate limiing: track failed auth attempts per IP
// Rate limiting: track failed auth attempts per IP
const failedAttempts = new Map<string, { count: number; resetAt: number }>();
const MAX_FAILED_ATTEMPTS = 5;
const LOCKOUT_DURATION = 15 * 60 * 1000; // 15 minutes
// Cloudflare Tunnel sends real IP in CF-Connecting-IP
const getClientIP = (req: Request) =>
req.headers.get('cf-connecting-ip') ||
req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
req.headers.get('x-real-ip') ||
'unknown';
const isRateLimited = (ip: string) => {
if (ALLOW_INSECURE_HTTP || isLocalIP(ip)) return false;
const now = Date.now();
const record = failedAttempts.get(ip);
@ -29,6 +25,8 @@ const isRateLimited = (ip: string) => {
};
const recordFailedAttempt = (ip: string) => {
if (ALLOW_INSECURE_HTTP || isLocalIP(ip)) return;
const now = Date.now();
const record = failedAttempts.get(ip);
@ -44,7 +42,7 @@ const recordFailedAttempt = (ip: string) => {
const checkAuth = (req: Request) => {
if (!API_KEY) {
return new Response('Unauthorized', { status: 401 });
return new Response('API_KEY environment variable not configured', { status: 401 });
}
const clientIP = getClientIP(req);
@ -53,15 +51,14 @@ const checkAuth = (req: Request) => {
return new Response('Too many failed attempts. Try again later.', { status: 429 });
}
const proto = req.headers.get('x-forwarded-proto') || 'http';
const host = req.headers.get('host') || '';
const isLocalhost = host.startsWith('localhost') || host.startsWith('127.0.0.1');
if (proto !== 'https' && !isLocalhost && !ALLOW_INSECURE_HTTP) {
return new Response('HTTPS required when API_KEY is configured', {
status: 426,
headers: { Upgrade: 'TLS/1.2, HTTP/1.1' },
});
if (!ALLOW_INSECURE_HTTP) {
const proto = req.headers.get('x-forwarded-proto') || 'http';
if (proto !== 'https') {
return new Response('HTTPS required when API_KEY is configured', {
status: 426,
headers: { Upgrade: 'TLS/1.2, HTTP/1.1' },
});
}
}
const authHeader = req.headers.get('authorization');

38
server/utils/compress.ts Normal file
View file

@ -0,0 +1,38 @@
export function maybeCompress(
request: Request,
body: string,
contentType = 'text/html; charset=utf-8',
): Response {
const acceptEncoding = request.headers.get('accept-encoding') || '';
const bodyBytes = new TextEncoder().encode(body);
if (bodyBytes.length < 1024) {
return new Response(body, {
headers: { 'Content-Type': contentType },
});
}
if (acceptEncoding.includes('gzip')) {
const compressed = Bun.gzipSync(bodyBytes);
return new Response(compressed, {
headers: {
'Content-Type': contentType,
'Content-Encoding': 'gzip',
},
});
}
if (acceptEncoding.includes('deflate')) {
const compressed = Bun.deflateSync(bodyBytes);
return new Response(compressed, {
headers: {
'Content-Type': contentType,
'Content-Encoding': 'deflate',
},
});
}
return new Response(body, {
headers: { 'Content-Type': contentType },
});
}

View file

@ -16,3 +16,36 @@ export const getLanIP = () => {
return null;
};
export const getClientIP = (req: Request) => {
// Cloudflare Tunnel
const cfIP = req.headers.get('cf-connecting-ip');
if (cfIP) return cfIP;
// Standard reverse proxy headers
const forwardedFor = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim();
if (forwardedFor) return forwardedFor;
const realIP = req.headers.get('x-real-ip');
if (realIP) return realIP;
const remoteAddr = req.headers.get('remote-addr');
if (remoteAddr) return remoteAddr;
return 'unknown';
};
export const isLocalIP = (ip: string) => {
if (ip === '::1' || ip === 'localhost') return true;
const octets = ip.split('.').map(Number);
if (octets.length !== 4 || octets.some((n) => Number.isNaN(n))) return false;
const [a, b] = octets;
return (
a === 127 ||
a === 10 ||
(a === 192 && b === 168) ||
(a === 172 && b !== undefined && b >= 16 && b <= 31)
);
};