Rework Streaming controller to use signerMiddleware

This commit is contained in:
Alex Gleason 2024-10-22 18:44:02 -05:00
parent 28b874edc9
commit 9eaeeb3234
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
3 changed files with 49 additions and 62 deletions

View file

@ -177,7 +177,7 @@ app.use('/users/*', metricsMiddleware, logger(debug));
app.use('/nodeinfo/*', metricsMiddleware, logger(debug)); app.use('/nodeinfo/*', metricsMiddleware, logger(debug));
app.use('/oauth/*', metricsMiddleware, logger(debug)); app.use('/oauth/*', metricsMiddleware, logger(debug));
app.get('/api/v1/streaming', metricsMiddleware, streamingController); app.get('/api/v1/streaming', metricsMiddleware, signerMiddleware, streamingController);
app.get('/relay', metricsMiddleware, relayController); app.get('/relay', metricsMiddleware, relayController);
app.use( app.use(

View file

@ -14,8 +14,7 @@ import { MuteListPolicy } from '@/policies/MuteListPolicy.ts';
import { getFeedPubkeys } from '@/queries.ts'; import { getFeedPubkeys } from '@/queries.ts';
import { hydrateEvents } from '@/storages/hydrate.ts'; import { hydrateEvents } from '@/storages/hydrate.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
import { getTokenHash } from '@/utils/auth.ts'; import { Time } from '@/utils.ts';
import { bech32ToPubkey, Time } from '@/utils.ts';
import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts'; import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts';
import { renderNotification } from '@/views/mastodon/notifications.ts'; import { renderNotification } from '@/views/mastodon/notifications.ts';
@ -70,7 +69,8 @@ const connections = new Set<WebSocket>();
const streamingController: AppController = async (c) => { const streamingController: AppController = async (c) => {
const upgrade = c.req.header('upgrade'); const upgrade = c.req.header('upgrade');
const token = c.req.header('sec-websocket-protocol'); const protocol = c.req.header('sec-websocket-protocol');
const signer = c.get('signer');
const stream = streamSchema.optional().catch(undefined).parse(c.req.query('stream')); const stream = streamSchema.optional().catch(undefined).parse(c.req.query('stream'));
const controller = new AbortController(); const controller = new AbortController();
@ -78,8 +78,8 @@ const streamingController: AppController = async (c) => {
return c.text('Please use websocket protocol', 400); return c.text('Please use websocket protocol', 400);
} }
const pubkey = token ? await getTokenPubkey(token) : undefined; const pubkey = await signer?.getPublicKey();
if (token && !pubkey) { if (!pubkey) {
return c.json({ error: 'Invalid access token' }, 401); return c.json({ error: 'Invalid access token' }, 401);
} }
@ -91,7 +91,7 @@ const streamingController: AppController = async (c) => {
} }
} }
const { socket, response } = Deno.upgradeWebSocket(c.req.raw, { protocol: token, idleTimeout: 30 }); const { socket, response } = Deno.upgradeWebSocket(c.req.raw, { protocol, idleTimeout: 30 });
const store = await Storages.db(); const store = await Storages.db();
const pubsub = await Storages.pubsub(); const pubsub = await Storages.pubsub();
@ -231,21 +231,4 @@ async function topicToFilter(
} }
} }
async function getTokenPubkey(token: string): Promise<string | undefined> {
if (token.startsWith('token1')) {
const kysely = await Storages.kysely();
const tokenHash = await getTokenHash(token as `token1${string}`);
const { pubkey } = await kysely
.selectFrom('auth_tokens')
.select('pubkey')
.where('token_hash', '=', tokenHash)
.executeTakeFirstOrThrow();
return pubkey;
} else {
return bech32ToPubkey(token);
}
}
export { streamingController }; export { streamingController };

View file

@ -10,54 +10,58 @@ import { Storages } from '@/storages.ts';
import { aesDecrypt } from '@/utils/aes.ts'; import { aesDecrypt } from '@/utils/aes.ts';
import { getTokenHash } from '@/utils/auth.ts'; import { getTokenHash } from '@/utils/auth.ts';
/** We only accept "Bearer" type. */
const BEARER_REGEX = new RegExp(`^Bearer (${nip19.BECH32_REGEX.source})$`);
/** Make a `signer` object available to all controllers, or unset if the user isn't logged in. */ /** Make a `signer` object available to all controllers, or unset if the user isn't logged in. */
export const signerMiddleware: AppMiddleware = async (c, next) => { export const signerMiddleware: AppMiddleware = async (c, next) => {
const header = c.req.header('authorization'); const accessToken = getAccessToken(c.req.raw);
const match = header?.match(BEARER_REGEX);
if (match) { if (accessToken?.startsWith('token1')) {
const [_, bech32] = match; try {
const kysely = await Storages.kysely();
const tokenHash = await getTokenHash(accessToken as `token1${string}`);
if (bech32.startsWith('token1')) { const { pubkey, nip46_sk_enc, nip46_relays } = await kysely
try { .selectFrom('auth_tokens')
const kysely = await Storages.kysely(); .select(['pubkey', 'nip46_sk_enc', 'nip46_relays'])
const tokenHash = await getTokenHash(bech32 as `token1${string}`); .where('token_hash', '=', tokenHash)
.executeTakeFirstOrThrow();
const { pubkey, nip46_sk_enc, nip46_relays } = await kysely const nep46Seckey = await aesDecrypt(Conf.seckey, nip46_sk_enc);
.selectFrom('auth_tokens')
.select(['pubkey', 'nip46_sk_enc', 'nip46_relays'])
.where('token_hash', '=', tokenHash)
.executeTakeFirstOrThrow();
const nep46Seckey = await aesDecrypt(Conf.seckey, nip46_sk_enc); c.set('signer', new ConnectSigner(pubkey, new NSecSigner(nep46Seckey), nip46_relays));
} catch {
throw new HTTPException(401);
}
} else {
try {
const decoded = nip19.decode(accessToken!);
c.set('signer', new ConnectSigner(pubkey, new NSecSigner(nep46Seckey), nip46_relays)); switch (decoded.type) {
} catch { case 'npub':
throw new HTTPException(401); c.set('signer', new ReadOnlySigner(decoded.data));
} break;
} else { case 'nprofile':
try { c.set('signer', new ReadOnlySigner(decoded.data.pubkey));
const decoded = nip19.decode(bech32!); break;
case 'nsec':
switch (decoded.type) { c.set('signer', new NSecSigner(decoded.data));
case 'npub': break;
c.set('signer', new ReadOnlySigner(decoded.data));
break;
case 'nprofile':
c.set('signer', new ReadOnlySigner(decoded.data.pubkey));
break;
case 'nsec':
c.set('signer', new NSecSigner(decoded.data));
break;
}
} catch {
throw new HTTPException(401);
} }
} catch {
throw new HTTPException(401);
} }
} }
await next(); await next();
}; };
/** Extract the access token from the request headers. */
function getAccessToken(req: Request): string | undefined {
const upgrade = req.headers.get('upgrade');
// WebSockets use the `sec-websocket-protocol` header instead of `authorization`.
if (upgrade === 'websocket') {
return req.headers.get('sec-websocket-protocol') ?? undefined;
}
return req.headers.get('authorization')?.match(/^Bearer (.+)$/)?.[1];
}