Skip to content

Commit

Permalink
feat(proxy-agent): decouple agent events
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Feb 14, 2024
1 parent 06ef47a commit d89a750
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 64 deletions.
2 changes: 0 additions & 2 deletions .npmignore

This file was deleted.

6 changes: 6 additions & 0 deletions .nycrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"exclude": [
".yarn/**",
"scripts"
]
}
4 changes: 2 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@
"plugin"
],
"devDependencies": {
"cordis": "^3.10.0",
"cordis": "^3.10.1",
"undici": "^6.6.2"
},
"peerDependencies": {
"cordis": "^3.10.0"
"cordis": "^3.10.1"
},
"dependencies": {
"cosmokit": "^1.5.2",
Expand Down
88 changes: 32 additions & 56 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { Context, FunctionalService } from 'cordis'
import { Context, Service } from 'cordis'
import { base64ToArrayBuffer, defineProperty, Dict, trimSlash } from 'cosmokit'
import { ClientOptions } from 'ws'
import { loadFile, lookup, WebSocket } from 'undios/adapter'
import { isLocalAddress } from './utils.ts'
import type * as undici from 'undici'
import type * as http from 'http'

declare module 'cordis' {
interface Context {
Expand All @@ -16,8 +14,8 @@ declare module 'cordis' {
}

interface Events {
'http/dispatcher'(url: URL): undici.Dispatcher | undefined
'http/legacy-agent'(url: URL): http.Agent | undefined
'http/fetch-init'(init: RequestInit, config: HTTP.Config): void
'http/websocket-init'(init: ClientOptions, config: HTTP.Config): void
}
}

Expand Down Expand Up @@ -68,7 +66,6 @@ export namespace HTTP {
export interface Config {
headers?: Dict
timeout?: number
proxyAgent?: string
}

export interface RequestConfig extends Config {
Expand Down Expand Up @@ -97,14 +94,13 @@ export namespace HTTP {
export interface FileResponse {
mime?: string
name?: string
data: ArrayBufferLike
data: ArrayBuffer
}

export type Error = HTTPError
}

export interface HTTP {
[Context.current]: Context
<T>(url: string | URL, config?: HTTP.RequestConfig): Promise<HTTP.Response<T>>
<T>(method: HTTP.Method, url: string | URL, config?: HTTP.RequestConfig): Promise<HTTP.Response<T>>
config: HTTP.Config
Expand All @@ -115,24 +111,22 @@ export interface HTTP {
put: HTTP.Request2
}

export class HTTP extends FunctionalService {
export class HTTP extends Service {
static Error = HTTPError
/** @deprecated use `HTTP.Error.is()` instead */
static isAxiosError = HTTPError.is

static {
for (const method of ['get', 'delete'] as const) {
defineProperty(HTTP.prototype, method, async function (this: HTTP, url: string, config?: HTTP.Config) {
const caller = this[Context.current]
const response = await this.call(caller, method, url, config)
const response = await this(method, url, config)
return response.data
})
}

for (const method of ['patch', 'post', 'put'] as const) {
defineProperty(HTTP.prototype, method, async function (this: HTTP, url: string, data?: any, config?: HTTP.Config) {
const caller = this[Context.current]
const response = await this.call(caller, method, url, { data, ...config })
const response = await this(method, url, { data, ...config })
return response.data
})
}
Expand All @@ -155,17 +149,9 @@ export class HTTP extends FunctionalService {
return new HTTP(this[Context.current], HTTP.mergeConfig(this.config, config), true)
}

resolveDispatcher(href?: string) {
if (!href) return
const url = new URL(href)
const agent = this[Context.current].bail('http/dispatcher', url)
if (agent) return agent
throw new Error(`Cannot resolve proxy agent ${url}`)
}

resolveConfig(ctx: Context, init?: HTTP.RequestConfig): HTTP.RequestConfig {
resolveConfig(init?: HTTP.RequestConfig): HTTP.RequestConfig {
let result = { headers: {}, ...this.config }
let intercept = ctx[Context.intercept]
let intercept = this[Context.current][Context.intercept]
while (intercept) {
result = HTTP.mergeConfig(result, intercept.http)
intercept = Object.getPrototypeOf(intercept)
Expand All @@ -174,9 +160,9 @@ export class HTTP extends FunctionalService {
return result
}

static resolveURL(caller: Context, url: string | URL, config: HTTP.RequestConfig) {
resolveURL(url: string | URL, config: HTTP.RequestConfig) {
if (config.endpoint) {
// caller.emit('internal/warning', 'endpoint is deprecated, please use baseURL instead')
// this[Context.current].emit('internal/warning', 'endpoint is deprecated, please use baseURL instead')
try {
new URL(url)
} catch {
Expand Down Expand Up @@ -206,13 +192,14 @@ export class HTTP extends FunctionalService {
}
}

async call(caller: Context, ...args: any[]) {
async [Context.invoke](...args: any[]) {
const caller = this[Context.current]
let method: HTTP.Method | undefined
if (typeof args[1] === 'string' || args[1] instanceof URL) {
method = args.shift()
}
const config = this.resolveConfig(caller, args[1])
const url = HTTP.resolveURL(caller, args[0], config)
const config = this.resolveConfig(args[1])
const url = this.resolveURL(args[0], config)

const controller = new AbortController()
let timer: NodeJS.Timeout | number | undefined
Expand All @@ -227,14 +214,9 @@ export class HTTP extends FunctionalService {
}

try {
const raw = await fetch(url, {
method,
body: config.data,
headers: config.headers,
keepalive: config.keepAlive,
signal: controller.signal,
['dispatcher' as never]: this.resolveDispatcher(config?.proxyAgent),
}).catch((cause) => {
const init: RequestInit = { method, headers: config.headers, signal: controller.signal }
caller.emit('http/fetch-init', init, config)
const raw = await fetch(url, init).catch((cause) => {
const error = new HTTP.Error(`fetch ${url} failed`)
error.cause = cause
throw error
Expand Down Expand Up @@ -271,35 +253,30 @@ export class HTTP extends FunctionalService {
}

async head(url: string, config?: HTTP.Config) {
const caller = this[Context.current]
const response = await this.call(caller, 'HEAD', url, config)
const response = await this('HEAD', url, config)
return response.headers
}

/** @deprecated use `ctx.http()` instead */
axios<T>(url: string, config?: HTTP.Config): Promise<HTTP.Response<T>> {
const caller = this[Context.current]
caller.emit('internal/warning', 'ctx.http.axios() is deprecated, use ctx.http() instead')
return this.call(caller, url, config)
}

resolveAgent(href?: string) {
if (!href) return
const url = new URL(href)
const agent = this[Context.current].bail('http/legacy-agent', url)
if (agent) return agent
throw new Error(`Cannot resolve proxy agent ${url}`)
return this(url, config)
}

async ws(this: HTTP, url: string | URL, init?: HTTP.Config) {
const caller = this[Context.current]
const config = this.resolveConfig(caller, init)
url = HTTP.resolveURL(caller, url, config)
const socket = new WebSocket(url, 'Server' in WebSocket ? {
agent: this.resolveAgent(config?.proxyAgent),
handshakeTimeout: config?.timeout,
headers: config?.headers,
} as ClientOptions as never : undefined)
const config = this.resolveConfig(init)
url = this.resolveURL(url, config)
let options: ClientOptions | undefined
if ('Server' in WebSocket) {
options = {
handshakeTimeout: config?.timeout,
headers: config?.headers,
}
caller.emit('http/websocket-init', options, config)
}
const socket = new WebSocket(url, options)
const dispose = caller.on('dispose', () => {
socket.close(1001, 'context disposed')
})
Expand All @@ -312,13 +289,12 @@ export class HTTP extends FunctionalService {
async file(url: string, options: HTTP.FileConfig = {}): Promise<HTTP.FileResponse> {
const result = await loadFile(url)
if (result) return result
const caller = this[Context.current]
const capture = /^data:([\w/-]+);base64,(.*)$/.exec(url)
if (capture) {
const [, mime, base64] = capture
return { mime, data: base64ToArrayBuffer(base64) }
}
const { headers, data, url: responseUrl } = await this.call(caller, url, {
const { headers, data, url: responseUrl } = await this<ArrayBuffer>(url, {
method: 'GET',
responseType: 'arraybuffer',
timeout: +options.timeout! || undefined,
Expand Down
6 changes: 3 additions & 3 deletions packages/proxy-agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@
"plugin"
],
"devDependencies": {
"cordis": "^3.10.0"
"cordis": "^3.10.1"
},
"peerDependencies": {
"undios": "^0.1.0",
"cordis": "^3.10.0"
"cordis": "^3.10.1",
"undios": "^0.1.0"
},
"dependencies": {
"http-proxy-agent": "^7.0.0",
Expand Down
34 changes: 33 additions & 1 deletion packages/proxy-agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,30 @@
// modified from https://github.com/TooTallNate/proxy-agents/blob/c881a1804197b89580320b87082971c3c6a61746/packages/socks-proxy-agent/src/index.ts

import {} from 'undios'
import * as http from 'node:http'
import { lookup } from 'node:dns/promises'
import { Context, z } from 'cordis'
import { SocksClient, SocksProxy } from 'socks'
import { Agent, buildConnector, ProxyAgent } from 'undici'
import { Agent, buildConnector, Dispatcher, ProxyAgent } from 'undici'
import { HttpProxyAgent } from 'http-proxy-agent'
import { HttpsProxyAgent } from 'https-proxy-agent'
import { SocksProxyAgent } from 'socks-proxy-agent'

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

declare module 'undios' {
namespace HTTP {
interface Config {
proxyAgent?: string
}
}
}

function resolvePort(protocol: string, port: string) {
return port ? Number.parseInt(port) : protocol === 'http:' ? 80 : 443
}
Expand Down Expand Up @@ -64,6 +80,14 @@ export interface Config {}
export const Config: z<Config> = z.object({})

export function apply(ctx: Context, config: Config) {
ctx.on('http/fetch-init', (init, config) => {
if (!config?.proxyAgent) return
const url = new URL(config.proxyAgent)
const agent = ctx.bail('http/dispatcher', url)
if (!agent) throw new Error(`Cannot resolve proxy agent ${url}`)
init['dispatcher'] = agent
})

ctx.on('http/dispatcher', (url) => {
if (['http:', 'https:'].includes(url.protocol)) {
return new ProxyAgent(url.href)
Expand All @@ -73,6 +97,14 @@ export function apply(ctx: Context, config: Config) {
return socksAgent(result)
})

ctx.on('http/websocket-init', (init, config) => {
if (!config?.proxyAgent) return
const url = new URL(config.proxyAgent)
const agent = ctx.bail('http/legacy-agent', url)
if (!agent) throw new Error(`Cannot resolve proxy agent ${url}`)
init.agent = agent
})

ctx.on('http/legacy-agent', (url) => {
if (url.protocol === 'http:') return new HttpProxyAgent(url)
if (url.protocol === 'https:') return new HttpsProxyAgent(url)
Expand Down

0 comments on commit d89a750

Please sign in to comment.