Skip to content

Commit

Permalink
feat(proxy-agent): support lookup for socks
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Feb 12, 2024
1 parent e918da6 commit 94a4248
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 83 deletions.
2 changes: 1 addition & 1 deletion packages/http/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@cordisjs/plugin-http",
"name": "undios",
"description": "Axios-style HTTP client service for Cordis",
"version": "0.1.0",
"type": "module",
Expand Down
11 changes: 7 additions & 4 deletions packages/http/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Context, FunctionalService } from 'cordis'
import { base64ToArrayBuffer, defineProperty, Dict, trimSlash } from 'cosmokit'
import { ClientOptions } from 'ws'
import { loadFile, lookup, WebSocket } from '@cordisjs/plugin-http/adapter'
import { loadFile, lookup, WebSocket } from 'undios/adapter'
import { isLocalAddress } from './utils.ts'
import type * as undici from 'undici'
import type * as http from 'http'
Expand All @@ -17,7 +17,7 @@ declare module 'cordis' {

interface Events {
'http/dispatcher'(url: URL): undici.Dispatcher | undefined
'http/http-agent'(url: URL): http.Agent | undefined
'http/legacy-agent'(url: URL): http.Agent | undefined
}
}

Expand Down Expand Up @@ -286,7 +286,7 @@ export class HTTP extends FunctionalService {
resolveAgent(href?: string) {
if (!href) return
const url = new URL(href)
const agent = this[Context.current].bail('http/http-agent', url)
const agent = this[Context.current].bail('http/legacy-agent', url)
if (agent) return agent
throw new Error(`Cannot resolve proxy agent ${url}`)
}
Expand All @@ -300,9 +300,12 @@ export class HTTP extends FunctionalService {
handshakeTimeout: config?.timeout,
headers: config?.headers,
} as ClientOptions as never : undefined)
caller.on('dispose', () => {
const dispose = caller.on('dispose', () => {
socket.close(1001, 'context disposed')
})
socket.addEventListener('close', () => {
dispose()
})
return socket
}

Expand Down
13 changes: 7 additions & 6 deletions packages/socks/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cordisjs/plugin-http-socks",
"description": "Socks proxy agent support for @cordisjs/plugin-http",
"name": "undios-proxy-agent",
"description": "Proxy agent support for undios",
"version": "0.1.0",
"type": "module",
"main": "lib/index.js",
Expand Down Expand Up @@ -37,18 +37,19 @@
"agent",
"request",
"service",
"plugin"
"plugin",
"undici"
],
"devDependencies": {
"cordis": "^3.10.0",
"undici": "^6.6.2"
"cordis": "^3.10.0"
},
"peerDependencies": {
"@cordisjs/plugin-http": "^0.1.0",
"cordis": "^3.10.0"
},
"dependencies": {
"socks": "^2.7.1",
"socks-proxy-agent": "^8.0.2"
"socks-proxy-agent": "^8.0.2",
"undici": "^6.6.2"
}
}
120 changes: 48 additions & 72 deletions packages/socks/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// 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 {} from 'undios'
import { lookup } from 'node:dns/promises'
import { Context, z } from 'cordis'
import { SocksClient, SocksProxy } from 'socks'
import type { Agent, buildConnector, Client } from 'undici'
import { SocksProxyAgent } from 'socks-proxy-agent'
import { defineProperty } from 'cosmokit'

// @ts-ignore
// ensure the global dispatcher is initialized
Expand All @@ -30,31 +32,29 @@ function resolvePort(protocol: string, port: string) {
return port ? Number.parseInt(port) : protocol === 'http:' ? 80 : 443
}

function socksConnector(proxy: SocksProxy, tlsOpts: buildConnector.BuildOptions = {}): buildConnector.connector {
function createConnect({ proxy, shouldLookup }: ParseResult, 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
if (shouldLookup) {
hostname = (await lookup(hostname)).address
}
const event = await SocksClient.createConnection({
command: 'connect',
proxy,
timeout,
destination: {
host: hostname,
port: resolvePort(protocol, port),
},
existing_socket: httpSocket,
})
httpSocket = event.socket
} catch (error: any) {
return callback(error, null)
}

Expand All @@ -70,9 +70,9 @@ interface SocksDispatcherOptions extends Agent.Options {
connect?: buildConnector.BuildOptions
}

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

export const name = 'http-socks'
Expand All @@ -82,79 +82,55 @@ 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/dispatcher', (url) => {
const result = parseSocksURL(url)
if (!result) return
return socksAgent(result)
})

ctx.on('http/http-agent', (href) => {
try {
return new SocksProxyAgent(href)
} catch {}
ctx.on('http/legacy-agent', (url) => {
const result = parseSocksURL(url)
if (!result) return
return new SocksProxyAgent(url)
})
}

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

function parseSocksURL(url: URL): ParseResult | undefined {
let shouldLookup = false
let type: SocksProxy['type']

// 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
const host = url.hostname

// 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
shouldLookup = true
// eslint-disable-next-line no-fallthrough
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
shouldLookup = true
// eslint-disable-next-line no-fallthrough
case 'socks':
case 'socks5h':
type = 5
break
default:
throw new TypeError(
`A "socks" protocol must be specified! Got: ${String(
url.protocol,
)}`,
)
default: return
}

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,
})
}
const proxy: SocksProxy = { host, port, type }
if (url.username) defineProperty(proxy, 'userId', decodeURIComponent(url.username))
if (url.password) defineProperty(proxy, 'password', decodeURIComponent(url.password))

return { lookup, proxy }
return { shouldLookup, proxy }
}

0 comments on commit 94a4248

Please sign in to comment.