From 959dd27ad65317faa7b81c4b2c6591fa15e6558b Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Mon, 6 May 2024 08:19:42 +0000 Subject: [PATCH] refactor(api): :art: Finish Hono refactor --- bun.lockb | Bin 144460 -> 144460 bytes index.ts | 56 ++++- middlewares/agent-bans.ts | 16 ++ middlewares/bait.ts | 73 ++++++ middlewares/ip-bans.ts | 40 ++++ middlewares/logger.ts | 18 ++ server.ts | 207 +----------------- .../api/api/v1/accounts/:id/statuses.test.ts | 4 +- .../v1/accounts/familiar_followers/index.ts | 12 +- .../api/v1/accounts/relationships/index.ts | 9 +- server/api/api/v1/notifications/index.test.ts | 4 +- server/api/api/v1/statuses/index.test.ts | 68 +++--- server/api/api/v1/statuses/index.ts | 7 +- server/api/oauth/authorize/index.ts | 9 +- server/api/oauth/token/index.ts | 6 +- server2.ts | 21 -- tests/api/accounts.test.ts | 9 +- tests/api/statuses.test.ts | 15 +- tests/oauth.test.ts | 49 +++-- utils/api.ts | 2 + 20 files changed, 309 insertions(+), 316 deletions(-) create mode 100644 middlewares/agent-bans.ts create mode 100644 middlewares/bait.ts create mode 100644 middlewares/ip-bans.ts create mode 100644 middlewares/logger.ts delete mode 100644 server2.ts diff --git a/bun.lockb b/bun.lockb index be22e73f1d643e84cd2d146541a2800ed6e39999..e44f0e2cbb97cda59bd2d8b9cc383f7f94069fa0 100755 GIT binary patch delta 22 ecmX@}g5%5!j)pCa#=X { + if (config.frontend.glitch.enabled) { + const glitch = await handleGlitchRequest(context.req.raw, dualLogger); + + if (glitch) { + return glitch; + } + } + + const base_url_with_http = config.http.base_url.replace( + "https://", + "http://", + ); + + const replacedUrl = context.req.url + .replace(config.http.base_url, config.frontend.url) + .replace(base_url_with_http, config.frontend.url); + + const proxy = await fetch(replacedUrl, { + headers: { + // Include for SSR + "X-Forwarded-Host": `${config.http.bind}:${config.http.bind_port}`, + "Accept-Encoding": "identity", + }, + }).catch(async (e) => { + await dualLogger.logError(LogLevel.ERROR, "Server.Proxy", e as Error); + await dualLogger.log( + LogLevel.ERROR, + "Server.Proxy", + `The Frontend is not running or the route is not found: ${replacedUrl}`, + ); + return null; + }); + + proxy?.headers.set("Cache-Control", "max-age=31536000"); + + if (!proxy || proxy.status === 404) { + return errorResponse("Route not found on proxy or API route", 404); + } + + return proxy; +}); + createServer(config, app); await dualServerLogger.log( diff --git a/middlewares/agent-bans.ts b/middlewares/agent-bans.ts new file mode 100644 index 00000000..a48e1b0d --- /dev/null +++ b/middlewares/agent-bans.ts @@ -0,0 +1,16 @@ +import { errorResponse } from "@response"; +import { createMiddleware } from "hono/factory"; +import { config } from "~packages/config-manager"; + +export const agentBans = createMiddleware(async (context, next) => { + // Check for banned user agents (regex) + const ua = context.req.header("user-agent") ?? ""; + + for (const agent of config.http.banned_user_agents) { + if (new RegExp(agent).test(ua)) { + return errorResponse("Forbidden", 403); + } + } + + await next(); +}); diff --git a/middlewares/bait.ts b/middlewares/bait.ts new file mode 100644 index 00000000..f7724be7 --- /dev/null +++ b/middlewares/bait.ts @@ -0,0 +1,73 @@ +import { logger } from "@loggers"; +import { errorResponse, response } from "@response"; +import type { SocketAddress } from "bun"; +import { createMiddleware } from "hono/factory"; +import { matches } from "ip-matching"; +import { config } from "~packages/config-manager"; +import { LogLevel } from "~packages/log-manager"; + +export const bait = createMiddleware(async (context, next) => { + const request_ip = context.env?.ip as SocketAddress | undefined | null; + + if (config.http.bait.enabled) { + // Check for bait IPs + if (request_ip?.address) { + for (const ip of config.http.bait.bait_ips) { + try { + if (matches(ip, request_ip.address)) { + const file = Bun.file( + config.http.bait.send_file || "./beemovie.txt", + ); + + if (await file.exists()) { + return response(file); + } + await logger.log( + LogLevel.ERROR, + "Server.Bait", + `Bait file not found: ${config.http.bait.send_file}`, + ); + } + } catch (e) { + logger.log( + LogLevel.ERROR, + "Server.IPCheck", + `Error while parsing bait IP "${ip}" `, + ); + logger.logError( + LogLevel.ERROR, + "Server.IPCheck", + e as Error, + ); + + return errorResponse( + `A server error occured: ${(e as Error).message}`, + 500, + ); + } + } + } + + // Check for bait user agents (regex) + const ua = context.req.header("user-agent") ?? ""; + + for (const agent of config.http.bait.bait_user_agents) { + if (new RegExp(agent).test(ua)) { + const file = Bun.file( + config.http.bait.send_file || "./beemovie.txt", + ); + + if (await file.exists()) { + return response(file); + } + await logger.log( + LogLevel.ERROR, + "Server.Bait", + `Bait file not found: ${config.http.bait.send_file}`, + ); + } + } + } + + await next(); +}); diff --git a/middlewares/ip-bans.ts b/middlewares/ip-bans.ts new file mode 100644 index 00000000..190926a7 --- /dev/null +++ b/middlewares/ip-bans.ts @@ -0,0 +1,40 @@ +import { logger } from "@loggers"; +import { errorResponse } from "@response"; +import type { SocketAddress } from "bun"; +import { createMiddleware } from "hono/factory"; +import { matches } from "ip-matching"; +import { config } from "~packages/config-manager"; +import { LogLevel } from "~packages/log-manager"; + +export const ipBans = createMiddleware(async (context, next) => { + // Check for banned IPs + + const request_ip = context.env?.ip as SocketAddress | undefined | null; + + if (!request_ip?.address) { + await next(); + return; + } + + for (const ip of config.http.banned_ips) { + try { + if (matches(ip, request_ip?.address)) { + return errorResponse("Forbidden", 403); + } + } catch (e) { + logger.log( + LogLevel.ERROR, + "Server.IPCheck", + `Error while parsing banned IP "${ip}" `, + ); + logger.logError(LogLevel.ERROR, "Server.IPCheck", e as Error); + + return errorResponse( + `A server error occured: ${(e as Error).message}`, + 500, + ); + } + } + + await next(); +}); diff --git a/middlewares/logger.ts b/middlewares/logger.ts new file mode 100644 index 00000000..241f1a27 --- /dev/null +++ b/middlewares/logger.ts @@ -0,0 +1,18 @@ +import { dualLogger } from "@loggers"; +import type { SocketAddress } from "bun"; +import { createMiddleware } from "hono/factory"; +import { config } from "~packages/config-manager"; + +export const logger = createMiddleware(async (context, next) => { + const request_ip = context.env?.ip as SocketAddress | undefined | null; + + if (config.logging.log_requests) { + await dualLogger.logRequest( + context.req.raw, + config.logging.log_ip ? request_ip?.address : undefined, + config.logging.log_requests_verbose, + ); + } + + await next(); +}); diff --git a/server.ts b/server.ts index ef238cf7..d85fc356 100644 --- a/server.ts +++ b/server.ts @@ -1,19 +1,7 @@ -import { dualLogger } from "@loggers"; -import { clientResponse, errorResponse, response } from "@response"; -import type { MatchedRoute } from "bun"; import type { Config } from "config-manager"; -import { matches } from "ip-matching"; -import type { LogManager, MultiLogManager } from "log-manager"; -import { LogLevel } from "log-manager"; -import { processRoute } from "server-handler"; -import { handleGlitchRequest } from "~packages/glitch-server/main"; -import { matchRoute } from "~routes"; +import type { Hono } from "hono"; -export const createServer = ( - config: Config, - logger: LogManager | MultiLogManager, - isProd: boolean, -) => +export const createServer = (config: Config, app: Hono) => Bun.serve({ port: config.http.bind_port, tls: config.http.tls.enabled @@ -27,194 +15,7 @@ export const createServer = ( } : undefined, hostname: config.http.bind || "0.0.0.0", // defaults to "0.0.0.0" - async fetch(req) { - // Check for banned IPs - const request_ip = this.requestIP(req)?.address ?? ""; - - for (const ip of config.http.banned_ips) { - try { - if (matches(ip, request_ip)) { - return errorResponse("Forbidden", 403); - } - } catch (e) { - logger.log( - LogLevel.ERROR, - "Server.IPCheck", - `Error while parsing banned IP "${ip}" `, - ); - logger.logError( - LogLevel.ERROR, - "Server.IPCheck", - e as Error, - ); - - return errorResponse( - `A server error occured: ${(e as Error).message}`, - 500, - ); - } - } - - // Check for banned user agents (regex) - const ua = req.headers.get("User-Agent") ?? ""; - - for (const agent of config.http.banned_user_agents) { - if (new RegExp(agent).test(ua)) { - return errorResponse("Forbidden", 403); - } - } - - if (config.http.bait.enabled) { - // Check for bait IPs - for (const ip of config.http.bait.bait_ips) { - try { - if (matches(ip, request_ip)) { - const file = Bun.file( - config.http.bait.send_file || "./beemovie.txt", - ); - - if (await file.exists()) { - return response(file); - } - await logger.log( - LogLevel.ERROR, - "Server.Bait", - `Bait file not found: ${config.http.bait.send_file}`, - ); - } - } catch (e) { - logger.log( - LogLevel.ERROR, - "Server.IPCheck", - `Error while parsing bait IP "${ip}" `, - ); - logger.logError( - LogLevel.ERROR, - "Server.IPCheck", - e as Error, - ); - - return errorResponse( - `A server error occured: ${(e as Error).message}`, - 500, - ); - } - } - - // Check for bait user agents (regex) - for (const agent of config.http.bait.bait_user_agents) { - if (new RegExp(agent).test(ua)) { - const file = Bun.file( - config.http.bait.send_file || "./beemovie.txt", - ); - - if (await file.exists()) { - return response(file); - } - await logger.log( - LogLevel.ERROR, - "Server.Bait", - `Bait file not found: ${config.http.bait.send_file}`, - ); - } - } - } - - if (config.logging.log_requests) { - await logger.logRequest( - req.clone(), - config.logging.log_ip ? request_ip : undefined, - config.logging.log_requests_verbose, - ); - } - - const routePaths = [ - "/api", - "/media", - "/nodeinfo", - "/.well-known", - "/users", - "/objects", - "/oauth/token", - "/oauth/providers", - ]; - - // Check if URL starts with routePath - if ( - routePaths.some((path) => - new URL(req.url).pathname.startsWith(path), - ) || - (new URL(req.url).pathname.startsWith("/oauth/authorize") && - req.method === "POST") - ) { - // If route is .well-known, remove dot because the filesystem router can't handle dots for some reason - const matchedRoute = matchRoute( - new Request(req.url.replace(".well-known", "well-known"), { - method: req.method, - }), - ); - - if ( - matchedRoute?.filePath && - matchedRoute.name !== "/[...404]" && - !( - new URL(req.url).pathname.startsWith( - "/oauth/authorize", - ) && req.method === "GET" - ) - ) { - return await processRoute(matchedRoute, req, logger); - } - } - - if (config.frontend.glitch.enabled) { - if (!new URL(req.url).pathname.startsWith("/oauth")) { - const glitch = await handleGlitchRequest(req, dualLogger); - - if (glitch) { - return glitch; - } - } - } - - const base_url_with_http = config.http.base_url.replace( - "https://", - "http://", - ); - - const replacedUrl = req.url - .replace(config.http.base_url, config.frontend.url) - .replace(base_url_with_http, config.frontend.url); - - const proxy = await fetch(replacedUrl, { - headers: { - // Include for SSR - "X-Forwarded-Host": `${config.http.bind}:${config.http.bind_port}`, - "Accept-Encoding": "identity", - }, - }).catch(async (e) => { - await logger.logError( - LogLevel.ERROR, - "Server.Proxy", - e as Error, - ); - await logger.log( - LogLevel.ERROR, - "Server.Proxy", - `The Frontend is not running or the route is not found: ${replacedUrl}`, - ); - return null; - }); - - proxy?.headers.set("Cache-Control", "max-age=31536000"); - - if (!proxy || proxy.status === 404) { - return errorResponse( - "Route not found on proxy or API route", - 404, - ); - } - - return proxy; + fetch(req, server) { + return app.fetch(req, { ip: server.requestIP(req) }); }, }); diff --git a/server/api/api/v1/accounts/:id/statuses.test.ts b/server/api/api/v1/accounts/:id/statuses.test.ts index 3ffa6b18..09cc9953 100644 --- a/server/api/api/v1/accounts/:id/statuses.test.ts +++ b/server/api/api/v1/accounts/:id/statuses.test.ts @@ -105,10 +105,10 @@ describe(meta.route, () => { headers: { Authorization: `Bearer ${tokens[1].accessToken}`, }, - body: getFormData({ + body: new URLSearchParams({ status: "Reply", in_reply_to_id: timeline[0].id, - federate: false, + federate: "false", }), }), ); diff --git a/server/api/api/v1/accounts/familiar_followers/index.ts b/server/api/api/v1/accounts/familiar_followers/index.ts index 3dec4c43..2a5249f2 100644 --- a/server/api/api/v1/accounts/familiar_followers/index.ts +++ b/server/api/api/v1/accounts/familiar_followers/index.ts @@ -1,4 +1,4 @@ -import { applyConfig, auth, handleZodError, idValidator } from "@api"; +import { applyConfig, auth, handleZodError, idValidator, qsQuery } from "@api"; import { zValidator } from "@hono/zod-validator"; import { errorResponse, jsonResponse } from "@response"; import { inArray } from "drizzle-orm"; @@ -23,7 +23,7 @@ export const meta = applyConfig({ export const schemas = { query: z.object({ - "id[]": z.array(z.string().uuid()).min(1).max(10), + id: z.array(z.string().uuid()).min(1).max(10).or(z.string().uuid()), }), }; @@ -31,11 +31,12 @@ export default (app: Hono) => app.on( meta.allowedMethods, meta.route, + qsQuery(), zValidator("query", schemas.query, handleZodError), auth(meta.auth), async (context) => { const { user: self } = context.req.valid("header"); - const { "id[]": ids } = context.req.valid("query"); + const { id: ids } = context.req.valid("query"); if (!self) return errorResponse("Unauthorized", 401); @@ -46,7 +47,10 @@ export default (app: Hono) => }, where: (relationship, { inArray, and, eq }) => and( - inArray(relationship.subjectId, ids), + inArray( + relationship.subjectId, + Array.isArray(ids) ? ids : [ids], + ), eq(relationship.following, true), ), }); diff --git a/server/api/api/v1/accounts/relationships/index.ts b/server/api/api/v1/accounts/relationships/index.ts index 71a06029..2062af11 100644 --- a/server/api/api/v1/accounts/relationships/index.ts +++ b/server/api/api/v1/accounts/relationships/index.ts @@ -1,4 +1,4 @@ -import { applyConfig, auth, handleZodError, idValidator } from "@api"; +import { applyConfig, auth, handleZodError, idValidator, qsQuery } from "@api"; import { zValidator } from "@hono/zod-validator"; import { errorResponse, jsonResponse } from "@response"; import type { Hono } from "hono"; @@ -25,7 +25,7 @@ export const meta = applyConfig({ export const schemas = { query: z.object({ - "id[]": z.array(z.string().uuid()).min(1).max(10), + id: z.array(z.string().uuid()).min(1).max(10).or(z.string().uuid()), }), }; @@ -33,11 +33,14 @@ export default (app: Hono) => app.on( meta.allowedMethods, meta.route, + qsQuery(), zValidator("query", schemas.query, handleZodError), auth(meta.auth), async (context) => { const { user: self } = context.req.valid("header"); - const { "id[]": ids } = context.req.valid("query"); + const { id } = context.req.valid("query"); + + const ids = Array.isArray(id) ? id : [id]; if (!self) return errorResponse("Unauthorized", 401); diff --git a/server/api/api/v1/notifications/index.test.ts b/server/api/api/v1/notifications/index.test.ts index 93db3f52..790f17f1 100644 --- a/server/api/api/v1/notifications/index.test.ts +++ b/server/api/api/v1/notifications/index.test.ts @@ -83,10 +83,10 @@ beforeAll(async () => { headers: { Authorization: `Bearer ${tokens[1].accessToken}`, }, - body: getFormData({ + body: new URLSearchParams({ status: `@${users[0].getUser().username} test mention`, visibility: "direct", - federate: false, + federate: "false", }), }), ); diff --git a/server/api/api/v1/statuses/index.test.ts b/server/api/api/v1/statuses/index.test.ts index 9578b16f..e4065bae 100644 --- a/server/api/api/v1/statuses/index.test.ts +++ b/server/api/api/v1/statuses/index.test.ts @@ -27,7 +27,7 @@ describe(meta.route, () => { const response = await sendTestRequest( new Request(new URL(meta.route, config.http.base_url), { method: "POST", - body: new FormData(), + body: new URLSearchParams(), }), ); @@ -41,7 +41,7 @@ describe(meta.route, () => { headers: { Authorization: `Bearer ${tokens[0].accessToken}`, }, - body: new FormData(), + body: new URLSearchParams(), }), ); @@ -55,9 +55,9 @@ describe(meta.route, () => { headers: { Authorization: `Bearer ${tokens[0].accessToken}`, }, - body: getFormData({ + body: new URLSearchParams({ status: "a".repeat(config.validation.max_note_size + 1), - federate: false, + federate: "false", }), }), ); @@ -72,10 +72,10 @@ describe(meta.route, () => { headers: { Authorization: `Bearer ${tokens[0].accessToken}`, }, - body: getFormData({ + body: new URLSearchParams({ status: "Hello, world!", visibility: "invalid", - federate: false, + federate: "false", }), }), ); @@ -90,10 +90,10 @@ describe(meta.route, () => { headers: { Authorization: `Bearer ${tokens[0].accessToken}`, }, - body: getFormData({ + body: new URLSearchParams({ status: "Hello, world!", scheduled_at: "invalid", - federate: false, + federate: "false", }), }), ); @@ -108,10 +108,10 @@ describe(meta.route, () => { headers: { Authorization: `Bearer ${tokens[0].accessToken}`, }, - body: getFormData({ + body: new URLSearchParams({ status: "Hello, world!", in_reply_to_id: "invalid", - federate: false, + federate: "false", }), }), ); @@ -126,10 +126,10 @@ describe(meta.route, () => { headers: { Authorization: `Bearer ${tokens[0].accessToken}`, }, - body: getFormData({ + body: new URLSearchParams({ status: "Hello, world!", quote_id: "invalid", - federate: false, + federate: "false", }), }), ); @@ -144,10 +144,10 @@ describe(meta.route, () => { headers: { Authorization: `Bearer ${tokens[0].accessToken}`, }, - body: getFormData({ + body: new URLSearchParams({ status: "Hello, world!", "media_ids[]": "invalid", - federate: false, + federate: "false", }), }), ); @@ -162,9 +162,9 @@ describe(meta.route, () => { headers: { Authorization: `Bearer ${tokens[0].accessToken}`, }, - body: getFormData({ + body: new URLSearchParams({ status: "Hello, world!", - federate: false, + federate: "false", }), }), ); @@ -184,10 +184,10 @@ describe(meta.route, () => { headers: { Authorization: `Bearer ${tokens[0].accessToken}`, }, - body: getFormData({ + body: new URLSearchParams({ status: "Hello, world!", visibility: "unlisted", - federate: false, + federate: "false", }), }), ); @@ -208,9 +208,9 @@ describe(meta.route, () => { headers: { Authorization: `Bearer ${tokens[0].accessToken}`, }, - body: getFormData({ + body: new URLSearchParams({ status: "Hello, world!", - federate: false, + federate: "false", }), }), ); @@ -223,10 +223,10 @@ describe(meta.route, () => { headers: { Authorization: `Bearer ${tokens[0].accessToken}`, }, - body: getFormData({ + body: new URLSearchParams({ status: "Hello, world again!", in_reply_to_id: object.id, - federate: false, + federate: "false", }), }), ); @@ -247,9 +247,9 @@ describe(meta.route, () => { headers: { Authorization: `Bearer ${tokens[0].accessToken}`, }, - body: getFormData({ + body: new URLSearchParams({ status: "Hello, world!", - federate: false, + federate: "false", }), }), ); @@ -262,10 +262,10 @@ describe(meta.route, () => { headers: { Authorization: `Bearer ${tokens[0].accessToken}`, }, - body: getFormData({ + body: new URLSearchParams({ status: "Hello, world again!", quote_id: object.id, - federate: false, + federate: "false", }), }), ); @@ -290,9 +290,9 @@ describe(meta.route, () => { headers: { Authorization: `Bearer ${tokens[0].accessToken}`, }, - body: getFormData({ + body: new URLSearchParams({ status: `Hello, @${users[1].getUser().username}!`, - federate: false, + federate: "false", }), }), ); @@ -319,11 +319,11 @@ describe(meta.route, () => { headers: { Authorization: `Bearer ${tokens[0].accessToken}`, }, - body: getFormData({ + body: new URLSearchParams({ status: `Hello, @${users[1].getUser().username}@${ new URL(config.http.base_url).host }!`, - federate: false, + federate: "false", }), }), ); @@ -352,9 +352,9 @@ describe(meta.route, () => { headers: { Authorization: `Bearer ${tokens[0].accessToken}`, }, - body: getFormData({ + body: new URLSearchParams({ status: "Hi! ", - federate: false, + federate: "false", }), }), ); @@ -378,11 +378,11 @@ describe(meta.route, () => { headers: { Authorization: `Bearer ${tokens[0].accessToken}`, }, - body: getFormData({ + body: new URLSearchParams({ status: "Hello, world!", spoiler_text: "uwu ", - federate: false, + federate: "false", }), }), ); diff --git a/server/api/api/v1/statuses/index.ts b/server/api/api/v1/statuses/index.ts index 06c48e44..ae5e50bc 100644 --- a/server/api/api/v1/statuses/index.ts +++ b/server/api/api/v1/statuses/index.ts @@ -1,4 +1,4 @@ -import { applyConfig, auth, handleZodError } from "@api"; +import { applyConfig, auth, handleZodError, qs } from "@api"; import { zValidator } from "@hono/zod-validator"; import { errorResponse, jsonResponse } from "@response"; import { config } from "config-manager"; @@ -30,7 +30,7 @@ export const schemas = { .optional(), // TODO: Add regex to validate content_type: z.string().optional().default("text/plain"), - "media_ids[]": z + media_ids: z .array(z.string().uuid()) .max(config.validation.max_media_attachments) .optional(), @@ -83,6 +83,7 @@ export default (app: Hono) => app.on( meta.allowedMethods, meta.route, + qs(), zValidator("form", schemas.form, handleZodError), auth(meta.auth), async (context) => { @@ -92,7 +93,7 @@ export default (app: Hono) => const { status, - "media_ids[]": media_ids, + media_ids, "poll[options]": options, in_reply_to_id, quote_id, diff --git a/server/api/oauth/authorize/index.ts b/server/api/oauth/authorize/index.ts index 1707684a..34668f52 100644 --- a/server/api/oauth/authorize/index.ts +++ b/server/api/oauth/authorize/index.ts @@ -29,13 +29,11 @@ export const schemas = { .enum(["none", "login", "consent", "select_account"]) .optional() .default("none"), - max_age: z + max_age: z.coerce .number() .int() .optional() .default(60 * 60 * 24 * 7), - }), - body: z.object({ scope: z.string().optional(), redirect_uri: z.string().url().optional(), response_type: z.enum([ @@ -77,7 +75,6 @@ export default (app: Hono) => meta.allowedMethods, meta.route, zValidator("query", schemas.query, handleZodError), - zValidator("json", schemas.body, handleZodError), async (context) => { const { scope, @@ -87,8 +84,8 @@ export default (app: Hono) => state, code_challenge, code_challenge_method, - } = context.req.valid("json"); - const body = context.req.valid("json"); + } = context.req.valid("query"); + const body = context.req.valid("query"); const cookie = context.req.header("Cookie"); diff --git a/server/api/oauth/token/index.ts b/server/api/oauth/token/index.ts index 9cbbe2ec..5813d5e5 100644 --- a/server/api/oauth/token/index.ts +++ b/server/api/oauth/token/index.ts @@ -20,7 +20,7 @@ export const meta = applyConfig({ }); export const schemas = { - json: z.object({ + form: z.object({ code: z.string().optional(), code_verifier: z.string().optional(), grant_type: z.enum([ @@ -63,10 +63,10 @@ export default (app: Hono) => app.on( meta.allowedMethods, meta.route, - zValidator("json", schemas.json, handleZodError), + zValidator("form", schemas.form, handleZodError), async (context) => { const { grant_type, code, redirect_uri, client_id, client_secret } = - context.req.valid("json"); + context.req.valid("form"); switch (grant_type) { case "authorization_code": { diff --git a/server2.ts b/server2.ts deleted file mode 100644 index d85fc356..00000000 --- a/server2.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Config } from "config-manager"; -import type { Hono } from "hono"; - -export const createServer = (config: Config, app: Hono) => - Bun.serve({ - port: config.http.bind_port, - tls: config.http.tls.enabled - ? { - key: Bun.file(config.http.tls.key), - cert: Bun.file(config.http.tls.cert), - passphrase: config.http.tls.passphrase, - ca: config.http.tls.ca - ? Bun.file(config.http.tls.ca) - : undefined, - } - : undefined, - hostname: config.http.bind || "0.0.0.0", // defaults to "0.0.0.0" - fetch(req, server) { - return app.fetch(req, { ip: server.requestIP(req) }); - }, - }); diff --git a/tests/api/accounts.test.ts b/tests/api/accounts.test.ts index f77b002f..f71b705c 100644 --- a/tests/api/accounts.test.ts +++ b/tests/api/accounts.test.ts @@ -16,6 +16,12 @@ afterAll(async () => { await deleteUsers(); }); +const getFormData = (object: Record) => + Object.keys(object).reduce((formData, key) => { + formData.append(key, String(object[key])); + return formData; + }, new FormData()); + describe("API Tests", () => { describe("PATCH /api/v1/accounts/update_credentials", () => { test("should update the authenticated user's display name", async () => { @@ -29,9 +35,8 @@ describe("API Tests", () => { method: "PATCH", headers: { Authorization: `Bearer ${token.accessToken}`, - "Content-Type": "application/json", }, - body: JSON.stringify({ + body: getFormData({ display_name: "New Display Name", }), }, diff --git a/tests/api/statuses.test.ts b/tests/api/statuses.test.ts index cbc14914..9e825cd8 100644 --- a/tests/api/statuses.test.ts +++ b/tests/api/statuses.test.ts @@ -1,7 +1,6 @@ import { afterAll, describe, expect, test } from "bun:test"; import { config } from "config-manager"; import { getTestUsers, sendTestRequest, wrapRelativeUrl } from "~tests/utils"; -import type { Account as APIAccount } from "~types/mastodon/account"; import type { AsyncAttachment as APIAsyncAttachment } from "~types/mastodon/async_attachment"; import type { Context as APIContext } from "~types/mastodon/context"; import type { Status as APIStatus } from "~types/mastodon/status"; @@ -60,13 +59,12 @@ describe("API Tests", () => { method: "POST", headers: { Authorization: `Bearer ${token.accessToken}`, - "Content-Type": "application/json", }, - body: JSON.stringify({ + body: new URLSearchParams({ status: "Hello, world!", visibility: "public", - media_ids: [media1?.id], - federate: false, + "media_ids[]": media1?.id ?? "", + federate: "false", }), }, ), @@ -108,13 +106,12 @@ describe("API Tests", () => { method: "POST", headers: { Authorization: `Bearer ${token.accessToken}`, - "Content-Type": "application/json", }, - body: JSON.stringify({ + body: new URLSearchParams({ status: "This is a reply!", visibility: "public", - in_reply_to_id: status?.id, - federate: false, + in_reply_to_id: status?.id ?? "", + federate: "false", }), }, ), diff --git a/tests/oauth.test.ts b/tests/oauth.test.ts index d0a1b452..0b8e8329 100644 --- a/tests/oauth.test.ts +++ b/tests/oauth.test.ts @@ -1,4 +1,5 @@ import { afterAll, describe, expect, test } from "bun:test"; +import { config } from "~packages/config-manager"; import type { Application as APIApplication } from "~types/mastodon/application"; import type { Token as APIToken } from "~types/mastodon/token"; import { @@ -8,7 +9,7 @@ import { wrapRelativeUrl, } from "./utils"; -const base_url = "http://lysand.localhost:8080"; //config.http.base_url; +const base_url = config.http.base_url; let client_id: string; let client_secret: string; @@ -19,8 +20,8 @@ const { users, passwords, deleteUsers } = await getTestUsers(1); afterAll(async () => { await deleteUsers(); - await deleteOldTestUsers(); }); + describe("POST /api/v1/apps/", () => { test("should create an application", async () => { const formData = new FormData(); @@ -31,7 +32,7 @@ describe("POST /api/v1/apps/", () => { formData.append("scopes", "read write"); const response = await sendTestRequest( - new Request(wrapRelativeUrl("/api/v1/apps/", base_url), { + new Request(new URL("/api/v1/apps", config.http.base_url), { method: "POST", body: formData, }), @@ -66,8 +67,8 @@ describe("POST /api/auth/login/", () => { const response = await sendTestRequest( new Request( - wrapRelativeUrl( - `/api/auth/login/?client_id=${client_id}&redirect_uri=https://example.com&response_type=code&scope=read+write`, + new URL( + `/api/auth/login?client_id=${client_id}&redirect_uri=https://example.com&response_type=code&scope=read+write`, base_url, ), { @@ -77,8 +78,6 @@ describe("POST /api/auth/login/", () => { ), ); - console.log(await response.text()); - expect(response.status).toBe(302); expect(response.headers.get("location")).toBeDefined(); const locationHeader = new URL( @@ -102,24 +101,28 @@ describe("POST /api/auth/login/", () => { }); }); -describe("POST /oauth/authorize/", () => { +describe("GET /oauth/authorize/", () => { test("should get a code", async () => { const response = await sendTestRequest( - new Request(wrapRelativeUrl("/oauth/authorize", base_url), { - method: "POST", - headers: { - Cookie: `jwt=${jwt}`, - "Content-Type": "application/x-www-form-urlencoded", + new Request( + new URL( + `/oauth/authorize?${new URLSearchParams({ + client_id, + client_secret, + redirect_uri: "https://example.com", + response_type: "code", + scope: "read write", + max_age: "604800", + })}`, + base_url, + ), + { + method: "POST", + headers: { + Cookie: `jwt=${jwt}`, + }, }, - body: new URLSearchParams({ - client_id, - client_secret, - redirect_uri: "https://example.com", - response_type: "code", - scope: "read write", - max_age: "604800", - }), - }), + ), ); expect(response.status).toBe(302); @@ -138,7 +141,7 @@ describe("POST /oauth/authorize/", () => { describe("POST /oauth/token/", () => { test("should get an access token", async () => { const response = await sendTestRequest( - new Request(wrapRelativeUrl("/oauth/token/", base_url), { + new Request(wrapRelativeUrl("/oauth/token", base_url), { method: "POST", headers: { Authorization: `Bearer ${jwt}`, diff --git a/utils/api.ts b/utils/api.ts index b84376e2..da0d7dfa 100644 --- a/utils/api.ts +++ b/utils/api.ts @@ -147,6 +147,8 @@ export const qsQuery = () => { // @ts-ignore Very bad hack context.req.query = () => parsed; + // @ts-ignore I'm so sorry for this + context.req.queries = () => parsed; await next(); }); };