直接使用 GM_xmlhttpRequest 实现代理请求,无需外部服务器,绕过 CORS 限制
// ==UserScript==
// @name Direct Proxy Helper - 直接代理助手
// @namespace https://greasyfork.org/zh-CN/scripts/552407
// @version 1.0.3
// @description 直接使用 GM_xmlhttpRequest 实现代理请求,无需外部服务器,绕过 CORS 限制
// @author qa
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @connect *
// @run-at document-start
// @license MIT
// @homepageURL https://greasyfork.org/zh-CN/scripts/552407
// @supportURL https://greasyfork.org/zh-CN/scripts/552407/feedback
// ==/UserScript==
;(function () {
'use strict'
// ========== 配置区域 ==========
const CONFIG = {
// 调试输出现在通过 localStorage.imagedrive_debug 控制 (值为 'true')
// 默认超时时间(毫秒)
defaultTimeout: 30000
}
// 调试日志函数(仅当 localStorage.imagedrive_debug === 'true' 时输出)
function log(...args) {
try {
const enabled = typeof window !== 'undefined' && window.localStorage && window.localStorage.getItem('imagedrive_debug') === 'true'
if (enabled) {
console.log('[Direct Proxy]', ...args)
}
} catch (e) {
// 如果访问 localStorage 出错,则静默失败,不影响正常逻辑
}
}
/**
* 直接代理请求函数 - 使用 GM_xmlhttpRequest 直接发送请求
* @param {Object} options - 请求配置
* @param {string} options.url - 目标 URL
* @param {string} [options.method='GET'] - 请求方法
* @param {Object} [options.headers={}] - 自定义请求头
* @param {string|FormData|Object} [options.data] - 请求体数据
* @param {string} [options.responseType='text'] - 响应类型: 'text', 'json', 'blob', 'arraybuffer', 'document'
* @param {Function} [options.onSuccess] - 成功回调
* @param {Function} [options.onError] - 失败回调
* @param {Function} [options.onProgress] - 进度回调
* @param {number} [options.timeout] - 超时时间(毫秒)
* @param {boolean} [options.anonymous=false] - 是否匿名请求(不发送 cookies)
* @returns {Promise} - 返回 Promise 对象
*/
function directProxy(options) {
return new Promise((resolve, reject) => {
const {
url,
method = 'GET',
headers = {},
data = null,
responseType = 'text',
onSuccess,
onError,
onProgress,
timeout = CONFIG.defaultTimeout,
anonymous = false
} = options
if (!url) {
const error = new Error('URL is required')
reject(error)
if (onError) onError(error)
return
}
log('发起直接代理请求:', { url, method, headers })
// GM_xmlhttpRequest 配置
const gmOptions = {
method: method.toUpperCase(),
url: url,
headers: { ...headers },
timeout: timeout,
anonymous: anonymous,
onload: function (response) {
log('请求成功:', response.status, response.statusText)
log('原始响应头:', response.responseHeaders)
let result
try {
// 根据 responseType 处理响应
if (responseType === 'json') {
result = JSON.parse(response.responseText)
} else if (responseType === 'text') {
result = response.responseText
} else if (responseType === 'blob' || responseType === 'arraybuffer') {
result = response.response
} else if (responseType === 'document') {
// 解析为 DOM 文档
const parser = new DOMParser()
result = parser.parseFromString(response.responseText, 'text/html')
} else {
result = response.responseText
}
const parsedHeaders = parseResponseHeaders(response.responseHeaders)
// 强制添加/覆盖跨域允许头,便于在页面端判断并兼容缺失或错误的服务器头
try {
parsedHeaders['access-control-allow-origin'] = '*'
} catch (e) {
// ignore
}
log('解析后的响应头:', parsedHeaders)
const finalResponse = {
data: result,
status: response.status,
statusText: response.statusText,
headers: parsedHeaders,
finalUrl: response.finalUrl,
raw: response
}
resolve(finalResponse)
if (onSuccess) onSuccess(finalResponse)
} catch (e) {
log('解析响应失败:', e)
const error = new Error('Failed to parse response: ' + e.message)
error.response = response
reject(error)
if (onError) onError(error)
}
},
onerror: function (response) {
log('请求失败:', response)
const error = new Error(`Request failed: ${response.statusText || 'Network error'}`)
error.response = response
reject(error)
if (onError) onError(error)
},
ontimeout: function () {
log('请求超时')
const error = new Error('Request timeout')
reject(error)
if (onError) onError(error)
},
onprogress: function (progress) {
if (onProgress) {
onProgress({
loaded: progress.loaded,
total: progress.total,
percentage: progress.total > 0 ? (progress.loaded / progress.total) * 100 : 0
})
}
}
}
// 如果未显式指定 Origin,则默认覆盖为目标 URL 的域(protocol + // + host)
try {
const headerKeys = Object.keys(gmOptions.headers || {})
const hasOrigin = headerKeys.some(k => k && k.toLowerCase() === 'origin')
if (!hasOrigin) {
try {
const u = new URL(url)
gmOptions.headers['Origin'] = `${u.protocol}//${u.host}`
log('自动设置 Origin 头为目标域:', gmOptions.headers['Origin'])
} catch (e) {
// 无法解析 URL,则不设置 Origin
log('无法解析 URL 来设置默认 Origin:', e)
}
}
} catch (e) {
// 安全容错,不影响请求
}
// 处理请求体
if (data && method.toUpperCase() !== 'GET' && method.toUpperCase() !== 'HEAD') {
if (typeof data === 'string') {
gmOptions.data = data
} else if (data instanceof FormData) {
gmOptions.data = data
} else if (data instanceof Blob) {
// Blob 对象直接传递给 GM_xmlhttpRequest
gmOptions.data = data
log('使用 Blob 数据, 大小:', data.size, 'bytes')
} else if (data instanceof ArrayBuffer || data instanceof Uint8Array) {
// 二进制数据直接传递
gmOptions.data = data
log('使用二进制数据, 大小:', data.byteLength || data.length, 'bytes')
} else if (typeof data === 'object') {
gmOptions.data = JSON.stringify(data)
// 自动添加 Content-Type (如果没有指定)
if (!gmOptions.headers['Content-Type'] && !gmOptions.headers['content-type']) {
gmOptions.headers['Content-Type'] = 'application/json'
}
}
}
// 设置响应类型
if (responseType === 'blob' || responseType === 'arraybuffer') {
gmOptions.responseType = responseType
}
// 发起请求
GM_xmlhttpRequest(gmOptions)
})
}
/**
* 解析响应头字符串
*/
function parseResponseHeaders(headersString) {
const headers = {}
if (!headersString) {
log('警告: 响应头字符串为空')
return headers
}
// 更宽松且稳健的解析: 按第一个 ':' 分割,去除首尾空白,并将 header 名小写化
headersString
.trim()
.split(/\r?\n/)
.forEach(line => {
if (!line || !line.trim()) return
const idx = line.indexOf(':')
if (idx === -1) return
const name = line.substring(0, idx).trim().toLowerCase()
const value = line.substring(idx + 1).trim()
if (name) {
// 如果存在重复的 header,保留第一个并忽略空值
if (!Object.prototype.hasOwnProperty.call(headers, name) || headers[name] === '') {
headers[name] = value
}
}
})
return headers
}
/**
* 便捷的 GET 请求
*/
function proxyGet(url, options = {}) {
return directProxy({
...options,
url,
method: 'GET'
})
}
/**
* 便捷的 POST 请求
*/
function proxyPost(url, data, options = {}) {
return directProxy({
...options,
url,
method: 'POST',
data
})
}
/**
* 便捷的 PUT 请求
*/
function proxyPut(url, data, options = {}) {
return directProxy({
...options,
url,
method: 'PUT',
data
})
}
/**
* 便捷的 PATCH 请求
*/
function proxyPatch(url, data, options = {}) {
return directProxy({
...options,
url,
method: 'PATCH',
data
})
}
/**
* 便捷的 DELETE 请求
*/
function proxyDelete(url, options = {}) {
return directProxy({
...options,
url,
method: 'DELETE'
})
}
/**
* Fetch API 兼容的代理函数
*/
function proxyFetch(url, options = {}) {
const { method = 'GET', headers = {}, body = null, ...rest } = options
// 自动检测响应类型:如果没有明确指定,默认使用 arraybuffer 以支持二进制数据
// 这样可以正确处理图片、文件等二进制内容
const responseType = rest.responseType || 'arraybuffer'
return directProxy({
url,
method,
headers,
data: body,
responseType: responseType,
...rest
}).then(response => {
// 返回类似 fetch Response 的对象
return {
ok: response.status >= 200 && response.status < 300,
status: response.status,
statusText: response.statusText,
url: response.finalUrl || url,
headers: {
get: name => response.headers[name.toLowerCase()],
has: name => name.toLowerCase() in response.headers,
entries: () => Object.entries(response.headers),
keys: () => Object.keys(response.headers),
values: () => Object.values(response.headers),
forEach: callback => {
Object.entries(response.headers).forEach(([k, v]) => callback(v, k))
}
},
text: () => {
if (typeof response.data === 'string') {
return Promise.resolve(response.data)
} else if (response.data instanceof ArrayBuffer) {
const decoder = new TextDecoder()
return Promise.resolve(decoder.decode(response.data))
} else {
return Promise.resolve(String(response.data))
}
},
json: () => {
try {
let jsonText
if (typeof response.data === 'string') {
jsonText = response.data
} else if (response.data instanceof ArrayBuffer) {
const decoder = new TextDecoder()
jsonText = decoder.decode(response.data)
} else {
jsonText = String(response.data)
}
return Promise.resolve(JSON.parse(jsonText))
} catch (e) {
return Promise.reject(e)
}
},
blob: () => {
if (response.data instanceof Blob) {
return Promise.resolve(response.data)
}
return Promise.resolve(new Blob([response.data]))
},
arrayBuffer: () => {
if (response.data instanceof ArrayBuffer) {
return Promise.resolve(response.data)
}
// 尝试将其他类型转换为 ArrayBuffer
try {
let buffer
if (typeof response.data === 'string') {
// 字符串转 ArrayBuffer
const encoder = new TextEncoder()
buffer = encoder.encode(response.data).buffer
} else if (response.data instanceof Blob) {
// Blob 转 ArrayBuffer
return response.data.arrayBuffer()
} else if (response.data instanceof Uint8Array || response.data instanceof Int8Array) {
buffer = response.data.buffer
} else {
// 其他类型尝试转为字符串再转 ArrayBuffer
const encoder = new TextEncoder()
buffer = encoder.encode(String(response.data)).buffer
}
return Promise.resolve(buffer)
} catch (e) {
return Promise.reject(new Error('Failed to convert response to ArrayBuffer: ' + e.message))
}
},
clone: function () {
return this
},
_original: response
}
})
}
/**
* JSONP 请求辅助函数
*/
function proxyJsonp(url, options = {}) {
const {
callbackParam = 'callback',
callbackName = 'jsonpCallback_' + Date.now() + '_' + Math.random().toString(36).substr(2),
timeout = CONFIG.defaultTimeout
} = options
return new Promise((resolve, reject) => {
const separator = url.includes('?') ? '&' : '?'
const jsonpUrl = `${url}${separator}${callbackParam}=${callbackName}`
// 创建全局回调函数
unsafeWindow[callbackName] = function (data) {
delete unsafeWindow[callbackName]
resolve({
data: data,
status: 200,
statusText: 'OK'
})
}
// 使用 GM_xmlhttpRequest 发起 JSONP 请求
directProxy({
url: jsonpUrl,
method: 'GET',
timeout: timeout,
responseType: 'text'
})
.then(response => {
// 执行响应文本(包含 JSONP 回调)
try {
eval(response.data)
} catch (e) {
delete unsafeWindow[callbackName]
reject(new Error('JSONP parse error: ' + e.message))
}
})
.catch(error => {
delete unsafeWindow[callbackName]
reject(error)
})
// 超时处理
setTimeout(() => {
if (unsafeWindow[callbackName]) {
delete unsafeWindow[callbackName]
reject(new Error('JSONP request timeout'))
}
}, timeout)
})
}
// ========== 注入到页面 ==========
// 将代理函数注入到 unsafeWindow (页面的 window 对象)
unsafeWindow.ProxyHelper = {
// 配置
config: CONFIG,
// 核心请求函数
request: directProxy,
// 便捷方法
get: proxyGet,
post: proxyPost,
put: proxyPut,
patch: proxyPatch,
delete: proxyDelete,
// Fetch 兼容
fetch: proxyFetch,
// JSONP 支持
jsonp: proxyJsonp,
// 工具方法
setDebug: enabled => {
try {
if (enabled) {
localStorage.setItem('imagedrive_debug', 'true')
} else {
localStorage.removeItem('imagedrive_debug')
}
} catch (e) {
// 在受限环境(localStorage 不可用)静默忽略
}
},
setTimeout: ms => {
CONFIG.defaultTimeout = ms
}
}
// 同时注入到油猴脚本的 window 对象
window.ProxyHelper = unsafeWindow.ProxyHelper
log('直接代理助手已注入到页面,可通过 ProxyHelper 访问')
log('此代理直接使用 GM_xmlhttpRequest,无需外部服务器')
log('使用示例:')
log(' ProxyHelper.get("https://api.example.com/data")')
log(' ProxyHelper.post("https://api.example.com/data", { key: "value" })')
log(' ProxyHelper.fetch("https://api.example.com/data").then(r => r.json())')
})()
/*
========== 使用说明 ==========
这是一个完全独立的代理脚本,直接使用 GM_xmlhttpRequest 实现,无需任何外部服务器。
主要特性:
✅ 完全绕过 CORS 限制
✅ 可修改任意请求头
✅ 支持所有 HTTP 方法 (GET, POST, PUT, PATCH, DELETE)
✅ 支持多种响应类型 (text, json, blob, arraybuffer, document)
✅ Promise 和回调两种方式
✅ Fetch API 兼容接口
✅ 进度监控和超时控制
✅ 支持 JSONP 请求
✅ 支持匿名请求(不发送 cookies)
========== 使用示例 ==========
1. 基本 GET 请求:
ProxyHelper.get('https://api.example.com/users')
.then(response => {
console.log('数据:', response.data);
console.log('状态:', response.status);
console.log('响应头:', response.headers);
})
.catch(error => console.error('错误:', error));
2. POST 请求(自定义请求头):
ProxyHelper.post('https://api.example.com/users',
{ name: 'John', email: '[email protected]' },
{
headers: {
'Authorization': 'Bearer your-token-here',
'X-Custom-Header': 'custom-value',
'Content-Type': 'application/json'
}
}
).then(res => console.log(res.data));
3. 使用 Fetch 风格:
ProxyHelper.fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Authorization': 'Bearer token',
'Content-Type': 'application/json'
},
body: JSON.stringify({ key: 'value' })
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));
4. 完整配置的请求:
ProxyHelper.request({
url: 'https://api.example.com/data',
method: 'GET',
headers: {
'User-Agent': 'CustomAgent/1.0',
'Referer': 'https://example.com',
'Authorization': 'Bearer token'
},
responseType: 'json',
timeout: 10000,
anonymous: false, // 是否匿名(不发送 cookies)
onSuccess: (response) => {
console.log('成功:', response.data);
},
onError: (error) => {
console.error('失败:', error);
},
onProgress: (progress) => {
console.log('进度:', progress.percentage.toFixed(2) + '%');
}
});
5. 下载文件(Blob):
ProxyHelper.request({
url: 'https://example.com/file.pdf',
method: 'GET',
responseType: 'blob'
}).then(response => {
const blob = response.data;
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'file.pdf';
a.click();
URL.revokeObjectURL(url);
});
6. JSONP 请求:
ProxyHelper.jsonp('https://api.example.com/data', {
callbackParam: 'callback', // 回调参数名
timeout: 5000
}).then(response => {
console.log('JSONP 数据:', response.data);
});
7. 修改 Referer 和 Origin(绕过防盗链):
ProxyHelper.get('https://protected-api.example.com/image.jpg', {
headers: {
'Referer': 'https://allowed-domain.com/',
'Origin': 'https://allowed-domain.com'
},
responseType: 'blob'
}).then(response => {
const imgUrl = URL.createObjectURL(response.data);
document.querySelector('img').src = imgUrl;
});
8. 跨域 API 调用:
ProxyHelper.fetch('https://cors-blocked-api.com/endpoint', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'your-api-key'
},
body: JSON.stringify({ query: 'data' })
})
.then(res => res.json())
.then(data => console.log(data));
========== 配置方法 ==========
// 启用/禁用调试日志
ProxyHelper.setDebug(false);
// 设置默认超时时间(毫秒)
ProxyHelper.setTimeout(60000);
========== 优势 ==========
相比普通的 fetch 或 XMLHttpRequest:
1. 完全绕过 CORS 限制
2. 可以修改任意请求头(包括 Referer、Origin、User-Agent 等)
3. 支持跨域访问任何网站
4. 不受同源策略限制
相比使用 Cloudflare Worker:
1. 无需部署外部服务器
2. 延迟更低(直接访问目标)
3. 无需维护成本
4. 完全免费
*/