diff --git a/interface.d.ts b/interface.d.ts index 071338b5..133f46c7 100644 --- a/interface.d.ts +++ b/interface.d.ts @@ -5,6 +5,8 @@ export interface RequestBaseConfig { randomCNIP?: boolean // Whether to use a random Chinese IP address filled in X-Real-IP } +export type LoginPlatform = 'web' | 'pc' | 'ios' | 'iphone' | 'mobile' | string + export interface MultiPageConfig { limit?: string | number offset?: string | number @@ -272,14 +274,21 @@ export function batch( ): Promise export function captcha_sent( - params: { phone: string; ctcode?: number | string } & RequestBaseConfig, + params: { + phone: string + ctcode?: number | string + countrycode?: number | string + platform?: LoginPlatform + } & RequestBaseConfig, ): Promise export function captcha_verify( params: { ctcode?: number | string + countrycode?: number | string phone: number | string captcha: string + platform?: LoginPlatform } & RequestBaseConfig, ): Promise @@ -691,6 +700,7 @@ export function login_cellphone( phone: number | string countrycode?: number | string password: string + platform?: LoginPlatform } & RequestBaseConfig, ): Promise @@ -699,6 +709,7 @@ export function login_cellphone( phone: number | string countrycode?: number | string md5_password: string + platform?: LoginPlatform } & RequestBaseConfig, ): Promise @@ -707,6 +718,7 @@ export function login_cellphone( phone: number | string countrycode?: number | string captcha: number | string + platform?: LoginPlatform } & RequestBaseConfig, ): Promise @@ -1501,18 +1513,26 @@ export function topic_detail_event_hot( } & RequestBaseConfig, ): Promise -export function login_qr_key(params: RequestBaseConfig): Promise +export function login_qr_key( + params: { platform?: LoginPlatform; lastUnikey?: string } & RequestBaseConfig, +): Promise export function login_qr_create( params: { key?: number | string qrimg?: boolean | string + platform?: LoginPlatform + chainId?: string } & RequestBaseConfig, ): Promise export function login_qr_check( params: { key?: number | string + platform?: LoginPlatform + chainId?: string + secureCaptcha?: string + ydDeviceToken?: string } & RequestBaseConfig, ): Promise diff --git a/module/captcha_sent.js b/module/captcha_sent.js index fdf7231b..d31e9b3f 100644 --- a/module/captcha_sent.js +++ b/module/captcha_sent.js @@ -1,9 +1,27 @@ // 发送验证码 const createOption = require('../util/option.js') +const { + createMobileEapiOption, + isMobilePlatform, +} = require('../util/mobile.js') module.exports = (query, request) => { + if (isMobilePlatform(query)) { + const data = { + ctcode: query.ctcode || query.countrycode || '86', + cellphone: query.phone, + os: 'iOS', + fromPage: query.fromPage || 'RN', + rnBundleVersion: query.rnBundleVersion || '0.0.5', + rnBundleName: query.rnBundleName || 'new-rn-login', + verifyId: 1, + e_r: query.e_r === undefined ? true : query.e_r, + } + return request(`/api/sms/captcha/sent`, data, createMobileEapiOption(query)) + } + const data = { - ctcode: query.ctcode || '86', + ctcode: query.ctcode || query.countrycode || '86', secrete: 'music_middleuser_pclogin', cellphone: query.phone, } diff --git a/module/captcha_verify.js b/module/captcha_verify.js index 403bec2f..a9b11aae 100644 --- a/module/captcha_verify.js +++ b/module/captcha_verify.js @@ -1,9 +1,32 @@ // 校验验证码 const createOption = require('../util/option.js') +const { + createMobileEapiOption, + isMobilePlatform, +} = require('../util/mobile.js') module.exports = (query, request) => { + if (isMobilePlatform(query)) { + const data = { + ctcode: query.ctcode || query.countrycode || '86', + cellphone: query.phone, + captcha: query.captcha, + os: 'iOS', + fromPage: query.fromPage || 'RN', + rnBundleVersion: query.rnBundleVersion || '0.0.5', + rnBundleName: query.rnBundleName || 'new-rn-login', + verifyId: 1, + e_r: query.e_r === undefined ? true : query.e_r, + } + return request( + `/api/sms/captcha/verify`, + data, + createMobileEapiOption(query), + ) + } + const data = { - ctcode: query.ctcode || '86', + ctcode: query.ctcode || query.countrycode || '86', cellphone: query.phone, captcha: query.captcha, } diff --git a/module/login_cellphone.js b/module/login_cellphone.js index c1d8d452..38bd4780 100644 --- a/module/login_cellphone.js +++ b/module/login_cellphone.js @@ -3,22 +3,38 @@ const CryptoJS = require('crypto-js') const createOption = require('../util/option.js') +const { + createMobileEapiOption, + isMobilePlatform, +} = require('../util/mobile.js') module.exports = async (query, request) => { + const countrycode = query.countrycode || query.ctcode || '86' const data = { type: '1', https: 'true', phone: query.phone, - countrycode: query.countrycode || '86', - captcha: query.captcha, + countrycode, [query.captcha ? 'captcha' : 'password']: query.captcha ? query.captcha : query.md5_password || CryptoJS.MD5(query.password).toString(), remember: 'true', } + const mobile = isMobilePlatform(query) + + if (mobile) { + data.rememberLogin = 'true' + data.os = 'iOS' + data.fromPage = query.fromPage || 'RN' + data.rnBundleVersion = query.rnBundleVersion || '0.0.5' + data.rnBundleName = query.rnBundleName || 'new-rn-login' + data.verifyId = 1 + data.e_r = query.e_r === undefined ? true : query.e_r + } + let result = await request( - `/api/w/login/cellphone`, + mobile ? `/api/login/cellphone` : `/api/w/login/cellphone`, data, - createOption(query, 'weapi'), + mobile ? createMobileEapiOption(query) : createOption(query, 'weapi'), ) if (result.body.code === 200) { diff --git a/module/login_qr_check.js b/module/login_qr_check.js index 1f34cb09..d78d6d2b 100644 --- a/module/login_qr_check.js +++ b/module/login_qr_check.js @@ -1,29 +1,85 @@ const createOption = require('../util/option.js') +const { generateChainId } = require('../util/index') +const { + cookieHeaderToList, + createWebQrOption, + mergeCookieLists, +} = require('../util/web_qr_login.js') + +const qrLoginChainIds = + globalThis.__neteaseQrLoginChainIds || + (globalThis.__neteaseQrLoginChainIds = new Map()) + module.exports = async (query, request) => { + const platform = query.platform || 'web' + const chainId = + query.chainId || + qrLoginChainIds.get(String(query.key)) || + (platform === 'web' ? generateChainId(query.cookie || '') : '') const data = { key: query.key, - type: 3, + type: platform === 'web' ? 1 : 3, } + if (platform === 'web') { + data.noCheckToken = true + if (query.secureCaptcha) data.secureCaptcha = query.secureCaptcha + data.ydDeviceToken = query.ydDeviceToken || '' + } + + let option = createOption(query, '') + if (platform === 'web') { + option = createWebQrOption(query, { + 'X-loginMethod': 'QrCode', + 'x-login-chain-id': chainId, + }) + } + try { - let result = await request( - `/api/login/qrcode/client/login`, - data, - createOption(query), - ) + let result = await request(`/api/login/qrcode/client/login`, data, option) + let responseCookies = result.cookie + if (platform === 'web') { + responseCookies = mergeCookieLists( + option.webQrCookieSet, + result.cookie, + cookieHeaderToList(result.body.cookie), + ) + } + const responseCookie = result.body.cookie || responseCookies.join(';') + if ([800, 803].includes(result.body.code)) { + qrLoginChainIds.delete(String(query.key)) + } result = { status: 200, body: { ...result.body, - cookie: result.cookie.join(';'), + cookie: responseCookie, }, - cookie: result.cookie, + cookie: responseCookies, } return result } catch (error) { + if ([800, 803].includes(error.body && error.body.code)) { + qrLoginChainIds.delete(String(query.key)) + } + let body = error.body || {} + let cookies = error.cookie || [] + if (platform === 'web') { + cookies = mergeCookieLists( + option.webQrCookieSet, + error.cookie, + cookieHeaderToList(error.body && error.body.cookie), + ) + if (error.body) { + body = { + ...error.body, + cookie: error.body.cookie || cookies.join(';'), + } + } + } return { status: 200, - body: {}, - cookie: result.cookie, + body, + cookie: cookies, } } } diff --git a/module/login_qr_create.js b/module/login_qr_create.js index d94cbece..f32c6f80 100644 --- a/module/login_qr_create.js +++ b/module/login_qr_create.js @@ -1,20 +1,26 @@ const QRCode = require('qrcode') const { generateChainId } = require('../util/index') +const qrLoginChainIds = + globalThis.__neteaseQrLoginChainIds || + (globalThis.__neteaseQrLoginChainIds = new Map()) + module.exports = (query) => { return new Promise(async (resolve) => { - const platform = query.platform || 'pc' + const platform = query.platform || 'web' const cookie = query.cookie || '' + const chainId = + platform === 'web' ? query.chainId || generateChainId(cookie) : '' - // 构建基础URL - let url = `https://music.163.com/login?codekey=${query.key}` - - // 如果是web平台,则添加chainId参数 - - if (platform === 'web') { - const chainId = generateChainId(cookie) - url += `&chainId=${chainId}` + if (query.key && chainId) { + qrLoginChainIds.set(String(query.key), chainId) } + + // 构建基础URL + let url = + platform === 'web' + ? `https://music.163.com/st/platform/scanlogin?codekey=${query.key}&chainId=${encodeURIComponent(chainId)}&hdw_device=web&hdw_appid=web&hitExp=1` + : `https://music.163.com/login?codekey=${query.key}` return resolve({ code: 200, status: 200, @@ -23,6 +29,7 @@ module.exports = (query) => { data: { qrurl: url, qrimg: query.qrimg ? await QRCode.toDataURL(url) : '', + chainId, }, }, }) diff --git a/module/login_qr_key.js b/module/login_qr_key.js index 4ef36484..dade778d 100644 --- a/module/login_qr_key.js +++ b/module/login_qr_key.js @@ -1,19 +1,31 @@ const createOption = require('../util/option.js') +const { + createWebQrOption, + mergeCookieLists, +} = require('../util/web_qr_login.js') module.exports = async (query, request) => { + const platform = query.platform || 'web' const data = { - type: 3, + type: platform === 'web' ? 1 : 3, } - const result = await request( - `/api/login/qrcode/unikey`, - data, - createOption(query), - ) + if (platform === 'web') { + data.noCheckToken = true + if (query.lastUnikey) data.lastUnikey = query.lastUnikey + } + const option = + platform === 'web' ? createWebQrOption(query) : createOption(query, '') + + const result = await request(`/api/login/qrcode/unikey`, data, option) + const cookies = + platform === 'web' + ? mergeCookieLists(option.webQrCookieSet, result.cookie) + : result.cookie return { status: 200, body: { data: result.body, code: 200, }, - cookie: result.cookie, + cookie: cookies, } } diff --git a/public/docs/home.md b/public/docs/home.md index 5d1f87ae..d3008486 100644 --- a/public/docs/home.md +++ b/public/docs/home.md @@ -338,6 +338,8 @@ body { 说明: 调用此接口可生成一个 key +可选参数: `platform` 默认为 `web`, 会使用网页版二维码登录链路; 如需旧版链路可传 `pc` + **接口地址 :** `/login/qr/key` ##### 2. 二维码生成接口 @@ -348,19 +350,29 @@ body { 可选参数: `qrimg` 传入后会额外返回二维码图片 base64 编码 +可选参数: `platform` 默认为 `web`, 会生成网页版 `/st/platform/scanlogin` 二维码并返回 `chainId`; 如需旧版二维码可传 `pc` + +可选参数: `chainId` 网页版登录链路 ID, 不传时会自动生成并在返回的 `data.chainId` 中给出 + **接口地址 :** `/login/qr/create` -**调用例子 :** `/login/qr/create?key=xxx` +**调用例子 :** `/login/qr/create?key=xxx&platform=web&qrimg=true` ##### 3. 二维码检测扫码状态接口 -说明: 轮询此接口可获取二维码扫码状态,800 为二维码过期,801 为等待扫码,802 为待确认,803 为授权登录成功(803 状态码下会返回 cookies),如扫码后返回 502,则需加上 noCookie 参数,如`&noCookie=true` +说明: 轮询此接口可获取二维码扫码状态,800 为二维码过期,801 为等待扫码,802 为待确认,8821 为网页端安全验证,803 为授权登录成功(803 状态码下会返回 cookies),如扫码后返回 502,则需加上 noCookie 参数,如`&noCookie=true` 必选参数: `key`,由第一个接口生成 +可选参数: `platform` 默认为 `web`, 与二维码生成接口保持一致 + +可选参数: `chainId` 建议传入 `/login/qr/create` 返回的 `data.chainId` + +可选参数: `secureCaptcha` 网页端返回 8821 后, 需完成易盾验证码并将 `validate` 值传入后继续轮询 + **接口地址 :** `/login/qr/check` -**调用例子 :** `/login/qr/check?key=xxx` +**调用例子 :** `/login/qr/check?key=xxx&platform=web&chainId=xxx` 调用可参考项目文件例子 diff --git a/public/qrlogin-nocookie.html b/public/qrlogin-nocookie.html index 2c7daa6c..a8432a7e 100644 --- a/public/qrlogin-nocookie.html +++ b/public/qrlogin-nocookie.html @@ -139,11 +139,12 @@

二维码登录

const res2 = await axios({ url: `/login/qr/create?key=${key}&platform=web&qrimg=true×tamp=${Date.now()}`, }) + const chainId = res2.data.data.chainId document.querySelector('#qrImg').src = res2.data.data.qrimg updateStatus('请扫描二维码', 'waiting') timer = setInterval(async () => { - const statusRes = await checkStatus(key) + const statusRes = await checkStatus(key, chainId) if (statusRes.code === 800) { updateStatus('二维码已过期,请刷新页面', 'error') clearInterval(timer) @@ -151,6 +152,9 @@

二维码登录

updateStatus('等待手机扫码...', 'waiting') } else if (statusRes.code === 802) { updateStatus('二维码已扫描,请在手机上确认', 'waiting') + } else if (statusRes.code === 8821) { + updateStatus('需要完成网页登录安全验证', 'error') + clearInterval(timer) } else if (statusRes.code === 803) { clearInterval(timer) updateStatus('授权登录成功,正在保存信息...', 'success') @@ -164,9 +168,9 @@

二维码登录

} } - async function checkStatus(key) { + async function checkStatus(key, chainId) { const res = await axios({ - url: `/login/qr/check?key=${key}×tamp=${Date.now()}&noCookie=true`, + url: `/login/qr/check?key=${key}&platform=web&chainId=${encodeURIComponent(chainId)}×tamp=${Date.now()}&noCookie=true`, }) return res.data } diff --git a/public/qrlogin.html b/public/qrlogin.html index 1ad80939..d68ff857 100644 --- a/public/qrlogin.html +++ b/public/qrlogin.html @@ -137,13 +137,14 @@

二维码登录

const key = res.data.data.unikey const res2 = await axios({ - url: `/login/qr/create?key=${key}&platform=web&qrimg=true×tamp=${Date.now()}&ua=pc`, + url: `/login/qr/create?key=${key}&platform=web&qrimg=true×tamp=${Date.now()}`, }) + const chainId = res2.data.data.chainId document.querySelector('#qrImg').src = res2.data.data.qrimg updateStatus('请扫描二维码', 'waiting') timer = setInterval(async () => { - const statusRes = await checkStatus(key) + const statusRes = await checkStatus(key, chainId) if (statusRes.code === 800) { updateStatus('二维码已过期,请刷新页面', 'error') clearInterval(timer) @@ -151,6 +152,9 @@

二维码登录

updateStatus('等待手机扫码...', 'waiting') } else if (statusRes.code === 802) { updateStatus('二维码已扫描,请在手机上确认', 'waiting') + } else if (statusRes.code === 8821) { + updateStatus('需要完成网页登录安全验证', 'error') + clearInterval(timer) } else if (statusRes.code === 803) { clearInterval(timer) updateStatus('授权登录成功,正在保存信息...', 'success') @@ -164,9 +168,9 @@

二维码登录

} } - async function checkStatus(key) { + async function checkStatus(key, chainId) { const res = await axios({ - url: `/login/qr/check?key=${key}×tamp=${Date.now()}&ua=pc`, + url: `/login/qr/check?key=${key}&platform=web&chainId=${encodeURIComponent(chainId)}×tamp=${Date.now()}`, }) return res.data } diff --git a/util/crypto.js b/util/crypto.js index 775b1b8b..07bce302 100644 --- a/util/crypto.js +++ b/util/crypto.js @@ -95,22 +95,30 @@ const eapi = (url, object) => { } } const eapiResDecrypt = (encryptedParams, aeapi = false) => { - // 使用aesDecrypt解密参数 + const maybeGunzip = (buffer) => { + if (buffer.length > 2 && buffer[0] === 0x1f && buffer[1] === 0x8b) { + return zlib.gunzipSync(buffer) + } + return buffer + } + try { - const decrypted = aesDecrypt(encryptedParams, eapiKey, '', 'hex') // WordArray + let encryptedBuffer = Buffer.from(encryptedParams, 'hex') + if (aeapi) encryptedBuffer = maybeGunzip(encryptedBuffer) - if (aeapi) { - // 带压缩的解密:先转 Base64 再解压 - const decryptedBuffer = Buffer.from( - decrypted.toString(CryptoJS.enc.Base64), - 'base64', - ) - const decompressed = zlib.gunzipSync(decryptedBuffer) - return JSON.parse(decompressed.toString()) - } else { - // 普通解密:直接转 UTF-8 字符串 - return JSON.parse(decrypted.toString(CryptoJS.enc.Utf8)) - } + const decrypted = aesDecrypt( + encryptedBuffer.toString('hex'), + eapiKey, + '', + 'hex', + ) + let decryptedBuffer = Buffer.from( + decrypted.toString(CryptoJS.enc.Base64), + 'base64', + ) + if (aeapi) decryptedBuffer = maybeGunzip(decryptedBuffer) + + return JSON.parse(decryptedBuffer.toString()) } catch (error) { console.log(`eapiResDecrypt error:`, error) return null diff --git a/util/index.js b/util/index.js index e76d6651..224025e4 100644 --- a/util/index.js +++ b/util/index.js @@ -164,7 +164,9 @@ module.exports = { const version = 'v1' const randomNum = Math.floor(Math.random() * 1e6) const deviceId = - getCookieValue(cookie, 'sDeviceId') || 'unknown-' + randomNum + (cookie && typeof cookie === 'object' + ? cookie.sDeviceId + : getCookieValue(cookie, 'sDeviceId')) || 'unknown-' + randomNum const platform = 'web' const action = 'login' const timestamp = Date.now() diff --git a/util/mobile.js b/util/mobile.js new file mode 100644 index 00000000..3bf34568 --- /dev/null +++ b/util/mobile.js @@ -0,0 +1,63 @@ +const createOption = require('./option.js') +const { cookieToJson } = require('./index') +const { APP_CONF } = require('./config.json') + +const MOBILE_LOGIN_PLATFORMS = new Set(['app', 'ios', 'iphone', 'mobile']) +const IOS_APPVER = '9.5.37' +const IOS_OSVER = '18.7.2' +const IOS_BUILDVER = '7010' +const IOS_USER_AGENT = `neteasemusic/${IOS_APPVER} (iPhone; iOS ${IOS_OSVER}; Scale/3.00)` +const MOBILE_API_DOMAIN = + APP_CONF.xeapiDomain || 'https://interface3.music.163.com' + +const isMobilePlatform = (query = {}) => { + const platform = String(query.platform || '').toLowerCase() + return query.mobile === true || MOBILE_LOGIN_PLATFORMS.has(platform) +} + +const normalizeCookie = (cookie) => { + if (!cookie) return {} + return typeof cookie === 'string' ? cookieToJson(cookie) : cookie +} + +const createMobileEapiOption = (query = {}) => { + const e_r = query.e_r === undefined ? true : query.e_r + const option = createOption( + { + ...query, + domain: query.domain || MOBILE_API_DOMAIN, + e_r, + }, + 'eapi', + ) + + option.cookie = { + ...normalizeCookie(option.cookie), + os: 'iPhone OS', + osver: IOS_OSVER, + appver: IOS_APPVER, + buildver: IOS_BUILDVER, + channel: 'distribution', + } + option.domain = query.domain || MOBILE_API_DOMAIN + option.ua = query.ua || IOS_USER_AGENT + option.e_r = e_r + option.emptyHeader = true + option.mobileEapi = true + option.headers = { + ...option.headers, + 'content-type': 'application/x-www-form-urlencoded', + 'x-aeapi': 'true', + 'x-os': 'iPhone OS', + 'x-osver': IOS_OSVER, + 'x-appver': IOS_APPVER, + 'x-buildver': IOS_BUILDVER, + } + + return option +} + +module.exports = { + createMobileEapiOption, + isMobilePlatform, +} diff --git a/util/request.js b/util/request.js index 4d6ef4a2..d2bad1b6 100644 --- a/util/request.js +++ b/util/request.js @@ -15,6 +15,7 @@ const { cookieObjToString, toBoolean, generateRandomChineseIP, + generateDeviceId, } = require('./index') const { URLSearchParams, URL } = require('url') const { APP_CONF } = require('../util/config.json') @@ -84,8 +85,9 @@ const osMap = { }, iphone: { os: 'iPhone OS', - appver: '9.0.90', - osver: '16.2', + appver: '9.5.37', + osver: '18.7.2', + buildver: '7010', channel: 'distribution', }, osx: { @@ -109,7 +111,7 @@ const userAgentMap = { pc: 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/91.0.4472.164 NeteaseMusicDesktop/3.0.18.203152', android: 'NeteaseMusic/9.1.65.240927161425(9001065);Dalvik/2.1.0 (Linux; U; Android 14; 23013RK75C Build/UKQ1.230804.001)', - iphone: 'NeteaseMusic 9.0.90/5038 (iPhone; iOS 16.2; zh_CN)', + iphone: 'neteasemusic/9.5.37 (iPhone; iOS 18.7.2; Scale/3.00)', }, } @@ -118,7 +120,9 @@ const DOMAIN = APP_CONF.domain const API_DOMAIN = APP_CONF.apiDomain const XEAPI_DOMAIN = APP_CONF.xeapiDomain const ENCRYPT_RESPONSE = APP_CONF.encryptResponse -const SPECIAL_STATUS_CODES = new Set([201, 302, 400, 502, 800, 801, 802, 803]) +const SPECIAL_STATUS_CODES = new Set([ + 201, 302, 400, 502, 800, 801, 802, 803, 8821, +]) let xeapiSessionId = '' let xeapiSessionKey = '' @@ -132,6 +136,9 @@ const chooseUserAgent = (crypto, uaType = 'pc') => { const processCookieObject = (cookie, uri) => { const _ntes_nuid = CryptoJS.lib.WordArray.random(32).toString() const os = osMap[cookie.os] || osMap['pc'] + const normalizedOs = osMap[cookie.os] ? os.os : cookie.os || os.os + const deviceId = cookie.deviceId || global.deviceId || generateDeviceId() + global.deviceId = deviceId const processedCookie = { ...cookie, @@ -142,10 +149,12 @@ const processCookieObject = (cookie, uri) => { WNMCID: cookie.WNMCID || WNMCID, WEVNSM: cookie.WEVNSM || '1.0.0', osver: cookie.osver || os.osver, - deviceId: cookie.deviceId || global.deviceId, - os: cookie.os || os.os, + deviceId, + sDeviceId: cookie.sDeviceId || cookie.sdeviceid || deviceId, + os: normalizedOs, channel: cookie.channel || os.channel, appver: cookie.appver || os.appver, + buildver: cookie.buildver || os.buildver, } if (uri.indexOf('login') === -1) { @@ -197,7 +206,11 @@ const createRequest = (uri, data, options) => { cookie = cookieToJson(cookie) } - if (typeof cookie === 'object') { + if (options.skipCookieProcessing) { + headers['Cookie'] = + options.rawCookie || + (typeof cookie === 'object' ? cookieObjToString(cookie) : '') + } else if (typeof cookie === 'object') { cookie = processCookieObject(cookie, uri) headers['Cookie'] = cookieObjToString(cookie) } @@ -223,7 +236,7 @@ const createRequest = (uri, data, options) => { // 根据加密方式处理 switch (crypto) { case 'weapi': - headers['Referer'] = options.domain || DOMAIN + headers['Referer'] = headers['Referer'] || options.domain || DOMAIN headers['User-Agent'] = options.ua || chooseUserAgent('weapi') data.csrf_token = csrfToken encryptData = encrypt.weapi(data) @@ -319,8 +332,21 @@ const createRequest = (uri, data, options) => { : chooseUserAgent('api', 'iphone')) if (crypto === 'eapi') { + if (cookie.deviceId) { + headers['x-deviceid'] = headers['x-deviceid'] || cookie.deviceId + headers['x-sdeviceid'] = + headers['x-sdeviceid'] || cookie.sDeviceId || cookie.deviceId + } + headers['x-os'] = headers['x-os'] || cookie.os + headers['x-osver'] = headers['x-osver'] || cookie.osver + headers['x-appver'] = headers['x-appver'] || cookie.appver + headers['x-buildver'] = headers['x-buildver'] || header.buildver + if (cookie.MUSIC_U) headers['x-music-u'] = cookie.MUSIC_U + if (options.mobileEapi && cookie.deviceId && !data.deviceId) { + data.deviceId = cookie.deviceId + } // headers['x-aeapi'] = true // 服务器会使用gzip压缩返回值 - data.header = header + data.header = options.emptyHeader ? {} : header encryptData = encrypt.eapi(uri, data) url = (options.domain || API_DOMAIN) + '/eapi/' + uri.substr(5) diff --git a/util/web_qr_login.js b/util/web_qr_login.js new file mode 100644 index 00000000..8aaf0984 --- /dev/null +++ b/util/web_qr_login.js @@ -0,0 +1,94 @@ +const crypto = require('crypto') +const createOption = require('./option.js') +const { cookieObjToString, cookieToJson } = require('./index') + +const WEB_QR_USER_AGENT = + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:152.0) Gecko/20100101 Firefox/152.0' + +const randomFrom = (alphabet, length) => { + let value = '' + for (let i = 0; i < length; i++) { + value += alphabet[crypto.randomInt(0, alphabet.length)] + } + return value +} + +const randomHex = (bytes) => crypto.randomBytes(bytes).toString('hex') + +const randomWebToken = (length) => + randomFrom( + '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_', + length, + ) + +const normalizeCookie = (cookie) => { + if (!cookie) return {} + return typeof cookie === 'string' ? cookieToJson(cookie) : cookie +} + +const createWebQrCookie = (cookie) => { + const current = normalizeCookie(cookie) + const nuid = current._ntes_nuid || randomHex(16) + return { + ...current, + 'JSESSIONID-WYYY': current['JSESSIONID-WYYY'] || randomWebToken(190), + _iuqxldmzr_: current._iuqxldmzr_ || '33', + _ntes_nnid: current._ntes_nnid || `${nuid},${Date.now()}`, + _ntes_nuid: nuid, + NMTID: current.NMTID || `00${randomWebToken(39)}`, + WEVNSM: current.WEVNSM || '1.0.0', + WNMCID: + current.WNMCID || + `${randomFrom('abcdefghijklmnopqrstuvwxyz', 6)}.${Date.now()}.01.0`, + } +} + +const cookieToSetCookieList = (cookie) => + Object.keys(cookie).map((key) => `${key}=${cookie[key]}`) + +const cookieHeaderToList = (cookie = '') => + String(cookie) + .split(';') + .map((item) => item.trim()) + .filter(Boolean) + +const mergeCookieLists = (...lists) => { + const merged = new Map() + const add = (cookie) => { + const value = String(cookie || '').split(';')[0] + const index = value.indexOf('=') + if (index <= 0) return + merged.set(value.slice(0, index), value) + } + + lists.flat().forEach(add) + return [...merged.values()] +} + +const createWebQrOption = (query = {}, headers = {}) => { + const option = createOption(query, 'weapi') + const cookie = createWebQrCookie(option.cookie) + + option.cookie = cookie + option.rawCookie = cookieObjToString(cookie) + option.skipCookieProcessing = true + option.ua = query.ua || WEB_QR_USER_AGENT + option.headers = { + ...option.headers, + Referer: 'https://music.163.com/', + Origin: 'https://music.163.com', + 'x-os': 'web', + 'X-channelSource': 'undefined', + 'Nm-GCore-Status': '1', + ...headers, + } + option.webQrCookieSet = cookieToSetCookieList(cookie) + + return option +} + +module.exports = { + cookieHeaderToList, + createWebQrOption, + mergeCookieLists, +}