天天看點

開源的網易雲音樂API項目都是怎麼實作的?

這篇文章我們來詳細了解一下其中使用到的網易雲音樂​

​api​

​​項目​​NeteaseCloudMusicApi​​的實作原理。

​​NeteaseCloudMusicApi​​​使用​

​Node.js​

​​開發,主要用到的架構和庫有兩個,一個Web應用開發架構​​Express​​​,一個請求庫​​Axios​​,這兩個大家應該都很熟了就不過多介紹了。

建立express應用

項目的入口檔案為​

​/app.js​

​:

async function start() {
  require('./server').serveNcmApi({
    checkVersion: true,
  })
}
start()      

調用了​

​/server.js​

​​檔案的​

​serveNcmApi​

​​方法,讓我們轉到這個檔案,​

​serveNcmApi​

​方法簡化後如下:

async function serveNcmApi(options) {
    const port = Number(options.port || process.env.PORT || '3000')
    const host = options.host || process.env.HOST || ''
    const app = await consturctServer(options.moduleDefs)
    const appExt = app
    appExt.server = app.listen(port, host, () => {
        console.log(`server running @ http://${host ? host : 'localhost'}:${port}`)
    })

    return      

主要是啟動監聽指定端口,是以建立應用的主要邏輯在​

​consturctServer​

​方法:

async function consturctServer(moduleDefs) {
    // 建立一個應用
    const app = express()

    // 設定為true,則用戶端的IP位址被了解為X-Forwarded-*報頭中最左邊的條目
    app.set('trust proxy', true)

    /**
   * 配置CORS & 預檢請求
   */
    app.use((req, res, next) => {
        if (req.path !== '/' && !req.path.includes('.')) {
            res.set({
                'Access-Control-Allow-Credentials': true, // 跨域情況下,允許用戶端攜帶驗證資訊,比如cookie,同時,前端發送請求時也需要設定withCredentials: true
                'Access-Control-Allow-Origin': req.headers.origin || '*', // 允許跨域請求的域名,設定為*代表允許所有域名
                'Access-Control-Allow-Headers': 'X-Requested-With,Content-Type', // 用于給預檢請求(options)列出服務端允許的自定義标頭,如果前端發送的請求中包含自定義的請求标頭,且該标頭不包含在Access-Control-Allow-Headers中,那麼該請求無法成功發起
                'Access-Control-Allow-Methods': 'PUT,POST,GET,DELETE,OPTIONS', // 設定跨域請求允許的請求方法理想
                'Content-Type': 'application/json; charset=utf-8', // 設定響應資料的類型及編碼
            })
        }
        // OPTIONS為預檢請求,複雜請求會在發送真正的請求前先發送一個預檢請求,擷取伺服器支援的Access-Control-Allow-xxx相關資訊,判斷後續是否有必要再發送真正的請求,傳回狀态碼204代表請求成功,但是沒有内容
        req.method === 'OPTIONS' ? res.status(204).end() : next()
    })
    // ...      

首先建立了一個​

​Express​

​​應用,然後設定為信任代理,在​

​Express​

​​裡擷取​

​ip​

​​一般是通過​

​req.ip​

​​和​

​req.ips​

​​,​

​trust proxy​

​​預設值為​

​false​

​​,這種情況下​

​req.ips​

​​值是空的,當設定為​

​true​

​​時,​

​req.ip​

​​的值會從請求頭​

​X-Forwarded-For​

​​上取最左側的一個值,​

​req.ips​

​​則會包含​

​X-Forwarded-For​

​​頭部的所有​

​ip​

​位址。

​X-Forwarded-For​

​頭部的格式如下:

X-Forwarded-For: client1, proxy1, proxy2      

值通過一個 ​

​逗号+空格​

​​ 把多個​

​ip​

​​位址區分開,最左邊的​

​client1​

​​是最原始用戶端的​

​ip​

​位址,代理伺服器每成功收到一個請求,就把請求來源​

​ip​

​位址添加到右邊。

以上面為例,這個請求通過了兩台代理伺服器:​

​proxy1​

​​和​

​proxy2​

​​。請求由​

​client1​

​​發出,此時​

​XFF​

​​是空的,到了​

​proxy1​

​​時,​

​proxy1​

​​把​

​client1​

​​添加到​

​XFF​

​​中,之後請求發往​

​proxy2​

​​,通過​

​proxy2​

​​的時候,​

​proxy1​

​​被添加到​

​XFF​

​​中,之後請求發往最終伺服器,到達後​

​proxy2​

​​被添加到​

​XFF​

​中。

但是僞造這個字段非常容易,是以當代理不可信時,這個字段也不一定可靠,不過正常情況下​

​XFF​

​​中最後一個​

​ip​

​​位址肯定是最後一個代理伺服器的​

​ip​

​位址,這個會比較可靠。

随後設定了跨域響應頭,這裡的設定就是允許不同域名的網站也能請求成功的關鍵所在。

繼續:

async function consturctServer(moduleDefs) {
    // ...
    /**
   * 解析Cookie
   */
    app.use((req, _, next) => {
        req.cookies = {}
        //;(req.headers.cookie || '').split(/\s*;\s*/).forEach((pair) => { //  Polynomial regular expression //
        // 從請求頭中讀取cookie,cookie格式為:name=value;name2=value2...,是以先根據;切割為數組
        ;(req.headers.cookie || '').split(/;\s+|(?<!\s)\s+$/g).forEach((pair) => {
            let crack = pair.indexOf('=')
            // 沒有值的直接跳過
            if (crack < 1 || crack == pair.length - 1) return
            // 将cookie儲存到cookies對象上
            req.cookies[decode(pair.slice(0, crack)).trim()] = decode(
                pair.slice(crack + 1),
            ).trim()
        })
        next()
    })

    /**
   * 請求體解析和檔案上傳處理
   */
    app.use(express.json())
    app.use(express.urlencoded({ extended: false }))
    app.use(fileUpload())

    /**
   * 将public目錄下的檔案作為靜态檔案提供
   */
    app.use(express.static(path.join(__dirname, 'public')))

    /**
   * 緩存請求,兩分鐘内同樣的請求會從緩存裡讀取資料,不會向網易雲音樂伺服器發送請求
   */
    app.use(cache('2 minutes', (_, res) => res.statusCode === 200))
    // ...      

接下來注冊了一些中間件,用來解析​

​cookie​

​、處理請求體等,另外還做了接口緩存,防止太頻繁請求網易雲音樂伺服器導緻被封掉。

繼續:

async function consturctServer(moduleDefs) {
    // ...
    /**
   * 特殊路由
   */
    const special = {
        'daily_signin.js': '/daily_signin',
        'fm_trash.js': '/fm_trash',
        'personal_fm.js': '/personal_fm',
    }

    /**
   * 加載/module目錄下的所有子產品,每個子產品對應一個接口
   */
    const moduleDefinitions =
          moduleDefs ||
          (await getModulesDefinitions(path.join(__dirname, 'module'), special))
    // ...      

接下來加載了​

​/module​

​目錄下所有的子產品:

開源的網易雲音樂API項目都是怎麼實作的?

每個子產品代表一個對網易雲音樂接口的請求,比如擷取專輯詳情的​

​album_detail.js​

​:

開源的網易雲音樂API項目都是怎麼實作的?

子產品加載方法​

​getModulesDefinitions​

​如下:

async function getModulesDefinitions(true,
) {
  const files = await fs.promises.readdir(modulesPath)
  const parseRoute = (fileName) =>
    specificRoute && fileName in specificRoute
      ? specificRoute[fileName]
      : `/${fileName.replace(/\.js$/i, '').replace(/_/g, '/')}`
  // 周遊目錄下的所有檔案
  const modules = files
    .reverse()
    .filter((file) => file.endsWith('.js'))// 過濾出js檔案
    .map((file) => {
      const identifier = file.split('.').shift()// 子產品辨別
      const route = parseRoute(file)// 子產品對應的路由
      const modulePath = path.join(modulesPath, file)// 子產品路徑
      const module = doRequire ? require(modulePath) : modulePath// 加載子產品

      return { identifier, route, module }
    })

  return      

以剛才的​

​album_detail.js​

​子產品為例,傳回的資料如下:

{
    identifier: 'album_detail',
    route: '/album/detail',
    module: () => {/*子產品内容*/}
}      

接下來就是注冊路由:

async function consturctServer(moduleDefs) {
    // ...
    for (const moduleDef of moduleDefinitions) {
        // 注冊路由
        app.use(moduleDef.route, async (req, res) => {
            // cookie也可以從查詢參數、請求體上傳來
            ;[req.query, req.body].forEach((item) => {
                if (typeof item.cookie === 'string') {
                    // 将cookie字元串轉換成json類型
                    item.cookie = cookieToJson(decode(item.cookie))
                }
            })

            // 把cookie、查詢參數、請求頭、檔案都整合到一起,作為參數傳給每個子產品
            let query = Object.assign(
                {},
                { cookie: req.cookies },
                req.query,
                req.body,
                req.files,
            )

            try {
                // 執行子產品方法,即發起對網易雲音樂接口的請求
                const moduleResponse = await moduleDef.module(query, (...params) => {
                    // 參數注入用戶端IP
                    const obj = [...params]
                    // 處理ip,為了實作IPv4-IPv6互通,IPv4位址前會增加::ffff:
                    let ip = req.ip
                    if (ip.substr(0, 7) == '::ffff:') {
                        ip = ip.substr(7)
                    }
                    obj[3] = {
                        ...obj[3],
                        ip,
                    }
                    return request(...obj)
                })
                // 請求成功後,擷取響應中的cookie,并且通過Set-Cookie響應頭來将這個cookie設定到前端浏覽器上
                const cookies = moduleResponse.cookie
                if (Array.isArray(cookies) && cookies.length > 0) {
                    if (req.protocol === 'https') {
                        // 去掉跨域請求cookie的SameSite限制,這個屬性用來限制第三方Cookie,進而減少安全風險
                        res.append(
                            'Set-Cookie',
                            cookies.map((cookie) => {
                                return cookie + '; SameSite=None; Secure'
                            }),
                        )
                    } else {
                        res.append('Set-Cookie', cookies)
                    }
                }
                // 回複請求
                res.status(moduleResponse.status).send(moduleResponse.body)
            } catch (moduleResponse) {
                // 請求失敗處理
                // 沒有響應體,傳回404
                if (!moduleResponse.body) {
                    res.status(404).send({
                        code: 404,
                        data: null,
                        msg: 'Not Found',
                    })
                    return
                }
                // 301代表調用了需要登入的接口,但是并沒有登入
                if (moduleResponse.body.code == '301')
                    moduleResponse.body.msg = '需要登入'
                res.append('Set-Cookie', moduleResponse.cookie)
                res.status(moduleResponse.status).send(moduleResponse.body)
            }
        })
    }

    return      

邏輯很清晰,将每個子產品都注冊成一個路由,接收到對應的請求後,将​

​cookie​

​​、查詢參數、請求體等都傳給對應的子產品,然後請求網易雲音樂的接口,如果請求成功了,那麼處理一下網易雲音樂接口傳回的​

​cookie​

​,最後将資料都傳回給前端即可,如果接口失敗了,那麼也進行對應的處理。

其中從請求的查詢參數和請求體裡擷取​

​cookie​

​​可能不是很好了解,因為​

​cookie​

​​一般是從請求體裡帶過來,這麼做應該主要是為了支援在​

​Node.js​

​裡調用:

開源的網易雲音樂API項目都是怎麼實作的?

請求成功後,傳回的資料裡如果存在​

​cookie​

​​,那麼會進行一些處理,首先如果是​

​https​

​​的請求,那麼會設定​

​SameSite=None; Secure​

​​,​

​SameSite​

​​是​

​Cookie​

​​中的一個屬性,用來限制第三方​

​Cookie​

​​,進而減少安全風險。​

​Chrome 51​

​​ 開始新增這個屬性,用來防止​

​CSRF​

​​攻擊和使用者追蹤,有三個可選值:​

​strict/lax/none​

​​,預設為​

​lax​

​​,比如在域名為​

​https://123.com​

​​的頁面裡調用​

​https://456.com​

​​域名的接口,預設情況下除了導航到​

​123​

​​網址的​

​get​

​​請求除外,其他請求都不會攜帶​

​123​

​​域名的​

​cookie​

​​,如果設定為​

​strict​

​​更嚴格,完全不會攜帶​

​cookie​

​​,是以這個項目為了友善跨域調用,設定為​

​none​

​​,不進行限制,設定為​

​none​

​​的同時需要設定​

​Secure​

​屬性。

最後通過​

​Set-Cookie​

​​響應頭将​

​cookie​

​寫入前端的浏覽器即可。

發送請求

接下來看一下上面涉及到發送請求所使用的​

​request​

​​方法,這個方法在​

​/util/request.js​

​檔案,首先引入了一些子產品:

const encrypt = require('./crypto')
const axios = require('axios')
const PacProxyAgent = require('pac-proxy-agent')
const http = require('http')
const https = require('https')
const tunnel = require('tunnel')
const { URLSearchParams, URL } = require('url')
const config = require('../util/config.json')
// ...      

然後就是具體發送請求的方法​

​createRequest​

​,這個方法也挺長的,我們慢慢來看:

const createRequest = (method, url, data = {}, options) => {
    return new Promise((resolve, reject) => {
        let headers = { 'User-Agent': chooseUserAgent(options.ua) }
        // ...      

函數會傳回一個​

​Promise​

​​,首先定義了一個請求頭對象,并添加了​

​User-Agent​

​頭,這個頭部會儲存浏覽器類型、版本号、渲染引擎,以及作業系統、版本、CPU類型等資訊,标準格式為:

浏覽器辨別 (作業系統辨別; 加密等級辨別; 浏覽器語言) 渲染引擎辨別 版本資訊      

不用多說,僞造這個頭顯然是用來欺騙伺服器,讓它認為這個請求是來自浏覽器,而不是同樣也來自服務端。

預設寫死了幾個​

​User-Agent​

​頭部随機進行選擇:

const chooseUserAgent = (ua = false) => {
    const userAgentList = {
        mobile: [
            'Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/604.1',
            'Mozilla/5.0 (Linux; Android 9; PCT-AL10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.64 HuaweiBrowser/10.0.3.311 Mobile Safari/537.36',
            // ...
        ],
        pc: [
            'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:80.0) Gecko/20100101 Firefox/80.0',
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:80.0) Gecko/20100101 Firefox/80.0',
            // ...
        ],
    }
    let realUserAgentList =
        userAgentList[ua] || userAgentList.mobile.concat(userAgentList.pc)
    return ['mobile', 'pc', false].indexOf(ua) > -1
        ? realUserAgentList[Math.floor(Math.random() * realUserAgentList.length)]
    : ua
}      

繼續看:

const createRequest = (method, url, data = {}, options) => {
    return new Promise((resolve, reject) => {
        // ...
        // 如果是post請求,修改編碼格式
        if (method.toUpperCase() === 'POST')
            headers['Content-Type'] = 'application/x-www-form-urlencoded'
        // 僞造Referer頭
        if (url.includes('music.163.com'))
            headers['Referer'] = 'https://music.163.com'
        // 設定ip頭部
        let ip = options.realIP || options.ip || ''
        if (ip) {
            headers['X-Real-IP'] = ip
            headers['X-Forwarded-For'] = ip
        }
        // ...      

繼續設定了幾個頭部字段,​

​Axios​

​​預設的編碼格式為​

​json​

​​,而​

​POST​

​​請求一般都會使用​

​application/x-www-form-urlencoded​

​編碼格式。

​Referer​

​​頭代表發送請求時所在頁面的​

​url​

​​,比如在​

​https://123.com​

​​頁面内調用​

​https://456.com​

​​接口,​

​Referer​

​​頭會設定為​

​https://123.com​

​,這個頭部一般用來防盜鍊。是以僞造這個頭部也是為了欺騙伺服器這個請求是來自它們自己的頁面。

接下來設定了兩個​

​ip​

​​頭部,​

​realIP​

​需要前端手動傳遞:

開源的網易雲音樂API項目都是怎麼實作的?

繼續:

const createRequest = (method, url, data = {}, options) => {
    return new Promise((resolve, reject) => {
        // ...
        // 設定cookie
        if (typeof options.cookie === 'object') {
            if (!options.cookie.MUSIC_U) {
                // 遊客
                if (!options.cookie.MUSIC_A) {
                    options.cookie.MUSIC_A = config.anonymous_token
                }
            }
            headers['Cookie'] = Object.keys(options.cookie)
                .map(
                (key) =>
                encodeURIComponent(key) +
                '=' +
                encodeURIComponent(options.cookie[key]),
            )
                .join('; ')
        } else if (options.cookie) {
            headers['Cookie'] = options.cookie
        }
        // ...      

接下來設定​

​cookie​

​​,分兩種類型,一種是對象類型,這種情況​

​cookie​

​​一般來源于查詢參數或者請求體,另一種為字元串,這個就是正常情況下請求頭帶過來的。​

​MUSIC_U​

​​應該就是登入後的​

​cookie​

​​了,​

​MUSIC_A​

​​應該是一個​

​token​

​​,未登入情況下調用某些接口可能報錯,是以會設定一個遊客​

​token​

​:

開源的網易雲音樂API項目都是怎麼實作的?

繼續:

const createRequest = (method, url, data = {}, options) => {
    return new Promise((resolve, reject) => {
        // ...
        if (options.crypto === 'weapi') {
            let csrfToken = (headers['Cookie'] || '').match(/_csrf=([^(;|$)]+)/)
            data.csrf_token = csrfToken ? csrfToken[1] : ''
            data = encrypt.weapi(data)
            url = url.replace(/\w*api/, 'weapi')
        } else if (options.crypto === 'linuxapi') {
            data = encrypt.linuxapi({
                method: method,
                url: url.replace(/\w*api/, 'api'),
                params: data,
            })
            headers['User-Agent'] =
                'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36'
            url = 'https://music.163.com/api/linux/forward'
        } else if (options.crypto === 'eapi') {
            const cookie = options.cookie || {}
            const csrfToken = cookie['__csrf'] || ''
            const header = {
                osver: cookie.osver, //系統版本
                deviceId: cookie.deviceId, //encrypt.base64.encode(imei + '\t02:00:00:00:00:00\t5106025eb79a5247\t70ffbaac7')
                appver: cookie.appver || '8.7.01', // app版本
                versioncode: cookie.versioncode || '140', //版本号
                mobilename: cookie.mobilename, //裝置model
                buildver: cookie.buildver || Date.now().toString().substr(0, 10),
                resolution: cookie.resolution || '1920x1080', //裝置分辨率
                __csrf: csrfToken,
                os: cookie.os || 'android',
                channel: cookie.channel,
                requestId: `${Date.now()}_${Math.floor(Math.random() * 1000)
                .toString()
                .padStart(4, '0')}`,
            }
            if (cookie.MUSIC_U) header['MUSIC_U'] = cookie.MUSIC_U
            if (cookie.MUSIC_A) header['MUSIC_A'] = cookie.MUSIC_A
            headers['Cookie'] = Object.keys(header)
                .map(
                (key) =>
                encodeURIComponent(key) + '=' + encodeURIComponent(header[key]),
            )
                .join('; ')
            data.header = header
            data = encrypt.eapi(options.url, data)
            url = url.replace(/\w*api/, 'eapi')
        }
        // ...      

這一段代碼會比較難了解,筆者也沒有看懂,反正大緻呢這個項目使用了四種類型網易雲音樂的接口:​

​weapi​

​​、​

​linuxapi​

​​、​

​eapi​

​​、​

​api​

​,比如:

https://music.163.com/weapi/vipmall/albumproduct/detail
https://music.163.com/eapi/activate/initProfile
https://music.163.com/api/album/detail/dynamic      

每種類型的接口請求參數、加密方式都不一樣,是以需要分開單獨處理:

開源的網易雲音樂API項目都是怎麼實作的?

比如​

​weapi​

​:

let csrfToken = (headers['Cookie'] || '').match(/_csrf=([^(;|$)]+)/)
data.csrf_token = csrfToken ? csrfToken[1] : ''
data = encrypt.weapi(data)
url = url.replace(/\w*api/, 'weapi')      

将​

​cookie​

​​中的​

​_csrf​

​值取出加到請求資料中,然後加密資料:

const weapi = (object) => {
  const text = JSON.stringify(object)
  const secretKey = crypto
    .randomBytes(16)
    .map((n) => base62.charAt(n % 62).charCodeAt())
  return {
    params: aesEncrypt(
      Buffer.from(
        aesEncrypt(Buffer.from(text), 'cbc', presetKey, iv).toString('base64'),
      ),
      'cbc',
      secretKey,
      iv,
    ).toString('base64'),
    encSecKey: rsaEncrypt(secretKey.reverse(), publicKey).toString('hex'),
  }
}      

檢視其他加密算法:​​crypto.js​​。

至于這些是怎麼知道的呢,要麼就是網易雲音樂内部人士(基本不可能),要麼就是進行逆向了,比如網頁版的接口,打開控制台,發送請求,找到在源碼中的位置, 打斷點,檢視請求資料結構,閱讀壓縮或混淆後的源碼慢慢進行嘗試,總之,向這些大佬緻敬。

繼續:

const createRequest = (method, url, data = {}, options) => {
    return new Promise((resolve, reject) => {
        // ...
        // 響應的資料結構
        const answer = { status: 500, body: {}, cookie: [] }
        // 請求配置
        let settings = {
            method: method,
            url: url,
            headers: headers,
            data: new URLSearchParams(data).toString(),
            httpAgent: new http.Agent({ keepAlive: true }),
            httpsAgent: new https.Agent({ keepAlive: true }),
        }
        if (options.crypto === 'eapi') settings.encoding = null
        // 配置代理
        if (options.proxy) {
            if (options.proxy.indexOf('pac') > -1) {
                settings.httpAgent = new PacProxyAgent(options.proxy)
                settings.httpsAgent = new PacProxyAgent(options.proxy)
            } else {
                const purl = new URL(options.proxy)
                if (purl.hostname) {
                    const agent = tunnel.httpsOverHttp({
                        proxy: {
                            host: purl.hostname,
                            port: purl.port || 80,
                        },
                    })
                    settings.httpsAgent = agent
                    settings.httpAgent = agent
                    settings.proxy = false
                } else {
                    console.error('代理配置無效,不使用代理')
                }
            }
        } else {
            settings.proxy = false
        }
        if (options.crypto === 'eapi') {
            settings = {
                ...settings,
                responseType: 'arraybuffer',
            }
        }
        // ...      

這裡主要是定義了響應的資料結構、定義了請求的配置資料,以及針對​

​eapi​

​做了一些特殊處理,最主要是代理的相關配置。

​Agent​

​​是​

​Node.js​

​​的​

​HTTP​

​​子產品中的一個類,負責管理​

​http​

​​用戶端連接配接的持久性和重用。 它維護一個給定主機和端口的待處理請求隊列,為每個請求重用單個套接字連接配接,直到隊列為空,此時套接字要麼被銷毀,要麼放入池中,在池裡會被再次用于請求到相同的主機和端口,總之就是省去了每次發起​

​http​

​請求時需要重新建立套接字的時間,提高效率。

​pac​

​​指代理自動配置,其實就是包含了一個​

​javascript​

​​函數的文本檔案,這個函數會決定是直接連接配接還是通過某個代理連接配接,比直接寫死一個代理友善一點,當然需要配置的​

​options.proxy​

​​是這個檔案的遠端位址,格式為:​

​'pac+【pac檔案位址】+'​

​​。​

​pac-proxy-agent​

​​子產品會提供一個​

​http.Agent​

​​實作,它會根據指定的​

​PAC​

​​代理檔案判斷使用哪個​

​HTTP​

​​、​

​HTTPS​

​​ 或​

​SOCKS​

​代理,或者是直接連接配接。

至于為什麼要使用​

​tunnel​

​​子產品,筆者搜尋了一番還是沒有搞懂,可能是解決​

​http​

​​協定的接口請求網易雲音樂的​

​https​

​協定接口失敗的問題?知道的朋友可以評論區解釋一下~

const createRequest = (method, url, data = {}, options) => {
    return new Promise((resolve, reject) => {
        // ...
        axios(settings)
            .then((res) => {
                const body = res.data
                // 将響應的set-cookie頭中的cookie取出,直接儲存到響應對象上
                answer.cookie = (res.headers['set-cookie'] || []).map((x) =>
                    x.replace(/\s*Domain=[^(;|$)]+;*/, ''),// 去掉域名限制
                )
                try {
                    // eapi傳回的資料也是加密的,需要解密
                    if (options.crypto === 'eapi') {
                        answer.body = JSON.parse(encrypt.decrypt(body).toString())
                    } else {
                        answer.body = body
                    }
                    answer.status = answer.body.code || res.status
                    // 統一這些狀态碼為200,都代表成功
                    if (
                        [201, 302, 400, 502, 800, 801, 802, 803].indexOf(answer.body.code) > -1
                    ) {
                        // 特殊狀态碼
                        answer.status = 200
                    }
                } catch (e) {
                    try {
                        answer.body = JSON.parse(body.toString())
                    } catch (err) {
                        answer.body = body
                    }
                    answer.status = res.status
                }
                answer.status =
                    100 < answer.status && answer.status < 600 ? answer.status : 400
                // 狀态碼200代表成功,其他都代表失敗
                if (answer.status === 200) resolve(answer)
                else reject(answer)
            })
            .catch((err) => {
                answer.status = 502
                answer.body = { code: 502, msg: err }
                reject(answer)
            })
    })
}      

總結

繼續閱讀