server.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. const fs = require('fs')
  2. const path = require('path')
  3. const express = require('express')
  4. const request = require('./util/request')
  5. const packageJSON = require('./package.json')
  6. const exec = require('child_process').exec
  7. const cache = require('./util/apicache').middleware
  8. const { cookieToJson } = require('./util/index')
  9. const fileUpload = require('express-fileupload')
  10. const decode = require('safe-decode-uri-component')
  11. /**
  12. * The version check result.
  13. * @readonly
  14. * @enum {number}
  15. */
  16. const VERSION_CHECK_RESULT = {
  17. FAILED: -1,
  18. NOT_LATEST: 0,
  19. LATEST: 1,
  20. }
  21. /**
  22. * @typedef {{
  23. * identifier?: string,
  24. * route: string,
  25. * module: any
  26. * }} ModuleDefinition
  27. */
  28. /**
  29. * @typedef {{
  30. * port?: number,
  31. * host?: string,
  32. * checkVersion?: boolean,
  33. * moduleDefs?: ModuleDefinition[]
  34. * }} NcmApiOptions
  35. */
  36. /**
  37. * @typedef {{
  38. * status: VERSION_CHECK_RESULT,
  39. * ourVersion?: string,
  40. * npmVersion?: string,
  41. * }} VersionCheckResult
  42. */
  43. /**
  44. * @typedef {{
  45. * server?: import('http').Server,
  46. * }} ExpressExtension
  47. */
  48. /**
  49. * Get the module definitions dynamically.
  50. *
  51. * @param {string} modulesPath The path to modules (JS).
  52. * @param {Record<string, string>} [specificRoute] The specific route of specific modules.
  53. * @param {boolean} [doRequire] If true, require() the module directly.
  54. * Otherwise, print out the module path. Default to true.
  55. * @returns {Promise<ModuleDefinition[]>} The module definitions.
  56. *
  57. * @example getModuleDefinitions("./module", {"album_new.js": "/album/create"})
  58. */
  59. async function getModulesDefinitions(
  60. modulesPath,
  61. specificRoute,
  62. doRequire = true,
  63. ) {
  64. const files = await fs.promises.readdir(modulesPath)
  65. const parseRoute = (/** @type {string} */ fileName) =>
  66. specificRoute && fileName in specificRoute
  67. ? specificRoute[fileName]
  68. : `/${fileName.replace(/\.js$/i, '').replace(/_/g, '/')}`
  69. const modules = files
  70. .reverse()
  71. .filter((file) => file.endsWith('.js'))
  72. .map((file) => {
  73. const identifier = file.split('.').shift()
  74. const route = parseRoute(file)
  75. const modulePath = path.join(modulesPath, file)
  76. const module = doRequire ? require(modulePath) : modulePath
  77. return { identifier, route, module }
  78. })
  79. return modules
  80. }
  81. /**
  82. * Check if the version of this API is latest.
  83. *
  84. * @returns {Promise<VersionCheckResult>} If true, this API is up-to-date;
  85. * otherwise, this API should be upgraded and you would
  86. * need to notify users to upgrade it manually.
  87. */
  88. async function checkVersion() {
  89. return new Promise((resolve) => {
  90. exec('npm info NeteaseCloudMusicApi version', (err, stdout) => {
  91. if (!err) {
  92. let version = stdout.trim()
  93. /**
  94. * @param {VERSION_CHECK_RESULT} status
  95. */
  96. const resolveStatus = (status) =>
  97. resolve({
  98. status,
  99. ourVersion: packageJSON.version,
  100. npmVersion: version,
  101. })
  102. resolveStatus(
  103. packageJSON.version < version
  104. ? VERSION_CHECK_RESULT.NOT_LATEST
  105. : VERSION_CHECK_RESULT.LATEST,
  106. )
  107. } else {
  108. resolve({
  109. status: VERSION_CHECK_RESULT.FAILED,
  110. })
  111. }
  112. })
  113. })
  114. }
  115. /**
  116. * Construct the server of NCM API.
  117. *
  118. * @param {ModuleDefinition[]} [moduleDefs] Customized module definitions [advanced]
  119. * @returns {Promise<import("express").Express>} The server instance.
  120. */
  121. async function consturctServer(moduleDefs) {
  122. const app = express()
  123. const { CORS_ALLOW_ORIGIN } = process.env
  124. app.set('trust proxy', true)
  125. /**
  126. * Serving static files
  127. */
  128. app.use(express.static(path.join(__dirname, 'public')))
  129. /**
  130. * CORS & Preflight request
  131. */
  132. app.use((req, res, next) => {
  133. if (req.path !== '/' && !req.path.includes('.')) {
  134. res.set({
  135. 'Access-Control-Allow-Credentials': true,
  136. 'Access-Control-Allow-Origin':
  137. CORS_ALLOW_ORIGIN || req.headers.origin || '*',
  138. 'Access-Control-Allow-Headers': 'X-Requested-With,Content-Type',
  139. 'Access-Control-Allow-Methods': 'PUT,POST,GET,DELETE,OPTIONS',
  140. 'Content-Type': 'application/json; charset=utf-8',
  141. })
  142. }
  143. req.method === 'OPTIONS' ? res.status(204).end() : next()
  144. })
  145. /**
  146. * Cookie Parser
  147. */
  148. app.use((req, _, next) => {
  149. req.cookies = {}
  150. //;(req.headers.cookie || '').split(/\s*;\s*/).forEach((pair) => { // Polynomial regular expression //
  151. ;(req.headers.cookie || '').split(/;\s+|(?<!\s)\s+$/g).forEach((pair) => {
  152. let crack = pair.indexOf('=')
  153. if (crack < 1 || crack == pair.length - 1) return
  154. req.cookies[decode(pair.slice(0, crack)).trim()] = decode(
  155. pair.slice(crack + 1),
  156. ).trim()
  157. })
  158. next()
  159. })
  160. /**
  161. * Body Parser and File Upload
  162. */
  163. app.use(express.json({ limit: '50mb' }))
  164. app.use(express.urlencoded({ extended: false, limit: '50mb' }))
  165. app.use(fileUpload())
  166. /**
  167. * Cache
  168. */
  169. app.use(cache('2 minutes', (_, res) => res.statusCode === 200))
  170. /**
  171. * Special Routers
  172. */
  173. const special = {
  174. 'daily_signin.js': '/daily_signin',
  175. 'fm_trash.js': '/fm_trash',
  176. 'personal_fm.js': '/personal_fm',
  177. }
  178. /**
  179. * Load every modules in this directory
  180. */
  181. const moduleDefinitions =
  182. moduleDefs ||
  183. (await getModulesDefinitions(path.join(__dirname, 'module'), special))
  184. for (const moduleDef of moduleDefinitions) {
  185. // Register the route.
  186. app.use(moduleDef.route, async (req, res) => {
  187. ;[req.query, req.body].forEach((item) => {
  188. if (typeof item.cookie === 'string') {
  189. item.cookie = cookieToJson(decode(item.cookie))
  190. }
  191. })
  192. let query = Object.assign(
  193. {},
  194. { cookie: req.cookies },
  195. req.query,
  196. req.body,
  197. req.files,
  198. )
  199. try {
  200. const moduleResponse = await moduleDef.module(query, (...params) => {
  201. // 参数注入客户端IP
  202. const obj = [...params]
  203. let ip = req.ip
  204. if (ip.substr(0, 7) == '::ffff:') {
  205. ip = ip.substr(7)
  206. }
  207. if (ip == '::1') {
  208. ip = global.cnIp
  209. }
  210. // console.log(ip)
  211. obj[3] = {
  212. ...obj[3],
  213. ip,
  214. }
  215. return request(...obj)
  216. })
  217. console.log('[OK]', decode(req.originalUrl))
  218. const cookies = moduleResponse.cookie
  219. if (!query.noCookie) {
  220. if (Array.isArray(cookies) && cookies.length > 0) {
  221. if (req.protocol === 'https') {
  222. // Try to fix CORS SameSite Problem
  223. res.append(
  224. 'Set-Cookie',
  225. cookies.map((cookie) => {
  226. return cookie + '; SameSite=None; Secure'
  227. }),
  228. )
  229. } else {
  230. res.append('Set-Cookie', cookies)
  231. }
  232. }
  233. }
  234. res.status(moduleResponse.status).send(moduleResponse.body)
  235. } catch (/** @type {*} */ moduleResponse) {
  236. console.log('[ERR]', decode(req.originalUrl), {
  237. status: moduleResponse.status,
  238. body: moduleResponse.body,
  239. })
  240. if (!moduleResponse.body) {
  241. res.status(404).send({
  242. code: 404,
  243. data: null,
  244. msg: 'Not Found',
  245. })
  246. return
  247. }
  248. if (moduleResponse.body.code == '301')
  249. moduleResponse.body.msg = '需要登录'
  250. if (!query.noCookie) {
  251. res.append('Set-Cookie', moduleResponse.cookie)
  252. }
  253. res.status(moduleResponse.status).send(moduleResponse.body)
  254. }
  255. })
  256. }
  257. return app
  258. }
  259. /**
  260. * Serve the NCM API.
  261. * @param {NcmApiOptions} options
  262. * @returns {Promise<import('express').Express & ExpressExtension>}
  263. */
  264. async function serveNcmApi(options) {
  265. const port = Number(options.port || process.env.PORT || '3000')
  266. const host = options.host || process.env.HOST || ''
  267. const checkVersionSubmission =
  268. options.checkVersion &&
  269. checkVersion().then(({ npmVersion, ourVersion, status }) => {
  270. if (status == VERSION_CHECK_RESULT.NOT_LATEST) {
  271. console.log(
  272. `最新版本: ${npmVersion}, 当前版本: ${ourVersion}, 请及时更新`,
  273. )
  274. }
  275. })
  276. const constructServerSubmission = consturctServer(options.moduleDefs)
  277. const [_, app] = await Promise.all([
  278. checkVersionSubmission,
  279. constructServerSubmission,
  280. ])
  281. /** @type {import('express').Express & ExpressExtension} */
  282. const appExt = app
  283. appExt.server = app.listen(port, host, () => {
  284. console.log(`server running @ http://${host ? host : 'localhost'}:${port}`)
  285. })
  286. return appExt
  287. }
  288. module.exports = {
  289. serveNcmApi,
  290. getModulesDefinitions,
  291. }