Skip to content

Commit

Permalink
feat(core): implement SOCKS support for HTTP requests
Browse files Browse the repository at this point in the history
- Add socksConnector and socksDispatcher functions for SOCKS proxy support
- Integrate with Undici for efficient connection management
- Support both single and chained SOCKS proxies
- Handle HTTPS connections with TLS upgrade
- Provide fallback to direct connection for empty proxy list
  • Loading branch information
dingyi222666 committed Nov 29, 2024
1 parent bf32c01 commit c18ca9d
Showing 1 changed file with 108 additions and 2 deletions.
110 changes: 108 additions & 2 deletions packages/core/src/utils/request.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { socksDispatcher } from 'fetch-socks'
import { HttpsProxyAgent } from 'https-proxy-agent'
import { logger } from 'koishi-plugin-chatluna'
import {
ChatLunaError,
ChatLunaErrorCode
} from 'koishi-plugin-chatluna/utils/error'
import { SocksProxyAgent } from 'socks-proxy-agent'
import unidci, { FormData, ProxyAgent } from 'undici'
import unidci, { Agent, buildConnector, FormData, ProxyAgent } from 'undici'
import * as fetchType from 'undici/types/fetch'
import { ClientRequestArgs } from 'http'
import { ClientOptions, WebSocket } from 'ws'
import { SocksClient, SocksProxy } from 'socks'
import Connector = buildConnector.connector
import TLSOptions = buildConnector.BuildOptions

export { FormData }

Expand Down Expand Up @@ -174,3 +176,107 @@ export function randomUA() {

return `Mozilla/5.0 (Windows NT ${osVersion}; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) ${browser}/${version}.0.0.0 Safari/537.36`
}

// see https://github.com/Kaciras/fetch-socks/blob/master/index.ts

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) {
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) })
}

0 comments on commit c18ca9d

Please sign in to comment.