Files
yenai-plugin/lib/request/request.js
2024-04-20 00:51:25 +08:00

241 lines
7.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import fetch from "node-fetch"
import { Config, Plugin_Path } from "../../components/index.js"
import { Agent } from "https"
import { HttpsProxyAgent } from "./httpsProxyAgentMod.js"
import _ from "lodash"
const CHROME_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Safari/537.36"
const POSTMAN_UA = "PostmanRuntime/7.29.0"
class HTTPResponseError extends Error {
constructor(response) {
super(`HTTP Error Response: ${response.status} ${response.statusText}`)
this.response = response
}
}
class RequestError extends Error {
constructor(message) {
super(message)
this.name = "RequestError"
}
}
const checkStatus = response => {
if (response.ok) {
// response.status >= 200 && response.status < 300
return response
} else {
throw new HTTPResponseError(response)
}
}
export const qs = (obj) => {
let res = ""
for (const [ k, v ] of Object.entries(obj)) { res += `${k}=${encodeURIComponent(v)}&` }
return res.slice(0, res.length - 1)
}
const mergeOptions = (defaultOptions, userOptions) => {
const _defaultOptions = {
outErrorLog: true
}
// 优化headers的合并逻辑确保安全性
const headers = { ...defaultOptions.headers, ...userOptions.headers }
return { ..._defaultOptions, ...defaultOptions, ...userOptions, headers }
}
export default new class {
/**
* 发送HTTP GET请求并返回响应
* @async
* @name get
* @param {string} url - 请求的URL
* @param {object} [options] - 请求的配置项
* @param {object} [options.params] - 请求的参数
* @param {object} [options.headers] - 请求的HTTP头部
* @param {boolean} [options.closeCheckStatus] - 是否关闭状态检查
* @param {'buffer'|'json'|'text'|'arrayBuffer'|'formData'|'blob'}[options.statusCode] - 期望的返回数据如果设置了该值则返回响应数据的特定的方法如json()、text()等)
* @param {boolean} [options.origError] 出现错误是否返回原始错误
* @param {boolean} [options.outErrorLog] 出现错误是否在控制台打印错误日志默认为true
* @returns {Promise<Response|*>} - HTTP响应或响应数据
* @throws {Error} - 如果请求失败,则抛出错误,将`options.origError`设置为true则抛出原始错误
*/
async get(url, options = {}) {
options = mergeOptions({ method: "GET", url }, options)
options = this._prepareRequest(options)
return this._reques(options)
}
/**
* 发送HTTP POST请求并返回响应
* @async
* @function
* @param {string} url - 请求的URL
* @param {object} [options] - 请求的配置项
* @param {object} [options.data] - 请求的数据
* @param {object} [options.params] - 请求的参数
* @param {object} [options.headers] - 请求的HTTP头部
* @param {boolean} [options.closeCheckStatus] - 是否关闭状态检查
* @param {'buffer'|'json'|'text'|'arrayBuffer'|'formData'|'blob'} [options.statusCode] - 期望的返回数据如果设置了该值则返回响应数据的特定的方法如json()、text()等)
* @param {boolean} [options.origError] 出现错误是否返回原始错误
* @param {boolean} [options.outErrorLog] 出现错误是否在控制台打印错误日志默认为true
* @returns {Promise<Response|*>} - HTTP响应或响应数据
* @throws {Error} - 如果请求失败,则抛出错误,将`options.origError`设置为true则抛出原始错误
*/
async post(url, options = {}) {
options = mergeOptions({
method: "POST", headers: { "Content-Type": "application/json" }, url
}, options)
options = this._prepareRequest(options)
if (options.data) {
logger.debug("[Yenai-Plugin]POST request params data: ", options.data)
if (/json/.test(options.headers["Content-Type"])) {
options.body = JSON.stringify(options.data)
} else if (
/x-www-form-urlencoded/.test(options.headers["Content-Type"])
) {
options.body = qs(options.data)
} else {
options.body = options.data
}
delete options.data
}
return this._reques(options)
}
/**
* 绕cf Get请求
* @param {string} url
* @param {object} options 同fetch第二参数
* @param {object} options.params 请求参数
* @returns {Promise<Response|*>}
*/
async cfGet(url, options = {}) {
options.agent = this.getAgent(true)
options.headers = {
"User-Agent": POSTMAN_UA,
...options.headers
}
return this.get(url, options)
}
/**
* 绕cf Post请求
* @param {string} url
* @param {object} options 同fetch第二参数
* @param {object | string} options.data 请求参数
* @returns {Promise<Response|*>}
*/
async cfPost(url, options = {}) {
options.agent = this.getAgent(true)
options.headers = {
"User-Agent": POSTMAN_UA,
...options.headers
}
return this.post(url, options)
}
getAgent(cf) {
let { proxyAddress, switchProxy } = Config.proxy
let { cfTLSVersion } = Config.picSearch
return cf
? this.getTlsVersionAgent(proxyAddress, cfTLSVersion)
: switchProxy
? new HttpsProxyAgent(proxyAddress)
: false
}
/**
* 从代理字符串获取指定 TLS 版本的代理
* @param {string} str
* @param {import('tls').SecureVersion} tlsVersion
*/
getTlsVersionAgent(str, tlsVersion) {
const tlsOpts = {
maxVersion: tlsVersion,
minVersion: tlsVersion
}
if (typeof str === "string") {
const isHttp = str.startsWith("http")
if (isHttp && Config.proxy.switchProxy) {
const opts = {
..._.pick(new URL(str), [
"protocol",
"hostname",
"port",
"username",
"password"
]),
tls: tlsOpts
}
return new HttpsProxyAgent(opts)
}
}
return new Agent(tlsOpts)
}
/**
* 代理请求图片
* @param {string} url 图片链接
* @param {object} options 配置
* @param {boolean} options.cache 是否缓存
* @param {number} options.timeout 超时时间
* @param {object} options.headers 请求头
* @returns {Promise<import('icqq').ImageElem>} 构造图片消息
*/
async proxyRequestImg(url, { cache, timeout, headers } = {}) {
if (!this.getAgent()) return segment.image(url, cache, timeout, headers)
let Request = await this.get(url, {
headers
}).catch(err => logger.error(err))
return segment.image(Request?.body ?? `${Plugin_Path}/resources/img/imgerror.png`, cache, timeout)
}
_prepareRequest(options) {
// 处理参数
if (options.params) {
options.url = `${options.url}?${qs(options.params)}`
}
logger.debug(`[Yenai-Plugin] ${options.method.toUpperCase()}请求:${decodeURI(options.url)}`)
options.headers = {
"User-Agent": options.headers && options.headers["User-Agent"] ? options.headers["User-Agent"] : CHROME_UA,
...options.headers
}
if (options.agent == undefined) options.agent = this.getAgent(options.cf)
return options
}
async _reques(options) {
try {
let res = await fetch(options.url, options)
res = await this._handleRes(res, options)
return res
} catch (err) {
this._handleError(err, options)
}
}
_handleRes(res, options) {
if (!options.closeCheckStatus) {
res = checkStatus(res)
}
if (options.statusCode) {
return res[options.statusCode]()
}
return res
}
_handleError(err, options) {
options.outErrorLog && logger.error(err)
if (options.origError) throw err
throw new RequestError(
`${options.method.toUpperCase()} Error${err.message.match(/reason:(.*)/)?.[1] || err.message}`
)
}
}()