Skip to content

Commit

Permalink
feat(socks): add socks proxy agent
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Feb 11, 2024
1 parent fea2f99 commit 45a1438
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 0 deletions.
54 changes: 54 additions & 0 deletions packages/socks/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "@cordisjs/plugin-http-socks",
"description": "Socks proxy agent support for @cordisjs/plugin-http",
"version": "0.1.0",
"type": "module",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"exports": {
".": {
"require": "./lib/index.cjs",
"import": "./lib/index.js",
"types": "./lib/index.d.ts"
},
"./package.json": "./package.json"
},
"files": [
"lib",
"src"
],
"author": "Shigma <[email protected]>",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/cordiverse/http.git",
"directory": "packages/socks"
},
"bugs": {
"url": "https://github.com/cordiverse/http/issues"
},
"homepage": "https://github.com/cordiverse/http",
"keywords": [
"cordis",
"http",
"fetch",
"socks",
"proxy",
"agent",
"request",
"service",
"plugin"
],
"devDependencies": {
"cordis": "^3.6.1",
"undici": "^6.6.2"
},
"peerDependencies": {
"@cordisjs/plugin-http": "^0.1.0",
"cordis": "^3.6.1"
},
"dependencies": {
"socks": "^2.7.1",
"socks-proxy-agent": "^8.0.2"
}
}
1 change: 1 addition & 0 deletions packages/socks/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @cordisjs/plugin-http-socks
156 changes: 156 additions & 0 deletions packages/socks/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// modified from https://github.com/Kaciras/fetch-socks/blob/41cec5a02c36687279ad2628f7c46327f7ff3e2d/index.ts
// modified from https://github.com/TooTallNate/proxy-agents/blob/c881a1804197b89580320b87082971c3c6a61746/packages/socks-proxy-agent/src/index.ts

import {} from '@cordisjs/plugin-http'
import { Context, z } from 'cordis'
import { SocksClient, SocksProxy } from 'socks'
import type { Agent, buildConnector, Client } from 'undici'
import { SocksProxyAgent } from 'socks-proxy-agent'

function getUniqueSymbol(object: object, name: string) {
const symbol = Object.getOwnPropertySymbols(object).find(s => s.toString() === `Symbol(${name})`)
return object[symbol!]
}

const kGlobalDispatcher = Symbol.for('undici.globalDispatcher.1')
const globalAgent = globalThis[kGlobalDispatcher] as Agent
const AgentConstructor = globalAgent.constructor as typeof Agent
const factory = getUniqueSymbol(globalAgent, 'factory') as NonNullable<Agent.Options['factory']>

function build(options: buildConnector.BuildOptions) {
const client = factory('http://0.0.0.0', { connections: 1, connect: options }) as Client
return getUniqueSymbol(client, 'connector') as buildConnector.connector
}

function resolvePort(protocol: string, port: string) {
return port ? Number.parseInt(port) : protocol === 'http:' ? 80 : 443
}

function socksConnector(proxy: SocksProxy, tlsOpts: buildConnector.BuildOptions = {}): buildConnector.connector {
const { timeout = 10e3 } = tlsOpts
const connect = build(tlsOpts)

return async (options, callback) => {
let { protocol, hostname, port, httpSocket } = options

const destination = {
host: hostname,
port: resolvePort(protocol, port),
}

const socksOpts = {
command: 'connect' as const,
proxy,
timeout,
destination,
existing_socket: httpSocket,
}

try {
const r = await SocksClient.createConnection(socksOpts)
httpSocket = r.socket
} catch (error) {
// @ts-ignore
return callback(error, null)
}

if (httpSocket && protocol !== 'https:') {
return callback(null, httpSocket.setNoDelay())
}

return connect({ ...options, httpSocket }, callback)
}
}

interface SocksDispatcherOptions extends Agent.Options {
connect?: buildConnector.BuildOptions
}

function socksDispatcher(proxies: SocksProxy, options: SocksDispatcherOptions = {}) {
const { connect, ...rest } = options
return new AgentConstructor({ ...rest, connect: socksConnector(proxies, connect) })
}

export const name = 'http-socks'

export interface Config {}

export const Config: z<Config> = z.object({})

export function apply(ctx: Context, config: Config) {
ctx.on('http/dispatcher', (href) => {
const url = new URL(href)
try {
const { proxy } = parseSocksURL(url)
return socksDispatcher(proxy)
} catch {}
})

ctx.on('http/http-agent', (href) => {
try {
return new SocksProxyAgent(href)
} catch {}
})
}

function parseSocksURL(url: URL): { lookup: boolean; proxy: SocksProxy } {
let lookup = false
let type: SocksProxy['type'] = 5
const host = url.hostname

// From RFC 1928, Section 3: https://tools.ietf.org/html/rfc1928#section-3
// "The SOCKS service is conventionally located on TCP port 1080"
const port = parseInt(url.port, 10) || 1080

// figure out if we want socks v4 or v5, based on the "protocol" used.
// Defaults to 5.
switch (url.protocol.replace(':', '')) {
case 'socks4':
lookup = true
type = 4
break
// pass through
case 'socks4a':
type = 4
break
case 'socks5':
lookup = true
type = 5
break
// pass through
case 'socks': // no version specified, default to 5h
type = 5
break
case 'socks5h':
type = 5
break
default:
throw new TypeError(
`A "socks" protocol must be specified! Got: ${String(
url.protocol,
)}`,
)
}

const proxy: SocksProxy = {
host,
port,
type,
}

if (url.username) {
Object.defineProperty(proxy, 'userId', {
value: decodeURIComponent(url.username),
enumerable: false,
})
}

if (url.password != null) {
Object.defineProperty(proxy, 'password', {
value: decodeURIComponent(url.password),
enumerable: false,
})
}

return { lookup, proxy }
}
10 changes: 10 additions & 0 deletions packages/socks/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib",
},
"include": [
"src",
],
}

0 comments on commit 45a1438

Please sign in to comment.