diff --git a/api/src/unraid-api/app/cors.ts b/api/src/unraid-api/app/cors.ts new file mode 100644 index 000000000..6daa2e7c6 --- /dev/null +++ b/api/src/unraid-api/app/cors.ts @@ -0,0 +1,78 @@ +import { type NestFastifyApplication } from '@nestjs/platform-fastify'; +import { type CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface'; +import { apiLogger } from '@app/core/log'; +import { getAllowedOrigins } from '@app/common/allowed-origins'; +import { BYPASS_PERMISSION_CHECKS } from '@app/environment'; +import { GraphQLError } from 'graphql'; +import { CookieService } from '../auth/cookie.service'; + +/** + * Returns whether the origin is allowed to access the API. + * + * @throws GraphQLError if the origin is not in the list of allowed origins + * and `BYPASS_PERMISSION_CHECKS` flag is not set. + */ +// note: don't make this function synchronous. throwing will then crash the server. +export async function isOriginAllowed(origin: string | undefined) { + const allowedOrigins = getAllowedOrigins(); + if (origin && allowedOrigins.includes(origin)) { + return true; + } else { + apiLogger.debug(`Origin not in allowed origins: ${origin}`); + + if (BYPASS_PERMISSION_CHECKS) { + return true; + } + + throw new GraphQLError( + 'The CORS policy for this site does not allow access from the specified Origin.' + ); + } +} + +/** + * Dynamically determines the CORS config for a request. + * + * - Expects any cookies to be parsed & available on the `cookies` property of the request. + * + * If the request contains a valid unraid session cookie, it is allowed to access + * the API from any origin. Otherwise, the origin must be explicitly listed in + * the `allowedOrigins` config option, or the `BYPASS_PERMISSION_CHECKS` flag + * must be set. + * + * @param req the request object + * @param callback the callback to call with the CORS options + */ +function dynamicCors(req: any, callback: (error: Error | null, options: CorsOptions) => void) { + const { cookies } = req; + const service = new CookieService(); + if (typeof cookies === 'object') { + service.hasValidAuthCookie(cookies).then((isValid) => { + if (isValid) { + callback(null, { origin: true }); + } else { + callback(null, { origin: isOriginAllowed }); + } + }); + } else { + callback(null, { origin: isOriginAllowed }); + } +} + +/**------------------------------------------------------------------------ + * ? Fastify Cors Config + * + * The fastify cors configuration function is very different from express, + * but Nest.js doesn't have clear docs or types describing this so I'm + * documenting it here. + * + * This takes a fastify app instance and returns a cors config function, instead + * of just the cors config function (which is nest's default behavior). + *------------------------------------------------------------------------**/ + +/** A wrapper function for the fastify CORS configuration. */ + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function configureFastifyCors(app: NestFastifyApplication) { + return dynamicCors; +} diff --git a/api/src/unraid-api/main.ts b/api/src/unraid-api/main.ts index 0271008ee..a4808007c 100644 --- a/api/src/unraid-api/main.ts +++ b/api/src/unraid-api/main.ts @@ -3,34 +3,15 @@ import { LoggerErrorInterceptor, Logger as PinoLogger } from 'nestjs-pino'; import { AppModule } from './app/app.module'; import Fastify from 'fastify'; import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify'; -import { type CorsOptionsDelegate } from 'cors'; -import { getAllowedOrigins } from '@app/common/allowed-origins'; import { HttpExceptionFilter } from '@app/unraid-api/exceptions/http-exceptions.filter'; -import { GraphQLError } from 'graphql'; import { GraphQLExceptionsFilter } from '@app/unraid-api/exceptions/graphql-exceptions.filter'; -import { BYPASS_PERMISSION_CHECKS, PORT } from '@app/environment'; +import { PORT } from '@app/environment'; import { type FastifyInstance } from 'fastify'; import { type Server, type IncomingMessage, type ServerResponse } from 'http'; import { apiLogger } from '@app/core/log'; -import cookieParser from 'cookie-parser'; - -export const corsOptionsDelegate: CorsOptionsDelegate = async (origin: string | undefined) => { - const allowedOrigins = getAllowedOrigins(); - if (origin && allowedOrigins.includes(origin)) { - return true; - } else { - apiLogger.debug(`Origin not in allowed origins: ${origin}`); - - if (BYPASS_PERMISSION_CHECKS) { - return true; - } - - throw new GraphQLError( - 'The CORS policy for this site does not allow access from the specified Origin.' - ); - } -}; +import fastifyCookie from '@fastify/cookie'; +import { configureFastifyCors } from './app/cors'; export async function bootstrapNestServer(): Promise { const server: FastifyInstance = Fastify({ @@ -38,11 +19,11 @@ export async function bootstrapNestServer(): Promise { }); const app = await NestFactory.create(AppModule, new FastifyAdapter(server), { - cors: { origin: corsOptionsDelegate }, bufferLogs: true, }); - app.use(cookieParser()); + app.register(fastifyCookie); // parse cookies before cors + app.enableCors(configureFastifyCors); // Setup Nestjs Pino Logger app.useLogger(app.get(PinoLogger));