mirror of
https://github.com/lone-cloud/prism
synced 2026-06-03 08:43:10 -07:00
more minor refacotring and cleanup
This commit is contained in:
parent
128aa9ffe5
commit
eeea750873
12 changed files with 113 additions and 136 deletions
|
|
@ -1,4 +1,3 @@
|
|||
import { networkInterfaces } from 'node:os';
|
||||
import { Hono } from 'hono';
|
||||
import { getConnInfo, serveStatic } from 'hono/bun';
|
||||
import { compress } from 'hono/compress';
|
||||
|
|
@ -15,12 +14,23 @@ import {
|
|||
} from '@/constants/config';
|
||||
import { PUBLIC_DIR } from '@/constants/paths';
|
||||
import { cleanupDaemon, initSignal } from '@/modules/signal';
|
||||
import admin from '@/routes/admin';
|
||||
import ntfy from '@/routes/ntfy';
|
||||
import unifiedpush from '@/routes/unified-push';
|
||||
import { isLocalIP } from '@/utils/auth';
|
||||
import { admin } from '@/routes/admin';
|
||||
import { ntfy } from '@/routes/ntfy';
|
||||
import { unifiedpush } from '@/routes/unified-push';
|
||||
import { getLanIP, isLocalIP } from '@/utils/auth';
|
||||
import { formatToCspString } from '@/utils/format';
|
||||
import { logError, logInfo, logVerbose, logWarn } from '@/utils/log';
|
||||
|
||||
const cspConfig = {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
imgSrc: ["'self'", 'data:'],
|
||||
formAction: ["'self'"],
|
||||
frameAncestors: ["'none'"],
|
||||
objectSrc: ["'none'"],
|
||||
};
|
||||
|
||||
initSignal();
|
||||
|
||||
if (!API_KEY) {
|
||||
|
|
@ -37,43 +47,8 @@ if (PROTON_IMAP_USERNAME && PROTON_IMAP_PASSWORD) {
|
|||
}
|
||||
}
|
||||
|
||||
const getLanIP = () => {
|
||||
const nets = networkInterfaces();
|
||||
|
||||
for (const name of Object.keys(nets)) {
|
||||
const interfaces = nets[name];
|
||||
|
||||
if (!interfaces) continue;
|
||||
|
||||
for (const iface of interfaces) {
|
||||
if (iface.family === 'IPv4' && !iface.internal) {
|
||||
return iface.address;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
const cspConfig = {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
imgSrc: ["'self'", 'data:'],
|
||||
formAction: ["'self'"],
|
||||
frameAncestors: ["'none'"],
|
||||
objectSrc: ["'none'"],
|
||||
};
|
||||
|
||||
const cspString = Object.entries(cspConfig)
|
||||
.map(([key, values]) => {
|
||||
const directive = key.replace(/([A-Z])/g, '-$1').toLowerCase();
|
||||
|
||||
return `${directive} ${values.join(' ')}`;
|
||||
})
|
||||
.join('; ');
|
||||
|
||||
app.use('*', (c, next) => {
|
||||
const proto = c.req.header('x-forwarded-proto') || 'http';
|
||||
const addr = getConnInfo(c).remote.address;
|
||||
|
|
@ -84,18 +59,15 @@ app.use('*', (c, next) => {
|
|||
})(c, next);
|
||||
}
|
||||
|
||||
c.header('Content-Security-Policy', cspString);
|
||||
c.header('Content-Security-Policy', formatToCspString(cspConfig));
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
app.use('*', (c, next) => {
|
||||
if (c.req.path === '/link/status-check') {
|
||||
return next();
|
||||
}
|
||||
app.use('*', timeout(5000));
|
||||
|
||||
return timeout(5000)(c, next);
|
||||
});
|
||||
app.use('*', compress());
|
||||
|
||||
app.use(
|
||||
'*',
|
||||
rateLimiter({
|
||||
|
|
|
|||
|
|
@ -24,6 +24,27 @@ const getReconnectDelay = () => {
|
|||
return delay;
|
||||
};
|
||||
|
||||
async function sendNotification(from: string, subject: string) {
|
||||
if (!(await hasValidAccount())) {
|
||||
logVerbose('Skipping notification (Signal not linked)');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const groupId = await getOrCreateGroup(`proton-${PROTON_SUP_TOPIC}`, PROTON_SUP_TOPIC);
|
||||
|
||||
await sendGroupMessage(groupId, subject, {
|
||||
androidPackage: 'ch.protonmail.android',
|
||||
title: from,
|
||||
});
|
||||
|
||||
logVerbose(`Email from ${from}: ${subject}`);
|
||||
} catch (error) {
|
||||
logError('Failed to send notification:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function startProtonMonitor() {
|
||||
if (!PROTON_IMAP_USERNAME || !PROTON_IMAP_PASSWORD) {
|
||||
logError('Missing required env vars: PROTON_IMAP_USERNAME and PROTON_IMAP_PASSWORD');
|
||||
|
|
@ -33,11 +54,6 @@ export async function startProtonMonitor() {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!(await hasValidAccount())) {
|
||||
logWarn('Signal account not linked. Proton Mail notifications will be skipped.');
|
||||
logWarn('Link your Signal account to enable email notifications.');
|
||||
}
|
||||
|
||||
logInfo(`Connecting to Proton Bridge at ${PROTON_BRIDGE_HOST}:${PROTON_BRIDGE_PORT}`);
|
||||
logInfo(`Monitoring mailbox: ${PROTON_IMAP_USERNAME}`);
|
||||
|
||||
|
|
@ -49,36 +65,16 @@ export async function startProtonMonitor() {
|
|||
keepalive: true,
|
||||
});
|
||||
|
||||
async function sendNotification(from: string, subject: string) {
|
||||
if (!(await hasValidAccount())) {
|
||||
logVerbose('Skipping notification (Signal not linked)');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const groupId = await getOrCreateGroup(`proton-${PROTON_SUP_TOPIC}`, PROTON_SUP_TOPIC);
|
||||
|
||||
await sendGroupMessage(groupId, subject, {
|
||||
androidPackage: 'ch.protonmail.android',
|
||||
title: from,
|
||||
});
|
||||
|
||||
logVerbose(`Email from ${from}: ${subject}`);
|
||||
} catch (error) {
|
||||
logError('Failed to send notification:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const openInbox = () =>
|
||||
imap.openBox('INBOX', false, (err, box) => {
|
||||
if (err) {
|
||||
logError('Failed to open inbox:', err);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!monitorStartTime) {
|
||||
monitorStartTime = Date.now();
|
||||
logVerbose(`Inbox opened with ${box.messages.total} existing messages`);
|
||||
} else {
|
||||
logVerbose('Inbox reopened (reconnection)');
|
||||
}
|
||||
|
|
@ -124,6 +120,7 @@ export async function startProtonMonitor() {
|
|||
const from = nameMatch?.[1]?.trim() || rawFrom;
|
||||
|
||||
reconnectAttempts = 0;
|
||||
|
||||
sendNotification(from, subject);
|
||||
});
|
||||
});
|
||||
|
|
@ -149,6 +146,7 @@ export async function startProtonMonitor() {
|
|||
|
||||
const delay = getReconnectDelay();
|
||||
logInfo(`Attempting to reconnect in ${delay / 1000}s (attempt ${reconnectAttempts})...`);
|
||||
|
||||
setTimeout(() => {
|
||||
if (!imapConnected) {
|
||||
logInfo('Reconnecting to Proton Bridge...');
|
||||
|
|
|
|||
|
|
@ -6,11 +6,24 @@ import {
|
|||
VERBOSE_LOGGING,
|
||||
} from '@/constants/config';
|
||||
import { SIGNAL_CLI, SIGNAL_CLI_DATA, SIGNAL_CLI_SOCKET } from '@/constants/paths';
|
||||
import type { ListAccountsResult, StartLinkResult, UpdateGroupResult } from '@/types';
|
||||
import { formatPhoneNumber } from '@/utils/format';
|
||||
import { logError, logInfo, logSuccess, logVerbose, logWarn } from '@/utils/log';
|
||||
import { call } from '@/utils/rpc';
|
||||
|
||||
interface StartLinkResult {
|
||||
deviceLinkUri: string;
|
||||
}
|
||||
|
||||
interface UpdateGroupResult {
|
||||
groupId: string;
|
||||
}
|
||||
|
||||
type ListAccountsResult = {
|
||||
number: string;
|
||||
name?: string;
|
||||
uuid?: string;
|
||||
}[];
|
||||
|
||||
let account: string | null = null;
|
||||
let currentLinkUri: string | null = null;
|
||||
let daemon: ReturnType<typeof Bun.spawn> | null = null;
|
||||
|
|
@ -27,20 +40,21 @@ export async function initSignal({ accountOverride }: { accountOverride?: string
|
|||
if (accountOverride) {
|
||||
account = accountOverride;
|
||||
logVerbose(`Signal account set: ${account}`);
|
||||
logSuccess('Signal account linked');
|
||||
|
||||
return { linked: true, account };
|
||||
}
|
||||
|
||||
const result = (await call('listAccounts', {}, null)) as ListAccountsResult;
|
||||
const [firstAccount] = result;
|
||||
|
||||
if (firstAccount) {
|
||||
account = firstAccount.number;
|
||||
logVerbose(`Signal account initialized: ${formatPhoneNumber(account)}`);
|
||||
logSuccess('Signal account linked');
|
||||
return { linked: true, account };
|
||||
}
|
||||
|
||||
logVerbose('No Signal accounts found');
|
||||
|
||||
return { linked: false, account: null };
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +104,9 @@ export async function finishLink() {
|
|||
},
|
||||
null,
|
||||
);
|
||||
|
||||
logSuccess('Device linked successfully');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -103,6 +119,7 @@ async function unlinkDevice() {
|
|||
|
||||
try {
|
||||
await rm(SIGNAL_CLI_DATA, { recursive: true, force: true });
|
||||
|
||||
logSuccess('Account data removed');
|
||||
} catch (error) {
|
||||
logError('Failed to remove account data directory:', error);
|
||||
|
|
@ -155,6 +172,7 @@ export async function sendGroupMessage(
|
|||
export async function checkSignalCli() {
|
||||
try {
|
||||
await call('listAccounts', {}, account);
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
|
|
@ -164,6 +182,7 @@ export async function checkSignalCli() {
|
|||
export async function hasValidAccount() {
|
||||
try {
|
||||
const result = (await call('listAccounts', {}, account)) as ListAccountsResult;
|
||||
|
||||
return result.length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
|
|
@ -233,6 +252,7 @@ export async function startDaemon() {
|
|||
|
||||
for (let i = 0; i < 60; i++) {
|
||||
await Bun.sleep(500);
|
||||
|
||||
try {
|
||||
const socket = await Bun.connect({
|
||||
unix: SIGNAL_CLI_SOCKET,
|
||||
|
|
@ -240,22 +260,31 @@ export async function startDaemon() {
|
|||
data() {},
|
||||
},
|
||||
});
|
||||
|
||||
socket.end();
|
||||
|
||||
logSuccess(`signal-cli daemon started (${(i + 1) * 0.5}s)`);
|
||||
|
||||
daemon = proc;
|
||||
|
||||
return proc;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (authError && !cleaned) {
|
||||
logWarn('Detected stale account data, cleaning up and retrying...');
|
||||
|
||||
proc.kill();
|
||||
|
||||
await unlinkDevice();
|
||||
|
||||
cleaned = true;
|
||||
|
||||
return startDaemon();
|
||||
}
|
||||
|
||||
logError('Failed to connect to signal-cli socket: daemon did not start within 30 seconds');
|
||||
|
||||
if (proc.exitCode !== null) {
|
||||
logError('signal-cli process exited with code:', proc.exitCode);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { SUP_ENDPOINT_PREFIX } from '@/constants/config';
|
||||
|
||||
export interface UnifiedPushMessage {
|
||||
interface UnifiedPushMessage {
|
||||
endpoint: string;
|
||||
title?: string;
|
||||
body?: string;
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@
|
|||
<h2>Endpoints</h2>
|
||||
<div id="endpoints-list"
|
||||
hx-get="/fragment/endpoints"
|
||||
hx-trigger="load"
|
||||
hx-trigger="load, every 10s"
|
||||
hx-indicator="#endpoints-list">
|
||||
<div class="loading">
|
||||
<div class="spinner"></div>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import {
|
|||
generateLinkQR,
|
||||
getAccount,
|
||||
hasValidAccount,
|
||||
initSignal,
|
||||
} from '@/modules/signal';
|
||||
import { getAllMappings, remove } from '@/modules/store';
|
||||
import { verifyApiKey } from '@/utils/auth';
|
||||
|
|
@ -19,7 +18,7 @@ let qrCacheTime = 0;
|
|||
let generatingPromise: Promise<string> | null = null;
|
||||
const QR_CACHE_TTL = 10 * 60 * 1000;
|
||||
|
||||
const admin = new Hono();
|
||||
export const admin = new Hono();
|
||||
|
||||
admin.use(
|
||||
'*',
|
||||
|
|
@ -166,16 +165,11 @@ const handleQRSection = async () => {
|
|||
cachedQR = qr;
|
||||
qrCacheTime = Date.now();
|
||||
|
||||
finishLink()
|
||||
.then(async () => {
|
||||
await initSignal();
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
generatingPromise = null;
|
||||
cachedQR = null;
|
||||
qrCacheTime = 0;
|
||||
});
|
||||
finishLink().finally(() => {
|
||||
generatingPromise = null;
|
||||
cachedQR = null;
|
||||
qrCacheTime = 0;
|
||||
});
|
||||
|
||||
return qr;
|
||||
} catch (error) {
|
||||
|
|
@ -201,5 +195,3 @@ const handleQRSection = async () => {
|
|||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
export default admin;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { getOrCreateGroup } from '@/modules/store';
|
|||
import { verifyApiKey } from '@/utils/auth';
|
||||
import { logError, logVerbose } from '@/utils/log';
|
||||
|
||||
const ntfy = new Hono();
|
||||
export const ntfy = new Hono();
|
||||
|
||||
ntfy.use(
|
||||
'*',
|
||||
|
|
@ -64,5 +64,3 @@ ntfy.post('/:topic', async (c) => {
|
|||
return c.text('Internal server error', 500);
|
||||
}
|
||||
});
|
||||
|
||||
export default ntfy;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { sendGroupMessage } from '@/modules/signal';
|
|||
import { getGroupId, getOrCreateGroup, remove } from '@/modules/store';
|
||||
import { formatAsSignalMessage, parseUnifiedPushRequest } from '@/modules/unified-push';
|
||||
|
||||
const unifiedpush = new Hono();
|
||||
export const unifiedpush = new Hono();
|
||||
|
||||
unifiedpush.get('/up', (c) =>
|
||||
c.json({
|
||||
|
|
@ -45,5 +45,3 @@ unifiedpush.delete('/up/:instance', async (c) => {
|
|||
remove(endpointId);
|
||||
return c.body(null, 204);
|
||||
});
|
||||
|
||||
export default unifiedpush;
|
||||
|
|
|
|||
36
server/types.d.ts
vendored
36
server/types.d.ts
vendored
|
|
@ -1,36 +0,0 @@
|
|||
export interface JsonRpcRequest {
|
||||
jsonrpc: '2.0';
|
||||
id: number;
|
||||
method: string;
|
||||
params: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface JsonRpcResponse<T = unknown> {
|
||||
jsonrpc: '2.0';
|
||||
id: number;
|
||||
result?: T;
|
||||
error?: {
|
||||
code: number;
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface StartLinkResult {
|
||||
deviceLinkUri: string;
|
||||
}
|
||||
|
||||
export interface FinishLinkResult {
|
||||
account: string;
|
||||
}
|
||||
|
||||
export interface UpdateGroupResult {
|
||||
groupId: string;
|
||||
}
|
||||
|
||||
export interface AccountInfo {
|
||||
number: string;
|
||||
name?: string;
|
||||
uuid?: string;
|
||||
}
|
||||
|
||||
export type ListAccountsResult = AccountInfo[];
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { timingSafeEqual } from 'node:crypto';
|
||||
import { networkInterfaces } from 'node:os';
|
||||
import type { Context } from 'hono';
|
||||
import { getConnInfo } from 'hono/bun';
|
||||
import { ALLOW_INSECURE_HTTP, API_KEY } from '@/constants/config';
|
||||
|
|
@ -10,6 +11,7 @@ export const isLocalIP = (addr: string | undefined) => {
|
|||
if (octets.length !== 4 || octets.some((n) => Number.isNaN(n))) return false;
|
||||
|
||||
const [a, b] = octets;
|
||||
|
||||
return (
|
||||
a === 127 ||
|
||||
a === 10 ||
|
||||
|
|
@ -35,3 +37,21 @@ export const verifyApiKey = (password: string, c?: Context) => {
|
|||
|
||||
return timingSafeEqual(providedBuffer, keyBuffer);
|
||||
};
|
||||
|
||||
export const getLanIP = () => {
|
||||
const nets = networkInterfaces();
|
||||
|
||||
for (const name of Object.keys(nets)) {
|
||||
const interfaces = nets[name];
|
||||
|
||||
if (!interfaces) continue;
|
||||
|
||||
for (const iface of interfaces) {
|
||||
if (iface.family === 'IPv4' && !iface.internal) {
|
||||
return iface.address;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -33,3 +33,8 @@ export const formatPhoneNumber = (phone: string): string => {
|
|||
|
||||
return phone;
|
||||
};
|
||||
|
||||
export const formatToCspString = (cspConfig: Record<string, string[]>) =>
|
||||
Object.entries(cspConfig)
|
||||
.map(([key, values]) => `${key.replace(/([A-Z])/g, '-$1').toLowerCase()} ${values.join(' ')}`)
|
||||
.join('; ');
|
||||
|
|
|
|||
|
|
@ -26,14 +26,15 @@ export const call = (method: string, params: Record<string, unknown>, account: s
|
|||
}
|
||||
}
|
||||
},
|
||||
error(_socket, error) {
|
||||
error(_, error) {
|
||||
reject(error);
|
||||
},
|
||||
connectError(_socket, error) {
|
||||
connectError(_, error) {
|
||||
reject(error);
|
||||
},
|
||||
close() {
|
||||
const isComplete = response.includes(MESSAGE_DELIMITER);
|
||||
|
||||
if (!isComplete) {
|
||||
reject(new Error('Connection closed before response received'));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue