Skip to content

Commit a699049

Browse files
farhanW3adam-maj
andauthored
ACCESS_CONTROL_ALLOW_ORIGIN into Config (#372)
* Add try/catch to webhook request * Add manual webhook status * Fix repeat webhooks * added CORS config to DB * dynamic loading of cors urls * uncommented commented out data * updated getConfiguration() to have accessControlAllowOrigin field with default values onCreate * Updatd Implementation to now send Error Webhook * updated input to stringp[] --------- Co-authored-by: Adam Majmudar <mr.adam.maj@gmail.com> Co-authored-by: Adam Majmudar <64697628+adam-maj@users.noreply.github.com>
1 parent 57fe1f9 commit a699049

File tree

17 files changed

+664
-14
lines changed

17 files changed

+664
-14
lines changed

.env.example

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@ ADMIN_WALLET_ADDRESS="<your-admin-wallet-address>"
1313
# PORT="3005"
1414
# HOST="0.0.0.0"
1515

16-
# Optional configuration to enable cors, defaults to allow all
17-
# ACCESS_CONTROL_ALLOW_ORIGIN="*"
18-
1916
# Optional configuration to enable https usage for localhost
2017
# ENABLE_HTTPS="false"
2118
# HTTPS_PASSPHRASE="..."

src/db/configuration/getConfiguration.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Configuration } from "@prisma/client";
22
import { LocalWallet } from "@thirdweb-dev/wallets";
33
import { WalletType } from "../../schema/wallet";
4+
import { mandatoryAllowedCorsUrls } from "../../server/utils/cors-urls";
45
import { decrypt } from "../../utils/crypto";
56
import { env } from "../../utils/env";
67
import { logger } from "../../utils/logger";
@@ -184,6 +185,7 @@ export const getConfiguration = async (): Promise<Config> => {
184185
authDomain: "thirdweb.com",
185186
authWalletEncryptedJson: await createAuthWalletEncryptedJson(),
186187
minWalletBalance: "20000000000000000",
188+
accessControlAllowOrigin: mandatoryAllowedCorsUrls.join(","),
187189
},
188190
update: {},
189191
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "configuration" ADD COLUMN "accessControlAllowOrigin" TEXT NOT NULL DEFAULT 'https://thirdweb.com,https://embed.ipfscdn.io';

src/prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ model Configuration {
4141
webhookAuthBearerToken String? @map("webhookAuthBearerToken")
4242
// Wallet balance
4343
minWalletBalance String @default("20000000000000000") @map("minWalletBalance")
44+
accessControlAllowOrigin String @default("https://thirdweb.com,https://embed.ipfscdn.io") @map("accessControlAllowOrigin")
4445
4546
@@map("configuration")
4647
}

src/server/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ const main = async () => {
4949
...(env.ENABLE_HTTPS ? httpsObject : {}),
5050
}).withTypeProvider<TypeBoxTypeProvider>();
5151

52+
server.decorateRequest("corsPreflightEnabled", false);
53+
5254
await withCors(server);
5355
await withRequestLogs(server);
5456
await withErrorHandler(server);

