Direct Proxy Helper - 直接代理助手

直接使用 GM_xmlhttpRequest 实现代理请求,无需外部服务器,绕过 CORS 限制

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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. 完全免费

*/