2024-09-23 11:51:15 +02:00
|
|
|
import { join } from "node:path";
|
2024-08-27 18:56:20 +02:00
|
|
|
import { handleZodError } from "@/api";
|
2024-11-25 13:09:28 +01:00
|
|
|
import { applyToHono } from "@/bull-board.ts";
|
2024-09-24 17:03:27 +02:00
|
|
|
import { configureLoggers } from "@/loggers";
|
2024-07-24 18:10:29 +02:00
|
|
|
import { sentry } from "@/sentry";
|
2024-08-27 18:55:02 +02:00
|
|
|
import { swaggerUI } from "@hono/swagger-ui";
|
2024-08-27 16:40:11 +02:00
|
|
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
2024-09-24 17:03:27 +02:00
|
|
|
/* import { prometheus } from "@hono/prometheus"; */
|
|
|
|
|
import { getLogger } from "@logtape/logtape";
|
2025-03-16 17:01:07 +01:00
|
|
|
import { inspect } from "bun";
|
2024-09-23 11:51:15 +02:00
|
|
|
import chalk from "chalk";
|
2024-12-18 20:42:40 +01:00
|
|
|
import { cors } from "hono/cors";
|
|
|
|
|
import { createMiddleware } from "hono/factory";
|
|
|
|
|
import { prettyJSON } from "hono/pretty-json";
|
|
|
|
|
import { secureHeaders } from "hono/secure-headers";
|
2025-02-15 02:47:29 +01:00
|
|
|
import { config } from "~/config.ts";
|
2024-08-27 18:09:15 +02:00
|
|
|
import pkg from "~/package.json" with { type: "application/json" };
|
2024-12-30 18:00:23 +01:00
|
|
|
import { ApiError } from "./classes/errors/api-error.ts";
|
2024-10-04 15:22:48 +02:00
|
|
|
import { PluginLoader } from "./classes/plugin/loader.ts";
|
|
|
|
|
import { agentBans } from "./middlewares/agent-bans.ts";
|
|
|
|
|
import { boundaryCheck } from "./middlewares/boundary-check.ts";
|
|
|
|
|
import { ipBans } from "./middlewares/ip-bans.ts";
|
|
|
|
|
import { logger } from "./middlewares/logger.ts";
|
|
|
|
|
import { routes } from "./routes.ts";
|
|
|
|
|
import type { ApiRouteExports, HonoEnv } from "./types/api.ts";
|
2024-06-27 02:44:08 +02:00
|
|
|
|
2024-11-02 00:43:33 +01:00
|
|
|
export const appFactory = async (): Promise<OpenAPIHono<HonoEnv>> => {
|
2024-09-24 14:42:39 +02:00
|
|
|
await configureLoggers();
|
2024-06-27 02:44:08 +02:00
|
|
|
const serverLogger = getLogger("server");
|
|
|
|
|
|
2024-08-27 17:20:36 +02:00
|
|
|
const app = new OpenAPIHono<HonoEnv>({
|
2024-06-27 02:44:08 +02:00
|
|
|
strict: false,
|
2024-08-27 18:55:02 +02:00
|
|
|
defaultHook: handleZodError,
|
2024-06-27 02:44:08 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.use(ipBans);
|
|
|
|
|
app.use(agentBans);
|
|
|
|
|
app.use(logger);
|
|
|
|
|
app.use(boundaryCheck);
|
2024-08-19 21:17:25 +02:00
|
|
|
app.use(
|
2024-08-19 21:26:13 +02:00
|
|
|
"/api/*",
|
2024-08-19 21:17:25 +02:00
|
|
|
secureHeaders({
|
|
|
|
|
contentSecurityPolicy: {
|
|
|
|
|
// We will not be returning HTML, so everything should be blocked
|
|
|
|
|
defaultSrc: ["'none'"],
|
|
|
|
|
scriptSrc: ["'none'"],
|
|
|
|
|
styleSrc: ["'none'"],
|
|
|
|
|
imgSrc: ["'none'"],
|
|
|
|
|
connectSrc: ["'none'"],
|
|
|
|
|
fontSrc: ["'none'"],
|
|
|
|
|
objectSrc: ["'none'"],
|
|
|
|
|
mediaSrc: ["'none'"],
|
|
|
|
|
frameSrc: ["'none'"],
|
|
|
|
|
frameAncestors: ["'none'"],
|
|
|
|
|
baseUri: ["'none'"],
|
|
|
|
|
formAction: ["'none'"],
|
|
|
|
|
childSrc: ["'none'"],
|
|
|
|
|
workerSrc: ["'none'"],
|
|
|
|
|
manifestSrc: ["'none'"],
|
|
|
|
|
},
|
|
|
|
|
referrerPolicy: "no-referrer",
|
|
|
|
|
xFrameOptions: "DENY",
|
|
|
|
|
xContentTypeOptions: "nosniff",
|
|
|
|
|
crossOriginEmbedderPolicy: "require-corp",
|
|
|
|
|
crossOriginOpenerPolicy: "same-origin",
|
|
|
|
|
crossOriginResourcePolicy: "same-origin",
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
app.use(
|
|
|
|
|
prettyJSON({
|
|
|
|
|
space: 4,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
app.use(
|
|
|
|
|
cors({
|
|
|
|
|
origin: "*",
|
|
|
|
|
allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
|
|
|
credentials: true,
|
|
|
|
|
}),
|
|
|
|
|
);
|
2024-08-29 20:32:04 +02:00
|
|
|
app.use(
|
|
|
|
|
createMiddleware<HonoEnv>(async (context, next) => {
|
|
|
|
|
context.set("config", config);
|
|
|
|
|
|
|
|
|
|
await next();
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
2024-08-19 21:53:39 +02:00
|
|
|
/* app.use("*", registerMetrics);
|
|
|
|
|
app.get("/metrics", printMetrics); */
|
2024-06-27 02:44:08 +02:00
|
|
|
// Disabled as federation now checks for this
|
|
|
|
|
// app.use(urlCheck);
|
|
|
|
|
|
|
|
|
|
// Inject own filesystem router
|
|
|
|
|
for (const [, path] of Object.entries(routes)) {
|
|
|
|
|
// use app.get(path, handler) to add routes
|
|
|
|
|
const route: ApiRouteExports = await import(path);
|
|
|
|
|
|
2024-12-30 20:18:48 +01:00
|
|
|
if (!route.default) {
|
2025-01-02 01:29:33 +01:00
|
|
|
continue;
|
2024-06-27 02:44:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
route.default(app);
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-23 11:51:15 +02:00
|
|
|
serverLogger.info`Loading plugins`;
|
|
|
|
|
|
2024-11-10 13:08:26 +01:00
|
|
|
const time1 = performance.now();
|
|
|
|
|
|
2024-09-23 11:51:15 +02:00
|
|
|
const loader = new PluginLoader();
|
|
|
|
|
|
2024-10-06 15:55:15 +02:00
|
|
|
const plugins = await loader.loadPlugins(
|
|
|
|
|
join(process.cwd(), "plugins"),
|
2024-10-11 17:03:33 +02:00
|
|
|
config.plugins?.autoload ?? true,
|
2024-10-06 15:55:15 +02:00
|
|
|
config.plugins?.overrides.enabled,
|
|
|
|
|
config.plugins?.overrides.disabled,
|
|
|
|
|
);
|
2024-09-23 11:51:15 +02:00
|
|
|
|
2024-11-10 13:08:26 +01:00
|
|
|
await PluginLoader.addToApp(plugins, app, serverLogger);
|
2024-09-24 14:42:39 +02:00
|
|
|
|
2024-11-10 13:08:26 +01:00
|
|
|
const time2 = performance.now();
|
2024-09-23 11:51:15 +02:00
|
|
|
|
2024-11-10 13:08:26 +01:00
|
|
|
serverLogger.info`Plugins loaded in ${`${chalk.gray(
|
|
|
|
|
(time2 - time1).toFixed(2),
|
|
|
|
|
)}ms`}`;
|
2024-08-29 20:32:04 +02:00
|
|
|
|
2024-08-27 18:09:15 +02:00
|
|
|
app.doc31("/openapi.json", {
|
|
|
|
|
openapi: "3.1.0",
|
|
|
|
|
info: {
|
|
|
|
|
title: "Versia Server API",
|
|
|
|
|
version: pkg.version,
|
|
|
|
|
license: {
|
|
|
|
|
name: "AGPL-3.0",
|
|
|
|
|
url: "https://www.gnu.org/licenses/agpl-3.0.html",
|
|
|
|
|
},
|
|
|
|
|
contact: pkg.author,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
app.doc("/openapi.3.0.0.json", {
|
|
|
|
|
openapi: "3.0.0",
|
|
|
|
|
info: {
|
|
|
|
|
title: "Versia Server API",
|
|
|
|
|
version: pkg.version,
|
|
|
|
|
license: {
|
|
|
|
|
name: "AGPL-3.0",
|
|
|
|
|
url: "https://www.gnu.org/licenses/agpl-3.0.html",
|
|
|
|
|
},
|
|
|
|
|
contact: pkg.author,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
app.get("/docs", swaggerUI({ url: "/openapi.json" }));
|
2024-11-25 13:09:28 +01:00
|
|
|
applyToHono(app);
|
2024-08-27 18:09:15 +02:00
|
|
|
|
2024-08-19 21:53:39 +02:00
|
|
|
app.options("*", (context) => {
|
2024-12-30 16:18:28 +01:00
|
|
|
return context.body(null, 204);
|
2024-06-27 02:44:08 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.all("*", async (context) => {
|
|
|
|
|
const replacedUrl = new URL(
|
|
|
|
|
new URL(context.req.url).pathname,
|
|
|
|
|
config.frontend.url,
|
|
|
|
|
).toString();
|
|
|
|
|
|
|
|
|
|
serverLogger.debug`Proxying ${replacedUrl}`;
|
|
|
|
|
|
|
|
|
|
const proxy = await fetch(replacedUrl, {
|
|
|
|
|
headers: {
|
|
|
|
|
// Include for SSR
|
|
|
|
|
"X-Forwarded-Host": `${config.http.bind}:${config.http.bind_port}`,
|
|
|
|
|
"Accept-Encoding": "identity",
|
|
|
|
|
},
|
|
|
|
|
redirect: "manual",
|
|
|
|
|
}).catch((e) => {
|
|
|
|
|
serverLogger.error`${e}`;
|
2024-07-24 19:04:00 +02:00
|
|
|
sentry?.captureException(e);
|
2024-06-27 02:44:08 +02:00
|
|
|
serverLogger.error`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) {
|
2024-12-30 18:00:23 +01:00
|
|
|
throw new ApiError(
|
2024-06-27 02:44:08 +02:00
|
|
|
404,
|
2024-12-30 18:00:23 +01:00
|
|
|
"Route not found on proxy or API route. Are you using the correct HTTP method?",
|
2024-06-27 02:44:08 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Disable CSP upgrade-insecure-requests if an .onion domain is used
|
|
|
|
|
if (new URL(context.req.url).hostname.endsWith(".onion")) {
|
|
|
|
|
proxy.headers.set(
|
|
|
|
|
"Content-Security-Policy",
|
|
|
|
|
proxy.headers
|
|
|
|
|
.get("Content-Security-Policy")
|
|
|
|
|
?.replace("upgrade-insecure-requests;", "") ?? "",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return proxy;
|
|
|
|
|
});
|
|
|
|
|
|
2024-08-19 21:03:59 +02:00
|
|
|
app.onError((error, c) => {
|
2024-12-30 18:00:23 +01:00
|
|
|
if (error instanceof ApiError) {
|
|
|
|
|
return c.json(
|
|
|
|
|
{
|
|
|
|
|
error: error.message,
|
|
|
|
|
details: error.details,
|
|
|
|
|
},
|
|
|
|
|
error.status,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-24 17:19:23 +02:00
|
|
|
serverLogger.error`${error}`;
|
2025-03-16 17:01:07 +01:00
|
|
|
serverLogger.error`${inspect(error)}`;
|
2024-07-24 18:10:29 +02:00
|
|
|
sentry?.captureException(error);
|
2024-08-19 21:03:59 +02:00
|
|
|
return c.json(
|
2024-07-20 00:30:13 +02:00
|
|
|
{
|
|
|
|
|
error: "A server error occured",
|
|
|
|
|
name: error.name,
|
|
|
|
|
message: error.message,
|
|
|
|
|
},
|
|
|
|
|
500,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2024-06-27 02:44:08 +02:00
|
|
|
return app;
|
|
|
|
|
};
|
2024-07-11 12:56:28 +02:00
|
|
|
|
|
|
|
|
export type App = Awaited<ReturnType<typeof appFactory>>;
|