Explorar o código

ref: minor refactor & comments

Sv443 hai 4 meses
pai
achega
1d3bebcac4
Modificáronse 4 ficheiros con 79 adicións e 38 borrados
  1. 2 0
      .env.template
  2. 52 28
      src/server.ts
  3. 13 1
      src/types.ts
  4. 12 9
      src/utils.ts

+ 2 - 0
.env.template

@@ -5,6 +5,8 @@ NODE_ENV=development
 HTTP_PORT=8074
 # Defaults to 0.0.0.0 (listen on all interfaces)
 HTTP_HOST=
+# Whether to trust upstream (reverse) proxies like nginx
+TRUST_PROXY=true
 
 # Defaults to true - whether to serve the documentation page
 HOST_HOMEPAGE=true

+ 52 - 28
src/server.ts

@@ -20,15 +20,18 @@ app.use(cors({
   methods: "GET,HEAD,OPTIONS",
   origin: "*",
 }));
+
 app.use(helmet({ 
   dnsPrefetchControl: true,
 }));
+
 app.use(compression({
   threshold: 256
 }));
+
 app.use(express.json());
 
-if(env.NODE_ENV?.toLowerCase() === "production")
+if(env.TRUST_PROXY?.toLowerCase() === "true")
   app.enable("trust proxy");
 
 app.disable("x-powered-by");
