From 9eaeeb323456704a68ccbff5c16da77883f1b45b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 22 Oct 2024 18:44:02 -0500 Subject: [PATCH] Rework Streaming controller to use signerMiddleware --- src/app.ts | 2 +- src/controllers/api/streaming.ts | 29 +++-------- src/middleware/signerMiddleware.ts | 80 ++++++++++++++++-------------- 3 files changed, 49 insertions(+), 62 deletions(-) diff --git a/src/app.ts b/src/app.ts index fdcacf29..20912121 100644 --- a/src/app.ts +++ b/src/app.ts @@ -177,7 +177,7 @@ app.use('/users/*', metricsMiddleware, logger(debug)); app.use('/nodeinfo/*', 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.use( diff --git a/src/controllers/api/streaming.ts b/src/controllers/api/streaming.ts index 9693a16c..a73e2ccf 100644 --- a/src/controllers/api/streaming.ts +++ b/src/controllers/api/streaming.ts @@ -14,8 +14,7 @@ import { MuteListPolicy } from '@/policies/MuteListPolicy.ts'; import { getFeedPubkeys } from '@/queries.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { Storages } from '@/storages.ts'; -import { getTokenHash } from '@/utils/auth.ts'; -import { bech32ToPubkey, Time } from '@/utils.ts'; +import { Time } from '@/utils.ts'; import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts'; import { renderNotification } from '@/views/mastodon/notifications.ts'; @@ -70,7 +69,8 @@ const connections = new Set(); const streamingController: AppController = async (c) => { 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 controller = new AbortController(); @@ -78,8 +78,8 @@ const streamingController: AppController = async (c) => { return c.text('Please use websocket protocol', 400); } - const pubkey = token ? await getTokenPubkey(token) : undefined; - if (token && !pubkey) { + const pubkey = await signer?.getPublicKey(); + if (!pubkey) { 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 pubsub = await Storages.pubsub(); @@ -231,21 +231,4 @@ async function topicToFilter( } } -async function getTokenPubkey(token: string): Promise { - 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 }; diff --git a/src/middleware/signerMiddleware.ts b/src/middleware/signerMiddleware.ts index b4cab1ec..de6c859f 100644 --- a/src/middleware/signerMiddleware.ts +++ b/src/middleware/signerMiddleware.ts @@ -10,54 +10,58 @@ import { Storages } from '@/storages.ts'; import { aesDecrypt } from '@/utils/aes.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. */ export const signerMiddleware: AppMiddleware = async (c, next) => { - const header = c.req.header('authorization'); - const match = header?.match(BEARER_REGEX); + const accessToken = getAccessToken(c.req.raw); - if (match) { - const [_, bech32] = match; + if (accessToken?.startsWith('token1')) { + try { + const kysely = await Storages.kysely(); + const tokenHash = await getTokenHash(accessToken as `token1${string}`); - if (bech32.startsWith('token1')) { - try { - const kysely = await Storages.kysely(); - const tokenHash = await getTokenHash(bech32 as `token1${string}`); + const { pubkey, nip46_sk_enc, nip46_relays } = await kysely + .selectFrom('auth_tokens') + .select(['pubkey', 'nip46_sk_enc', 'nip46_relays']) + .where('token_hash', '=', tokenHash) + .executeTakeFirstOrThrow(); - const { pubkey, nip46_sk_enc, nip46_relays } = await kysely - .selectFrom('auth_tokens') - .select(['pubkey', 'nip46_sk_enc', 'nip46_relays']) - .where('token_hash', '=', tokenHash) - .executeTakeFirstOrThrow(); + const nep46Seckey = await aesDecrypt(Conf.seckey, nip46_sk_enc); - 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)); - } catch { - throw new HTTPException(401); - } - } else { - try { - const decoded = nip19.decode(bech32!); - - switch (decoded.type) { - case 'npub': - 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); + switch (decoded.type) { + case 'npub': + 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); } } 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]; +}