1 |
/** |
2 |
* This file is part of SudoBot. |
3 |
* |
4 |
* Copyright (C) 2021-2023 OSN Developers. |
5 |
* |
6 |
* SudoBot is free software; you can redistribute it and/or modify it |
7 |
* under the terms of the GNU Affero General Public License as published by |
8 |
* the Free Software Foundation, either version 3 of the License, or |
9 |
* (at your option) any later version. |
10 |
* |
11 |
* SudoBot is distributed in the hope that it will be useful, but |
12 |
* WITHOUT ANY WARRANTY; without even the implied warranty of |
13 |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
14 |
* GNU Affero General Public License for more details. |
15 |
* |
16 |
* You should have received a copy of the GNU Affero General Public License |
17 |
* along with SudoBot. If not, see <https://www.gnu.org/licenses/>. |
18 |
*/ |
19 |
|
20 |
import cors from "cors"; |
21 |
import express, { |
22 |
Application, |
23 |
Request as ExpressRequest, |
24 |
Response as ExpressResponse, |
25 |
NextFunction |
26 |
} from "express"; |
27 |
import ratelimiter from "express-rate-limit"; |
28 |
import { Router } from "express-serve-static-core"; |
29 |
import { Server as HttpServer } from "http"; |
30 |
import Client from "../core/Client"; |
31 |
import { RouteMetadata, RouteMetadataEntry } from "../types/RouteMetadata"; |
32 |
import Controller from "./Controller"; |
33 |
import Response from "./Response"; |
34 |
|
35 |
export default class Server { |
36 |
protected readonly expressApp = express(); |
37 |
protected readonly rateLimiter = ratelimiter({ |
38 |
windowMs: 30 * 1000, |
39 |
max: 28, |
40 |
standardHeaders: true, |
41 |
legacyHeaders: false |
42 |
}); |
43 |
protected readonly configRateLimiter = ratelimiter({ |
44 |
windowMs: 10 * 1000, |
45 |
max: 7, |
46 |
standardHeaders: true, |
47 |
legacyHeaders: false |
48 |
}); |
49 |
public readonly port = process.env.PORT ?? 4000; |
50 |
public expressServer?: HttpServer; |
51 |
|
52 |
constructor(protected readonly client: Client) {} |
53 |
|
54 |
async onReady() { |
55 |
if (this.client.configManager.systemConfig.api.enabled) { |
56 |
await this.boot(); |
57 |
await this.start(); |
58 |
} |
59 |
} |
60 |
|
61 |
async boot() { |
62 |
this.expressApp.use(this.onError); |
63 |
this.expressApp.use(cors()); |
64 |
|
65 |
if (this.client.configManager.systemConfig.trust_proxies !== undefined) { |
66 |
this.client.logger.info( |
67 |
"Set express trust proxy option value to ", |
68 |
this.client.configManager.systemConfig.trust_proxies |
69 |
); |
70 |
|
71 |
this.expressApp.set( |
72 |
"trust proxy", |
73 |
this.client.configManager.systemConfig.trust_proxies |
74 |
); |
75 |
} |
76 |
|
77 |
this.expressApp.use(this.rateLimiter); |
78 |
this.expressApp.use("/config", this.configRateLimiter); |
79 |
this.expressApp.use(express.json()); |
80 |
|
81 |
const router = await this.createRouter(); |
82 |
this.expressApp.use("/", router); |
83 |
} |
84 |
|
85 |
async createRouter() { |
86 |
const router = express.Router(); |
87 |
await this.client.dynamicLoader.loadControllers(router); |
88 |
return router; |
89 |
} |
90 |
|
91 |
loadController(controller: Controller, controllerClass: typeof Controller, router: Router) { |
92 |
const actions = ( |
93 |
Symbol.metadata in controllerClass && controllerClass[Symbol.metadata] |
94 |
? controllerClass[Symbol.metadata]?.actionMethods |
95 |
: Reflect.getMetadata("action_methods", controllerClass.prototype) |
96 |
) as Record<string, RouteMetadata> | null; |
97 |
const aacMiddlewareList = ( |
98 |
Symbol.metadata in controllerClass && controllerClass[Symbol.metadata] |
99 |
? controllerClass[Symbol.metadata]?.adminAccessControlMiddleware |
100 |
: Reflect.getMetadata("aac_middleware", controllerClass.prototype) |
101 |
) as Record<string, (...args: unknown[]) => unknown> | null; |
102 |
const gacMiddlewareList = ( |
103 |
Symbol.metadata in controllerClass && controllerClass[Symbol.metadata] |
104 |
? controllerClass[Symbol.metadata]?.guildAccessControlMiddleware |
105 |
: Reflect.getMetadata("gac_middleware", controllerClass.prototype) |
106 |
) as Record<string, (...args: unknown[]) => unknown> | null; |
107 |
const requireAuthMiddlewareList = ( |
108 |
Symbol.metadata in controllerClass && controllerClass[Symbol.metadata] |
109 |
? controllerClass[Symbol.metadata]?.authMiddleware |
110 |
: Reflect.getMetadata("auth_middleware", controllerClass.prototype) |
111 |
) as Record<string, (...args: unknown[]) => unknown> | null; |
112 |
const validationMiddlewareList = ( |
113 |
Symbol.metadata in controllerClass && controllerClass[Symbol.metadata] |
114 |
? controllerClass[Symbol.metadata]?.validationMiddleware |
115 |
: Reflect.getMetadata("validation_middleware", controllerClass.prototype) |
116 |
) as Record<string, (...args: unknown[]) => unknown> | null; |
117 |
|
118 |
if (!actions) { |
119 |
return; |
120 |
} |
121 |
|
122 |
for (const callbackName in actions) { |
123 |
for (const method in actions[callbackName]) { |
124 |
const data = actions[callbackName][ |
125 |
method as keyof (typeof actions)[keyof typeof actions] |
126 |
] as RouteMetadataEntry | null; |
127 |
|
128 |
if (!data) { |
129 |
continue; |
130 |
} |
131 |
|
132 |
const middleware = []; |
133 |
|
134 |
if (requireAuthMiddlewareList?.[callbackName]) { |
135 |
middleware.push(requireAuthMiddlewareList?.[callbackName]); |
136 |
} |
137 |
|
138 |
if (aacMiddlewareList?.[callbackName]) { |
139 |
middleware.push(aacMiddlewareList?.[callbackName]); |
140 |
} |
141 |
|
142 |
if (gacMiddlewareList?.[callbackName]) { |
143 |
middleware.push(gacMiddlewareList?.[callbackName]); |
144 |
} |
145 |
|
146 |
if (validationMiddlewareList?.[callbackName]) { |
147 |
middleware.push(validationMiddlewareList?.[callbackName]); |
148 |
} |
149 |
|
150 |
middleware.push(...(data.middleware ?? [])); |
151 |
|
152 |
const wrappedMiddleware = middleware.map( |
153 |
m => (request: ExpressRequest, response: ExpressResponse, next: NextFunction) => |
154 |
m(this.client, request, response, next) |
155 |
); |
156 |
|
157 |
router[data.method.toLowerCase() as "head"].call( |
158 |
router, |
159 |
data.path, |
160 |
...(wrappedMiddleware as []), |
161 |
<Application>this.wrapControllerAction(controller, callbackName) |
162 |
); |
163 |
|
164 |
this.client.logger.debug( |
165 |
`Discovered API Route: ${data.method} ${data.path} -- in ${controllerClass.name}` |
166 |
); |
167 |
} |
168 |
} |
169 |
} |
170 |
|
171 |
wrapControllerAction(controller: Controller, callbackName: string) { |
172 |
return async (request: ExpressRequest, response: ExpressResponse) => { |
173 |
const callback = controller[callbackName as keyof typeof controller] as ( |
174 |
request: ExpressRequest, |
175 |
response: ExpressResponse |
176 |
) => unknown; |
177 |
const controllerResponse = await callback.call(controller, request, response); |
178 |
|
179 |
if (!response.headersSent) { |
180 |
if (controllerResponse instanceof Response) { |
181 |
controllerResponse.send(response); |
182 |
} else if (controllerResponse && typeof controllerResponse === "object") { |
183 |
response.json(controllerResponse); |
184 |
} else if (typeof controllerResponse === "string") { |
185 |
response.send(controllerResponse); |
186 |
} else if (typeof controllerResponse === "number") { |
187 |
response.send(controllerResponse.toString()); |
188 |
} else { |
189 |
this.client.logger.warn( |
190 |
"Invalid value was returned from the controller. Not sending a response." |
191 |
); |
192 |
} |
193 |
} |
194 |
}; |
195 |
} |
196 |
|
197 |
onError(err: unknown, _: ExpressRequest, res: ExpressResponse, next: NextFunction) { |
198 |
if (err instanceof SyntaxError && "status" in err && err.status === 400 && "body" in err) { |
199 |
res.status(400).json({ |
200 |
error: "Invalid JSON payload" |
201 |
}); |
202 |
|
203 |
return; |
204 |
} |
205 |
|
206 |
next(); |
207 |
} |
208 |
|
209 |
async start() { |
210 |
this.expressServer = this.expressApp.listen(this.port, () => |
211 |
this.client.logger.info(`API server is listening at port ${this.port}`) |
212 |
); |
213 |
} |
214 |
} |