@@ -59,32 +62,51 @@ export async function init() {
   // rate limiting
   app.use(async (req, res, next) => {
     const fmt = req?.query?.format ? String(req.query.format) : undefined;
-    const { authorization } = req.headers;
-    const authHeader = authorization?.startsWith("Bearer ") ? authorization.substring(7) : authorization;
-
-    res.setHeader("API-Info", `geniURL v${packageJson.version} (${packageJson.homepage})`);
-
-    if(authHeader && authTokens.has(authHeader))
-      return next();
-
-    const ipHash = await hashStr(getClientIp(req) ?? "IP_RESOLUTION_ERROR");
-
-    if(rlIgnorePaths.every((path) => !req.path.match(new RegExp(`^(/?v\\d+)?${path}`)))) {
-      rateLimiter.consume(ipHash)
-        .then((rateLimiterRes: RateLimiterRes) => {
-          setRateLimitHeaders(res, rateLimiterRes);
-          return next();
-        })
-        .catch((err) => {
-          if(err instanceof RateLimiterRes) {
-            setRateLimitHeaders(res, err);
-            return respond(res, 429, { error: true, matches: null, message: "You are being rate limited. Refer to the Retry-After header for when to try again." }, fmt);
-          }
-          else return respond(res, 500, { message: "Encountered an internal error. Please try again a little later." }, fmt);
-        });
+    try {
+      const { authorization } = req.headers;
+      const authHeader = authorization?.trim().replace(/^Bearer\s+/i, "");
+
+      res.setHeader("API-Info", `geniURL v${packageJson.version} (${packageJson.homepage})`);
+      res.setHeader("API-Version", packageJson.version);
+
+      if(authHeader && authTokens.has(authHeader))
+        return next();
+
+      const ipHash = await hashStr(getClientIp(req) ?? "IP_RESOLUTION_ERROR");
+
+      if(rlIgnorePaths.every((path) => !req.path.match(new RegExp(`^(/?v\\d+)?${path}`)))) {
+        rateLimiter.consume(ipHash)
+          .then((rateLimiterRes: RateLimiterRes) => {
+            setRateLimitHeaders(res, rateLimiterRes);
+            return next();
+          })
+          .catch((err) => {
+            if(err instanceof RateLimiterRes) {
+              setRateLimitHeaders(res, err);
+              return respond(res, 429, {
+                error: true,
+                matches: null,
+                message: "You are being rate limited. Refer to the Retry-After header for when to try again."
+              }, fmt);
+            }
+            else
+              return respond(res, 500, {
+                error: true,
+                matches: null,
+                message: `Encountered an internal error${e instanceof Error ? `: ${err.message}` : ""}. Please try again a little later.`,
+              }, fmt);
+          });
+      }
+      else
+        return next();
+    }
+    catch(e) {
+      return respond(res, "serverError", {
+        error: true,
+        matches: null,
+        message: `Encountered an internal error while applying rate limiting and checking for authorization${e instanceof Error ? `: ${e.message}` : ""}`
+      }, fmt);
     }
-    else
-      return next();
   });
 
   const listener = app.listen(port, host, () => {
@@ -105,8 +127,9 @@ function registerRoutes() {
   }
 }
 
+/** Returns all auth tokens as a set of strings */
 function getAuthTokens() {
-  const envVal = process.env["AUTH_TOKENS"];
+  const envVal = env.AUTH_TOKENS;
   let tokens: string[] = [];
 
   if(!envVal || envVal.length === 0)
@@ -117,7 +140,8 @@ function getAuthTokens() {
   return new Set<string>(tokens);
 }
 
-function setRateLimitHeaders (res: Response, rateLimiterRes: RateLimiterRes) {
+/** Sets all rate-limiting related headers on a response given a RateLimiterRes object */
+function setRateLimitHeaders(res: Response, rateLimiterRes: RateLimiterRes) {
   if(rateLimiterRes.remainingPoints === 0)
     res.setHeader("Retry-After", Math.ceil(rateLimiterRes.msBeforeNext / 1000));
   res.setHeader("X-RateLimit-Limit", rateLimiter.points);

+ 13 - 1
src/types.ts

@@ -1,12 +1,15 @@
 //#region server
 
+/** Successful or errored response object */
 export type ServerResponse<T> = SuccessResponse<T> | ErrorResponse;
 
+/** Successful response object */
 export type SuccessResponse<T> = {
   error: false;
   matches: number;
 } & T;
 
+/** Errored response object */
 export type ErrorResponse = {
   error: true;
   matches: 0 | null;
@@ -15,6 +18,7 @@ export type ErrorResponse = {
 
 //#region meta
 
+/** genius.com artist object */
 interface Artist {
   name: string | null;
   url: string | null;
@@ -22,7 +26,7 @@ interface Artist {
   headerImage: string | null;
 }
 
-/** geniURL song meta object */
+/** genius.com song meta object */
 export interface SongMeta {
   url: string;
   path: string;
@@ -48,6 +52,7 @@ export interface SongMeta {
 
 export type MetaSearchHit = SongMeta & { uuid?: string; };
 
+/** Arguments passed to the getMeta() function */
 export interface GetMetaArgs {
   q?: string;
   artist?: string;
@@ -61,6 +66,7 @@ export type ScoredResults<T> = {
   result: T;
 };
 
+/** Resulting object from calling getMeta() */
 export interface GetMetaResult {
   top: SongMeta;
   all: SongMeta[];
@@ -68,6 +74,7 @@ export interface GetMetaResult {
 
 //#region translations
 
+/** genius.com translation object */
 export interface SongTranslation {
   language: string;
   id: number;
@@ -78,8 +85,10 @@ export interface SongTranslation {
 
 //#region server
 
+/** geniURL response type */
 export type ResponseType = "serverError" | "clientError" | "success";
 
+/** geniURL response file format */
 export type ResponseFormat = "json" | "xml";
 
 //#region API
@@ -133,6 +142,7 @@ export type SongObj = SongBaseObj & {
   }[];
 };
 
+/** Base object returned by the songs endpoints of the genius API */
 type SongBaseObj = {
   api_path: string;
   artist_names: string;
@@ -154,6 +164,7 @@ type SongBaseObj = {
   url: string;
 };
 
+/** Artist object returned by the genius API */
 type ArtistObj = {
   api_path: string;
   header_image_url: string;
@@ -163,6 +174,7 @@ type ArtistObj = {
   url: string;
 }
 
+/** Album object returned by the genius API */
 export interface Album {
   name: string;
   fullTitle: string;

+ 12 - 9
src/utils.ts

@@ -73,16 +73,17 @@ export function respond(res: Response, type: ResponseType | number, data: String
   res.status(statusCode).send(finalData);
 }
 
-export function redirectToDocs(res: Response) {
-  res.redirect(`/v${verMajor}/docs/`);
+/** Redirects to the documentation page at the given path (homepage by default) */
+export function redirectToDocs(res: Response, path?: string) {
+  res.redirect(`/v${verMajor}/docs/${path ? path.replace(/^\//, "") : ""}`);
 }
 
-/** Hashes a string using SHA-512, encoded as "hex" by default */
-export function hashStr(str: string, encoding: BinaryToTextEncoding = "hex"): Promise<string> {
+/** Hashes a string. Uses SHA-512 encoded as "hex" by default */
+export function hashStr(str: string | { toString: () => string }, algorithm = "sha512", encoding: BinaryToTextEncoding = "hex"): Promise<string> {
   return new Promise((resolve, reject) => {
     try {
-      const hash = createHash("sha512");
-      hash.update(str);
+      const hash = createHash(algorithm);
+      hash.update(String(str));
       resolve(hash.digest(encoding));
     }
     catch(e) {
@@ -92,9 +93,11 @@ export function hashStr(str: string, encoding: BinaryToTextEncoding = "hex"): Pr
 }
 
 /** Returns the length of the given data - returns -1 if the data couldn't be stringified */
-export function getByteLength(data: string | Record<string, unknown>) {
+export function getByteLength(data: string | { toString: () => string } | Record<string, unknown>) {
   if(typeof data === "string" || "toString" in data)
     return Buffer.byteLength(String(data), "utf8");
-
-  return -1;
+  else if(typeof data === "object")
+    return Buffer.byteLength(JSON.stringify(data), "utf8");
+  else
+    return -1;
 }