server.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. const compression = require("compression");
  2. const express = require("express");
  3. const { check: portUsed } = require("tcp-port-used");
  4. const helmet = require("helmet");
  5. const { RateLimiterMemory } = require("rate-limiter-flexible");
  6. const k = require("kleur");
  7. const cors = require("cors");
  8. const jsonToXml = require("js2xmlparser");
  9. const packageJson = require("../package.json");
  10. const error = require("./error");
  11. const { getMeta } = require("./songMeta");
  12. /** @typedef {import("svcorelib").JSONCompatible} JSONCompatible */
  13. /** @typedef {import("express").Response} Response */
  14. /** @typedef {import("./types").ResponseType} ResponseType */
  15. /** @typedef {import("./types").ResponseFormat} ResponseFormat */
  16. const app = express();
  17. app.use(cors({ methods: "GET,HEAD,OPTIONS" }));
  18. app.use(helmet());
  19. app.use(express.json());
  20. app.use(compression());
  21. const rateLimiter = new RateLimiterMemory({
  22. points: 8,
  23. duration: 10,
  24. });
  25. async function init()
  26. {
  27. const port = parseInt(process.env.HTTP_PORT);
  28. if(await portUsed(port))
  29. return error(`TCP port ${port} is already used`, undefined, true);
  30. // on error
  31. app.use((err, req, res, next) => {
  32. if(typeof err === "string" || err instanceof Error)
  33. return respond(res, "serverError", `General error in HTTP server: ${err.toString()}`, req?.params?.format);
  34. else
  35. return next();
  36. });
  37. const listener = app.listen(port, () => {
  38. app.disable("x-powered-by");
  39. // rate limiting
  40. app.use(async (req, res, next) => {
  41. try
  42. {
  43. await rateLimiter.consume(req.ip);
  44. }
  45. catch(rlRejected)
  46. {
  47. res.set("Retry-After", rlRejected?.msBeforeNext ? String(Math.round(rlRejected.msBeforeNext / 1000)) || 1 : 1);
  48. return respond(res, 429, { message: "You are being rate limited" }, req?.params?.format);
  49. }
  50. return next();
  51. });
  52. registerEndpoints();
  53. console.log(k.green(`Ready on port ${port}`));
  54. });
  55. listener.on("error", (err) => error("General server error", err, true));
  56. }
  57. function registerEndpoints()
  58. {
  59. try
  60. {
  61. app.get("/", (req, res) => {
  62. res.redirect(packageJson.homepage);
  63. });
  64. app.get("/search", async (req, res) => {
  65. const { q, format } = req.query;
  66. if(typeof q !== "string" || q.length === 0)
  67. return respond(res, "clientError", "No query parameter (?q=...) provided or it is invalid", req?.params?.format);
  68. const meta = await getMeta(q);
  69. // js2xmlparser needs special treatment when using arrays to produce a good XML structure
  70. const response = format === "xml" ? { top: meta.top, all: { "result": meta.all } } : meta;
  71. return respond(res, "success", response, req?.params?.format);
  72. });
  73. app.get("/search/top", async (req, res) => {
  74. const { q } = req.query;
  75. if(typeof q !== "string" || q.length === 0)
  76. return respond(res, "clientError", "No query parameter (?q=...) provided or it is invalid", req?.params?.format);
  77. const meta = await getMeta(q);
  78. return respond(res, "success", meta.top, req?.params?.format);
  79. });
  80. }
  81. catch(err)
  82. {
  83. error("Error while registering endpoints", err, true);
  84. }
  85. }
  86. /**
  87. * @param {Response} res
  88. * @param {ResponseType|number} type Specifies the type of response and thus a predefined status code - overload: set to number for custom status code
  89. * @param {JSONCompatible} data JSON object for "success", else an error message string
  90. * @param {ResponseFormat} [format]
  91. */
  92. function respond(res, type, data, format)
  93. {
  94. let statusCode = 500;
  95. let error = true;
  96. let resData = {};
  97. if(typeof format !== "string" || !["json", "xml"].includes(format.toLowerCase()))
  98. format = "json";
  99. format = format.toLowerCase();
  100. switch(type)
  101. {
  102. case "success":
  103. error = false;
  104. statusCode = 200;
  105. resData = { ...data };
  106. break;
  107. case "clientError":
  108. error = true;
  109. statusCode = 400;
  110. resData = { message: data };
  111. break;
  112. case "serverError":
  113. error = true;
  114. statusCode = 500;
  115. resData = { message: data };
  116. break;
  117. default:
  118. if(typeof type === "number")
  119. {
  120. error = false;
  121. statusCode = type;
  122. resData = { ...data };
  123. }
  124. break;
  125. }
  126. resData = {
  127. error,
  128. ...resData,
  129. timestamp: Date.now(),
  130. };
  131. const finalData = format === "xml" ? jsonToXml.parse("data", resData) : resData;
  132. res.status(statusCode).send(finalData);
  133. }
  134. module.exports = { init };