src/server/middleware/auth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ export const withAuth = async (server: FastifyInstance) => {
167167
server.decorateRequest("user", null);
168168

169169
// Add auth validation middleware to check for authenticated requests
170-
server.addHook("onRequest", async (req, res) => {
170+
server.addHook("preHandler", async (req, res) => {
171171
if (
172172
req.url === "/favicon.ico" ||
173173
req.url === "/" ||

src/server/middleware/cors/cors.ts

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
import {
2+
FastifyInstance,
3+
FastifyReply,
4+
FastifyRequest,
5+
HookHandlerDoneFunction,
6+
} from "fastify";
7+
import { getConfig } from "../../../utils/cache/getConfig";
8+
import {
9+
addAccessControlRequestHeadersToVaryHeader,
10+
addOriginToVaryHeader,
11+
} from "./vary";
12+
13+
interface ArrayOfValueOrArray<T> extends Array<ValueOrArray<T>> {}
14+
15+
type OriginCallback = (
16+
err: Error | null,
17+
origin: ValueOrArray<OriginType>,
18+
) => void;
19+
type OriginType = string | boolean | RegExp;
20+
type ValueOrArray<T> = T | ArrayOfValueOrArray<T>;
21+
type OriginFunction = (
22+
origin: string | undefined,
23+
callback: OriginCallback,
24+
) => void;
25+
26+
interface FastifyCorsOptions {
27+
/**
28+
* Configures the Access-Control-Allow-Origin CORS header.
29+
*/
30+
origin?: ValueOrArray<OriginType> | OriginFunction;
31+
/**
32+
* Configures the Access-Control-Allow-Credentials CORS header.
33+
* Set to true to pass the header, otherwise it is omitted.
34+
*/
35+
credentials?: boolean;
36+
/**
37+
* Configures the Access-Control-Expose-Headers CORS header.
38+
* Expects a comma-delimited string (ex: 'Content-Range,X-Content-Range')
39+
* or an array (ex: ['Content-Range', 'X-Content-Range']).
40+
* If not specified, no custom headers are exposed.
41+
*/
42+
exposedHeaders?: string | string[];
43+
/**
44+
* Configures the Access-Control-Allow-Headers CORS header.
45+
* Expects a comma-delimited string (ex: 'Content-Type,Authorization')
46+
* or an array (ex: ['Content-Type', 'Authorization']). If not
47+
* specified, defaults to reflecting the headers specified in the
48+
* request's Access-Control-Request-Headers header.
49+
*/
50+
allowedHeaders?: string | string[];
51+
/**
52+
* Configures the Access-Control-Allow-Methods CORS header.
53+
* Expects a comma-delimited string (ex: 'GET,PUT,POST') or an array (ex: ['GET', 'PUT', 'POST']).
54+
*/
55+
methods?: string | string[];
56+
/**
57+
* Configures the Access-Control-Max-Age CORS header.
58+
* Set to an integer to pass the header, otherwise it is omitted.
59+
*/
60+
maxAge?: number;
61+
/**
62+
* Configures the Cache-Control header for CORS preflight responses.
63+
* Set to an integer to pass the header as `Cache-Control: max-age=${cacheControl}`,
64+
* or set to a string to pass the header as `Cache-Control: ${cacheControl}` (fully define
65+
* the header value), otherwise the header is omitted.
66+
*/
67+
cacheControl?: number | string | null;
68+
/**
69+
* Pass the CORS preflight response to the route handler (default: false).
70+
*/
71+
preflightContinue?: boolean;
72+
/**
73+
* Provides a status code to use for successful OPTIONS requests,
74+
* since some legacy browsers (IE11, various SmartTVs) choke on 204.
75+
*/
76+
optionsSuccessStatus?: number;
77+
/**
78+
* Pass the CORS preflight response to the route handler (default: false).
79+
*/
80+
preflight?: boolean;
81+
/**
82+
* Enforces strict requirement of the CORS preflight request headers (Access-Control-Request-Method and Origin).
83+
* Preflight requests without the required headers will result in 400 errors when set to `true` (default: `true`).
84+
*/
85+
strictPreflight?: boolean;
86+
/**
87+
* Hide options route from the documentation built using fastify-swagger (default: true).
88+
*/
89+
hideOptionsRoute?: boolean;
90+
}
91+
92+
const defaultOptions = {
93+
origin: "*",
94+
methods: "GET,HEAD,PUT,PATCH,POST,DELETE",
95+
preflightContinue: false,
96+
optionsSuccessStatus: 204,
97+
credentials: false,
98+
exposedHeaders: undefined,
99+
allowedHeaders: undefined,
100+
maxAge: undefined,
101+
preflight: true,
102+
strictPreflight: true,
103+
};
104+
105+
export const fastifyCors = async (
106+
fastify: FastifyInstance,
107+
req: FastifyRequest,
108+
reply: FastifyReply,
109+
opts: FastifyCorsOptions,
110+
next: HookHandlerDoneFunction,
111+
) => {
112+
const config = await getConfig();
113+
114+
const originArray = config.accessControlAllowOrigin.split(",") as string[];
115+
opts.origin = originArray;
116+
117+
let hideOptionsRoute = true;
118+
if (opts.hideOptionsRoute !== undefined) {
119+
hideOptionsRoute = opts.hideOptionsRoute;
120+
}
121+
const corsOptions = normalizeCorsOptions(opts);
122+
addCorsHeadersHandler(fastify, corsOptions, req, reply, next);
123+
124+
next();
125+
};
126+
127+
function normalizeCorsOptions(opts: FastifyCorsOptions) {
128+
const corsOptions = { ...defaultOptions, ...opts };
129+
if (Array.isArray(opts.origin) && opts.origin.indexOf("*") !== -1) {
130+
corsOptions.origin = "*";
131+
}
132+
if (Number.isInteger(corsOptions.cacheControl)) {
133+
// integer numbers are formatted this way
134+
corsOptions.cacheControl = `max-age=${corsOptions.cacheControl}`;
135+
} else if (typeof corsOptions.cacheControl !== "string") {
136+
// strings are applied directly and any other value is ignored
137+
corsOptions.cacheControl = undefined;
138+
}
139+
return corsOptions;
140+
}
141+
142+
const addCorsHeadersHandler = (
143+
fastify: FastifyInstance,
144+
options: FastifyCorsOptions,
145+
req: FastifyRequest,
146+
reply: FastifyReply,
147+
next: HookHandlerDoneFunction,
148+
) => {
149+
// Always set Vary header
150+
// https://github.com/rs/cors/issues/10
151+
addOriginToVaryHeader(reply);
152+
153+
const resolveOriginOption =
154+
typeof options.origin === "function"
155+
? resolveOriginWrapper(fastify, options.origin)
156+
: (_: any, cb: any) => cb(null, options.origin);
157+
158+
resolveOriginOption(
159+
req,
160+
(error: Error | null, resolvedOriginOption: boolean) => {
161+
if (error !== null) {
162+
return next(error);
163+
}
164+
165+
// Disable CORS and preflight if false
166+
if (resolvedOriginOption === false) {
167+
return next();
168+
}
169+
170+
// Falsy values are invalid
171+
if (!resolvedOriginOption) {
172+
return next(new Error("Invalid CORS origin option"));
173+
}
174+
175+
addCorsHeaders(req, reply, resolvedOriginOption, options);
176+
177+
if (req.raw.method === "OPTIONS" && options.preflight === true) {
178+
// Strict mode enforces the required headers for preflight
179+
if (
180+
options.strictPreflight === true &&
181+
(!req.headers.origin || !req.headers["access-control-request-method"])
182+
) {
183+
reply
184+
.status(400)
185+
.type("text/plain")
186+
.send("Invalid Preflight Request");
187+
return;
188+
}
189+
190+
req.corsPreflightEnabled = true;
191+
192+
addPreflightHeaders(req, reply, options);
193+
194+
if (!options.preflightContinue) {
195+
// Do not call the hook callback and terminate the request
196+
// Safari (and potentially other browsers) need content-length 0,
197+
// for 204 or they just hang waiting for a body
198+
reply
199+
.code(options.optionsSuccessStatus!)
200+
.header("Content-Length", "0")
201+
.send();
202+
return;
203+
}
204+
}
205+
206+
return next();
207+
},
208+
);
209+
};
210+
211+
const addCorsHeaders = (
212+
req: FastifyRequest,
213+
reply: FastifyReply,
214+
originOption: any,
215+
corsOptions: FastifyCorsOptions,
216+
) => {
217+
const origin = getAccessControlAllowOriginHeader(
218+
req.headers.origin!,
219+
originOption,
220+
);
221+
222+
// In the case of origin not allowed the header is not
223+
// written in the response.
224+
// https://github.com/fastify/fastify-cors/issues/127
225+
if (origin) {
226+
reply.header("Access-Control-Allow-Origin", origin);
227+
}
228+
229+
if (corsOptions.credentials) {
230+
reply.header("Access-Control-Allow-Credentials", "true");
231+
}
232+
233+
if (corsOptions.exposedHeaders !== null) {
234+
reply.header(
235+
"Access-Control-Expose-Headers",
236+
Array.isArray(corsOptions.exposedHeaders)
237+
? corsOptions.exposedHeaders.join(", ")
238+
: corsOptions.exposedHeaders,
239+
);
240+
}
241+
};
242+
243+
function addPreflightHeaders(
244+
req: FastifyRequest,
245+
reply: FastifyReply,
246+
corsOptions: FastifyCorsOptions,
247+
) {
248+
reply.header(
249+
"Access-Control-Allow-Methods",
250+
Array.isArray(corsOptions.methods)
251+
? corsOptions.methods.join(", ")
252+
: corsOptions.methods,
253+
);
254+
255+
if (!corsOptions.allowedHeaders) {
256+
addAccessControlRequestHeadersToVaryHeader(reply);
257+
const reqAllowedHeaders = req.headers["access-control-request-headers"];
258+
if (reqAllowedHeaders !== undefined) {
259+
reply.header("Access-Control-Allow-Headers", reqAllowedHeaders);
260+
}
261+
} else {
262+
reply.header(
263+
"Access-Control-Allow-Headers",
264+
Array.isArray(corsOptions.allowedHeaders)
265+
? corsOptions.allowedHeaders.join(", ")
266+
: corsOptions.allowedHeaders,
267+
);
268+
}
269+
270+
if (corsOptions.maxAge !== null) {
271+
reply.header("Access-Control-Max-Age", String(corsOptions.maxAge));
272+
}
273+
274+
if (corsOptions.cacheControl) {
275+
reply.header("Cache-Control", corsOptions.cacheControl);
276+
}
277+
}
278+
279+
const resolveOriginWrapper = (fastify: FastifyInstance, origin: any) => {
280+
return (req: FastifyRequest, cb: any) => {
281+
const result = origin.call(fastify, req.headers.origin, cb);
282+
283+
// Allow for promises
284+
if (result && typeof result.then === "function") {
285+
result.then((res: any) => cb(null, res), cb);
286+
}
287+
};
288+
};
289+
290+
const getAccessControlAllowOriginHeader = (
291+
reqOrigin: string | undefined,
292+
originOption: string,
293+
) => {
294+
if (originOption === "*") {
295+
// allow any origin
296+
return "*";
297+
}
298+
299+
if (typeof originOption === "string") {
300+
// fixed origin
301+
return originOption;
302+
}
303+
304+
// reflect origin
305+
return isRequestOriginAllowed(reqOrigin, originOption) ? reqOrigin : false;
306+
};
307+
308+
const isRequestOriginAllowed = (
309+
reqOrigin: string | undefined,
310+
allowedOrigin: string | RegExp,
311+
) => {
312+
if (Array.isArray(allowedOrigin)) {
313+
for (let i = 0; i < allowedOrigin.length; ++i) {
314+
if (isRequestOriginAllowed(reqOrigin, allowedOrigin[i])) {
315+
return true;
316+
}
317+
}
318+
return false;
319+
} else if (typeof allowedOrigin === "string") {
320+
return reqOrigin === allowedOrigin;
321+
} else if (allowedOrigin instanceof RegExp && reqOrigin) {
322+
allowedOrigin.lastIndex = 0;
323+
return allowedOrigin.test(reqOrigin);
324+
} else {
325+
return !!allowedOrigin;
326+
}
327+
};

0 commit comments

Comments
 (0)