mirror of
https://github.com/purocean/yn.git
synced 2026-10-16 07:26:07 +08:00
feat: add ctx.lib.proxyFetch method
This commit is contained in:
@@ -56,9 +56,11 @@
|
||||
"ripgrep-wrapper": "^1.1.1",
|
||||
"safe-buffer": "^5.2.1",
|
||||
"socket.io": "^4.7.2",
|
||||
"socks": "^2.8.3",
|
||||
"socks-proxy-agent": "^6.1.1",
|
||||
"tar-stream": "^2.2.0",
|
||||
"transliteration": "^2.2.0",
|
||||
"undici": "^6.16.1",
|
||||
"uuid": "^9.0.0",
|
||||
"yaml": "^2.2.2",
|
||||
"yargs": "^15.3.1"
|
||||
|
||||
+3
-1
@@ -15,7 +15,7 @@ import { registerAction } from './action'
|
||||
import { registerShortcut } from './shortcut'
|
||||
import { initJSONRPCClient, jsonRPCClient } from './jsonrpc'
|
||||
import { $t } from './i18n'
|
||||
import { getProxyAgent, newProxyAgent } from './proxy-agent'
|
||||
import { getProxyAgent, getProxyDispatcher, newProxyAgent, newProxyDispatcher } from './proxy-agent'
|
||||
import config from './config'
|
||||
import { initProxy } from './proxy'
|
||||
import { initEnvs } from './envs'
|
||||
@@ -529,6 +529,8 @@ registerAction('quit', quit)
|
||||
registerAction('show-open-dialog', showOpenDialog)
|
||||
registerAction('refresh-menus', refreshMenus)
|
||||
registerAction('get-proxy-agent', getProxyAgent)
|
||||
registerAction('get-proxy-dispatcher', getProxyDispatcher)
|
||||
registerAction('new-proxy-dispatcher', newProxyDispatcher)
|
||||
registerAction('new-proxy-agent', newProxyAgent)
|
||||
|
||||
powerMonitor.on('shutdown', quit)
|
||||
|
||||
@@ -48,6 +48,8 @@ export async function transformProtocolRequest (request: ProtocolRequest) {
|
||||
if (body) {
|
||||
req.headers['content-length'] = body.length.toString()
|
||||
req._read = Readable.from(body)._read
|
||||
} else {
|
||||
req._read = () => req.push(null)
|
||||
}
|
||||
|
||||
const out = new Transform({
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import { session } from 'electron'
|
||||
import { Dispatcher, ProxyAgent } from 'undici'
|
||||
import { socksDispatcher } from './undici-socket-agent'
|
||||
const SocksProxyAgent = require('socks-proxy-agent')
|
||||
const HttpsProxyAgent = require('https-proxy-agent')
|
||||
const HttpProxyAgent = require('http-proxy-agent')
|
||||
@@ -14,6 +16,19 @@ export function newProxyAgent (proxyUrl: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function newProxyDispatcher (proxyUrl: string): Dispatcher {
|
||||
if (proxyUrl.toLowerCase().startsWith('socks://') || proxyUrl.toLowerCase().startsWith('socks5://')) {
|
||||
const url = new URL(proxyUrl)
|
||||
return socksDispatcher({
|
||||
type: 5,
|
||||
host: url.hostname,
|
||||
port: +url.port
|
||||
})
|
||||
}
|
||||
|
||||
return new ProxyAgent(proxyUrl)
|
||||
}
|
||||
|
||||
export async function getProxyAgent (url: string) {
|
||||
const proxy = await session.defaultSession.resolveProxy(url)
|
||||
if (!proxy) {
|
||||
@@ -41,3 +56,27 @@ export async function getProxyAgent (url: string) {
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export async function getProxyDispatcher (url: string): Promise<Dispatcher | undefined> {
|
||||
const proxy = await session.defaultSession.resolveProxy(url)
|
||||
if (!proxy) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const proxies = String(proxy).trim().split(/\s*;\s*/g).filter(Boolean)
|
||||
const first = proxies[0]
|
||||
const parts = first.split(/\s+/)
|
||||
const type = parts[0]
|
||||
|
||||
let proxyUrl = ''
|
||||
if (type === 'SOCKS' || type === 'SOCKS5') {
|
||||
// use a SOCKS proxy
|
||||
proxyUrl = 'socks://' + parts[1]
|
||||
} else if (type === 'PROXY' || type === 'HTTPS') {
|
||||
proxyUrl = (type === 'HTTPS' ? 'https' : 'http') + '://' + parts[1]
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return newProxyDispatcher(proxyUrl)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import Koa from 'koa'
|
||||
import bodyParser from 'koa-body'
|
||||
import * as mime from 'mime'
|
||||
import request from 'request'
|
||||
import * as undici from 'undici'
|
||||
import { promisify } from 'util'
|
||||
import { STATIC_DIR, HOME_DIR, HELP_DIR, USER_PLUGIN_DIR, FLAG_DISABLE_SERVER, APP_NAME, USER_THEME_DIR, RESOURCES_DIR, BUILD_IN_STYLES, USER_EXTENSION_DIR, USER_DATA } from '../constant'
|
||||
import * as file from './file'
|
||||
@@ -37,6 +38,10 @@ const noCache = (ctx: any) => {
|
||||
}
|
||||
|
||||
const checkPermission = (ctx: any, next: any) => {
|
||||
if (ctx.path.startsWith('/api/proxy-fetch')) {
|
||||
return next()
|
||||
}
|
||||
|
||||
const token = ctx.query._token || (ctx.headers.authorization || '').replace('Bearer ', '')
|
||||
|
||||
if (ctx.req._protocol || (!token && isLocalhost(ctx.request.ip))) {
|
||||
@@ -319,7 +324,45 @@ const userFile = async (ctx: any, next: any) => {
|
||||
}
|
||||
|
||||
const proxy = async (ctx: any, next: any) => {
|
||||
if (ctx.path.startsWith('/api/proxy')) {
|
||||
if (ctx.path.startsWith('/api/proxy-fetch/')) {
|
||||
const url = ctx.originalUrl.replace(/^.*\/api\/proxy-fetch\//, '')
|
||||
|
||||
// TODO ssrf
|
||||
// const _url = new URL(url)
|
||||
// if (
|
||||
// _url.hostname === 'localhost' ||
|
||||
// (
|
||||
// (ip.isV4Format(_url.hostname) || ip.isV6Format(_url.hostname)) &&
|
||||
// ip.isPrivate(_url.hostname)
|
||||
// )
|
||||
// ) {
|
||||
// throw new Error('Invalid URL')
|
||||
// }
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { 'x-proxy-url': proxyUrl, host, ...headers } = ctx.headers
|
||||
|
||||
const dispatcher = proxyUrl
|
||||
? getAction('new-proxy-dispatcher')(proxyUrl)
|
||||
: await getAction('get-proxy-dispatcher')(url)
|
||||
|
||||
const response = await undici.request(url, {
|
||||
dispatcher,
|
||||
method: ctx.method,
|
||||
headers,
|
||||
body: ctx.req,
|
||||
})
|
||||
|
||||
// Set the response status, headers, and body
|
||||
ctx.status = response.statusCode
|
||||
ctx.set(response.headers)
|
||||
ctx.body = response.body
|
||||
} catch (error: any) {
|
||||
ctx.status = 500
|
||||
throw error
|
||||
}
|
||||
} else if (ctx.path.startsWith('/api/proxy')) {
|
||||
const data = ctx.method === 'POST' ? ctx.request.body : ctx.query
|
||||
const url = data.url
|
||||
const options = typeof data.options === 'string' ? JSON.parse(ctx.query.options) : data.options
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
// https://github.com/Kaciras/fetch-socks/blob/master/index.ts
|
||||
|
||||
import { SocksClient, SocksProxy } from 'socks'
|
||||
import { Agent, buildConnector } from 'undici'
|
||||
import Connector = buildConnector.connector;
|
||||
import TLSOptions = buildConnector.BuildOptions;
|
||||
|
||||
export type SocksProxies = SocksProxy | SocksProxy[];
|
||||
|
||||
/**
|
||||
* Since socks does not guess HTTP ports, we need to do that.
|
||||
*
|
||||
* @param protocol Upper layer protocol, "http:" or "https:"
|
||||
* @param port A string containing the port number of the URL, maybe empty.
|
||||
*/
|
||||
function resolvePort (protocol: string, port: string) {
|
||||
return port ? Number.parseInt(port) : protocol === 'http:' ? 80 : 443
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Undici connector which establish the connection through socks proxies.
|
||||
*
|
||||
* If the proxies is an empty array, it will connect directly.
|
||||
*
|
||||
* @param proxies The proxy server to use or the list of proxy servers to chain.
|
||||
* @param tlsOpts TLS upgrade options.
|
||||
*/
|
||||
export function socksConnector (proxies: SocksProxies, tlsOpts: TLSOptions = {}): Connector {
|
||||
const chain = Array.isArray(proxies) ? proxies : [proxies]
|
||||
const { timeout = 1e4 } = tlsOpts
|
||||
const undiciConnect = buildConnector(tlsOpts)
|
||||
|
||||
return async (options, callback) => {
|
||||
let { protocol, hostname, port, httpSocket } = options
|
||||
|
||||
for (let i = 0; i < chain.length; i++) {
|
||||
const next = chain[i + 1]
|
||||
|
||||
const destination = i === chain.length - 1 ? {
|
||||
host: hostname,
|
||||
port: resolvePort(protocol, port),
|
||||
} : {
|
||||
port: next.port,
|
||||
host: next.host ?? next.ipaddress!,
|
||||
}
|
||||
|
||||
const socksOpts = {
|
||||
command: 'connect' as const,
|
||||
proxy: chain[i],
|
||||
timeout,
|
||||
destination,
|
||||
existing_socket: httpSocket,
|
||||
}
|
||||
|
||||
try {
|
||||
const r = await SocksClient.createConnection(socksOpts)
|
||||
httpSocket = r.socket
|
||||
} catch (error: any) {
|
||||
return callback(error, null)
|
||||
}
|
||||
}
|
||||
|
||||
// httpSocket may not exist when the chain is empty.
|
||||
if (httpSocket && protocol !== 'https:') {
|
||||
return callback(null, httpSocket.setNoDelay())
|
||||
}
|
||||
|
||||
/*
|
||||
* There are 2 cases here:
|
||||
* If httpSocket doesn't exist, let Undici make a connection.
|
||||
* If httpSocket exists & protocol is HTTPS, do TLS upgrade.
|
||||
*/
|
||||
return undiciConnect({ ...options, httpSocket }, callback)
|
||||
}
|
||||
}
|
||||
|
||||
export interface SocksDispatcherOptions extends Agent.Options {
|
||||
|
||||
/**
|
||||
* TLS upgrade options, see:
|
||||
* https://undici.nodejs.org/#/docs/api/Client?id=parameter-connectoptions
|
||||
*
|
||||
* The connect function is not supported.
|
||||
* If you want to create a custom connector, you can use `socksConnector`.
|
||||
*/
|
||||
connect?: TLSOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Undici Agent with socks connector.
|
||||
*
|
||||
* If the proxies is an empty array, it will connect directly.
|
||||
*
|
||||
* @param proxies The proxy server to use or the list of proxy servers to chain.
|
||||
* @param options Additional options passed to the Agent constructor.
|
||||
*/
|
||||
export function socksDispatcher (proxies: SocksProxies, options: SocksDispatcherOptions = {}) {
|
||||
const { connect, ...rest } = options
|
||||
return new Agent({ ...rest, connect: socksConnector(proxies, connect) })
|
||||
}
|
||||
@@ -83,6 +83,23 @@ export async function proxyRequest (
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy fetch.
|
||||
* @param url string
|
||||
* @param init RequestInit
|
||||
* @returns
|
||||
*/
|
||||
export async function proxyFetch (url: string, init?: RequestInit) {
|
||||
const res: Response = await fetch(`/api/proxy-fetch/${url}`, init)
|
||||
|
||||
if (res.headers.get('x-yank-note-api-status') === 'error') {
|
||||
const msg = res.headers.get('x-yank-note-api-message') || 'error'
|
||||
throw new Error(decodeURIComponent(msg))
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch help file content.
|
||||
* @param docName
|
||||
|
||||
+6
-6
@@ -24,22 +24,22 @@ export default defineConfig({
|
||||
port: 8066,
|
||||
proxy: {
|
||||
'/static': {
|
||||
target: 'http://localhost:3044'
|
||||
target: 'http://127.0.0.1:3044'
|
||||
},
|
||||
'/custom-css': {
|
||||
target: 'http://localhost:3044'
|
||||
target: 'http://127.0.0.1:3044'
|
||||
},
|
||||
'/extension': {
|
||||
target: 'http://localhost:3044'
|
||||
target: 'http://127.0.0.1:3044'
|
||||
},
|
||||
'/github.css': {
|
||||
target: 'http://localhost:3044'
|
||||
target: 'http://127.0.0.1:3044'
|
||||
},
|
||||
'/api': {
|
||||
target: 'http://localhost:3044'
|
||||
target: 'http://127.0.0.1:3044'
|
||||
},
|
||||
'/ws': {
|
||||
target: 'http://localhost:3044',
|
||||
target: 'http://127.0.0.1:3044',
|
||||
ws: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5462,6 +5462,14 @@ internal-slot@^1.0.5:
|
||||
hasown "^2.0.0"
|
||||
side-channel "^1.0.4"
|
||||
|
||||
ip-address@^9.0.5:
|
||||
version "9.0.5"
|
||||
resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a"
|
||||
integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==
|
||||
dependencies:
|
||||
jsbn "1.1.0"
|
||||
sprintf-js "^1.1.3"
|
||||
|
||||
ip@^1.1.5, ip@^1.1.9:
|
||||
version "1.1.9"
|
||||
resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.9.tgz#8dfbcc99a754d07f425310b86a99546b1151e396"
|
||||
@@ -6259,6 +6267,11 @@ js-yaml@^4.1.0:
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
|
||||
jsbn@1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040"
|
||||
integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==
|
||||
|
||||
jsbn@~0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
|
||||
@@ -8246,6 +8259,14 @@ socks@^2.6.2:
|
||||
ip "^2.0.0"
|
||||
smart-buffer "^4.2.0"
|
||||
|
||||
socks@^2.8.3:
|
||||
version "2.8.3"
|
||||
resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.3.tgz#1ebd0f09c52ba95a09750afe3f3f9f724a800cb5"
|
||||
integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==
|
||||
dependencies:
|
||||
ip-address "^9.0.5"
|
||||
smart-buffer "^4.2.0"
|
||||
|
||||
sort-keys-length@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/sort-keys-length/-/sort-keys-length-1.0.1.tgz#9cb6f4f4e9e48155a6aa0671edd336ff1479a188"
|
||||
@@ -8339,6 +8360,11 @@ sprintf-js@^1.1.2:
|
||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673"
|
||||
integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==
|
||||
|
||||
sprintf-js@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a"
|
||||
integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==
|
||||
|
||||
sprintf-js@~1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||
@@ -8937,6 +8963,11 @@ undici-types@~5.26.4:
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
|
||||
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
||||
|
||||
undici@^6.16.1:
|
||||
version "6.16.1"
|
||||
resolved "https://registry.yarnpkg.com/undici/-/undici-6.16.1.tgz#ff8f49c56e5a8629f92ad2ce00d4841b9619b19e"
|
||||
integrity sha512-NeNiTT7ixpeiL1qOIU/xTVpHpVP0svmI6PwoCKaMGaI5AsHOaRdwqU/f7Fi9eyU4u03nd5U/BC8wmRMnS9nqoA==
|
||||
|
||||
unique-filename@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-3.0.0.tgz#48ba7a5a16849f5080d26c760c86cf5cf05770ea"
|
||||
|
||||
Reference in New Issue
Block a user