request.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. const axios = require('axios')
  2. const https = require('https')
  3. const path = require('path')
  4. const Logger = require('../../lib/Logger')
  5. const { getJkesSettings, normalizeApiBase } = require('./jkesSettings')
  6. const logger = new Logger(path.join(__dirname, '../../logs/JKES.log'), 'INFO')
  7. let jkesHttpsAgent
  8. function getJkesHttpsAgent() {
  9. if (!jkesHttpsAgent) {
  10. const s = getJkesSettings()
  11. jkesHttpsAgent = new https.Agent({
  12. keepAlive: true,
  13. minVersion: 'TLSv1.2',
  14. rejectUnauthorized: s.tlsRejectUnauthorized !== false
  15. })
  16. }
  17. return jkesHttpsAgent
  18. }
  19. function buildJkesHeaders(token) {
  20. const tokenClean = String(token ?? '').trim()
  21. const s = getJkesSettings()
  22. return {
  23. 'x-auth-token': tokenClean,
  24. 'content-type': 'application/json',
  25. 'Accept-Encoding': 'gzip,compress,br,deflate',
  26. 'User-Agent': s.userAgent,
  27. Referer: s.referer
  28. }
  29. }
  30. function isJkesLoginExpiredPayload(payload) {
  31. if (!payload || typeof payload !== 'object') return false
  32. const code = Number(payload.code)
  33. const msg = String(payload.message ?? payload.msg ?? '').trim()
  34. return (
  35. (code === 500 && msg.includes('无效用户')) ||
  36. (code === 401 && msg.includes('您的认证已经失效'))
  37. )
  38. }
  39. function makeJkesLoginExpiredError(payload) {
  40. const msg = '乐跑账号登录失效,请重新使用乐跑登录器进行登录'
  41. const err = new Error(msg)
  42. err.code = 'JKES_AUTH_EXPIRED'
  43. err.loginExpired = true
  44. err.retryable = false
  45. err.payload = payload
  46. return err
  47. }
  48. /**
  49. * @param {string} url 如 /sys/user/getMyInfo
  50. * @param {object} [data] POST body
  51. * @param {string} token
  52. */
  53. async function jkesRequest(url, data, token) {
  54. const tokenClean = String(token ?? '').trim()
  55. if (!tokenClean) {
  56. logger.error('[JKES] 缺少 token')
  57. return null
  58. }
  59. const s = getJkesSettings()
  60. const pathPart = url.startsWith('/') ? url : `/${url}`
  61. const fullUrl = `${normalizeApiBase(s.apiBase)}${pathPart}`
  62. const body = data === undefined || data === null ? {} : data
  63. let parsed
  64. try {
  65. parsed = new URL(fullUrl)
  66. } catch (e) {
  67. logger.error(`[JKES] 非法 URL: ${fullUrl}`)
  68. return null
  69. }
  70. if (parsed.protocol !== 'https:') {
  71. logger.error(`[JKES] 必须使用 https: ${fullUrl}`)
  72. return null
  73. }
  74. try {
  75. logger.info(`[JKES] POST ${pathPart}`)
  76. const axiosOpts = {
  77. headers: buildJkesHeaders(tokenClean),
  78. timeout: Number(s.requestTimeoutMs) || 30000,
  79. validateStatus: () => true,
  80. httpsAgent: getJkesHttpsAgent(),
  81. beforeRedirect: (options) => {
  82. if (options.protocol !== 'https:') {
  83. logger.error(`[JKES] 拒绝跟随非 HTTPS 重定向: ${options.href}`)
  84. throw new Error('JKES 重定向目标必须为 https')
  85. }
  86. }
  87. }
  88. if (!s.useSystemProxy) {
  89. axiosOpts.proxy = false
  90. }
  91. const res = await axios.post(fullUrl, body, axiosOpts)
  92. const payload = res.data
  93. const payloadPreview =
  94. typeof payload === 'object' && payload !== null
  95. ? JSON.stringify(payload)
  96. : String(payload ?? '')
  97. if (isJkesLoginExpiredPayload(payload)) {
  98. throw makeJkesLoginExpiredError(payload)
  99. }
  100. if (res.status !== 200) {
  101. logger.error(
  102. `[JKES] HTTP ${res.status} ${pathPart} 响应: ${payloadPreview.slice(0, 1000)}`
  103. )
  104. if (typeof payload === 'object' && payload !== null) {
  105. return payload
  106. }
  107. return null
  108. }
  109. return payload
  110. } catch (error) {
  111. if (error?.loginExpired) throw error
  112. const st = error.response?.status
  113. const dataErr = error.response?.data
  114. const errStr =
  115. typeof dataErr === 'object' && dataErr !== null
  116. ? JSON.stringify(dataErr)
  117. : String(dataErr ?? '')
  118. logger.error(
  119. `[JKES] 请求异常 ${pathPart} http=${st ?? 'n/a'} body=${errStr.slice(0, 500)} ${error.message}`
  120. )
  121. return null
  122. }
  123. }
  124. module.exports = {
  125. jkesRequest,
  126. isJkesLoginExpiredPayload,
  127. makeJkesLoginExpiredError,
  128. get BASE_URL() {
  129. return normalizeApiBase(getJkesSettings().apiBase)
  130. }
  131. }