From c83f5dd4a1ae43dba1abc69c19fab8629c3e38f8 Mon Sep 17 00:00:00 2001 From: suhaotian Date: Fri, 8 Mar 2024 21:07:04 +1100 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20add=20proxy=20plugin=20=F0=9F=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bun-example/index.ts | 2 + cloudflare-example/src/xior.ts | 2 + next-example/app/http.ts | 2 + package.json | 9 +++- pnpm-lock.yaml | 16 ++++++ src/plugins/proxy.ts | 76 +++++++++++++++++++++++++++ src/tests/axios-compatible.test.ts | 5 ++ src/tests/plugins/error-retry.test.ts | 2 - src/tests/plugins/proxy.test.ts | 68 ++++++++++++++++++++++++ src/tests/server.ts | 16 ++++++ src/xior.ts | 2 +- vite-example/src/main.ts | 2 + 12 files changed, 198 insertions(+), 4 deletions(-) create mode 100644 src/plugins/proxy.ts create mode 100644 src/tests/plugins/proxy.test.ts diff --git a/bun-example/index.ts b/bun-example/index.ts index a8cb409..ad326f2 100644 --- a/bun-example/index.ts +++ b/bun-example/index.ts @@ -5,6 +5,7 @@ import xior, { merge, delay, buildSortedURL, encodeParams } from 'xior'; import cachePlugin from 'xior/plugins/cache'; import errorRetryPlugin from 'xior/plugins/error-retry'; import uploadDownloadProgressPlugin from 'xior/plugins/progress'; +import proxyPlugin from 'xior/plugins/proxy'; import throttlePlugin from 'xior/plugins/throttle'; // console.log(merge, delay, buildSortedURL); @@ -16,6 +17,7 @@ instance.plugins.use(errorRetryPlugin({})); instance.plugins.use(cachePlugin({})); instance.plugins.use(throttlePlugin({})); instance.plugins.use(uploadDownloadProgressPlugin({})); +instance.plugins.use(proxyPlugin({})); instance.plugins.use((adapter) => { return async (config) => { diff --git a/cloudflare-example/src/xior.ts b/cloudflare-example/src/xior.ts index 57222a4..4dd9879 100644 --- a/cloudflare-example/src/xior.ts +++ b/cloudflare-example/src/xior.ts @@ -2,6 +2,7 @@ import xior from 'xior'; import cachePlugin from 'xior/plugins/cache'; import errorRetryPlugin from 'xior/plugins/error-retry'; import uploadDownloadProgressPlugin from 'xior/plugins/progress'; +import proxyPlugin from 'xior/plugins/proxy'; import throttlePlugin from 'xior/plugins/throttle'; export const http = xior.create(); @@ -10,3 +11,4 @@ http.plugins.use(errorRetryPlugin()); http.plugins.use(throttlePlugin()); http.plugins.use(cachePlugin()); http.plugins.use(uploadDownloadProgressPlugin()); +http.plugins.use(proxyPlugin()); diff --git a/next-example/app/http.ts b/next-example/app/http.ts index 57222a4..4dd9879 100644 --- a/next-example/app/http.ts +++ b/next-example/app/http.ts @@ -2,6 +2,7 @@ import xior from 'xior'; import cachePlugin from 'xior/plugins/cache'; import errorRetryPlugin from 'xior/plugins/error-retry'; import uploadDownloadProgressPlugin from 'xior/plugins/progress'; +import proxyPlugin from 'xior/plugins/proxy'; import throttlePlugin from 'xior/plugins/throttle'; export const http = xior.create(); @@ -10,3 +11,4 @@ http.plugins.use(errorRetryPlugin()); http.plugins.use(throttlePlugin()); http.plugins.use(cachePlugin()); http.plugins.use(uploadDownloadProgressPlugin()); +http.plugins.use(proxyPlugin()); diff --git a/package.json b/package.json index 539c812..968f604 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,12 @@ "import": "./plugins/progress/index.mjs", "module": "./plugins/progress/index.esm.js" }, + "./plugins/proxy": { + "types": "./plugins/proxy/index.d.ts", + "require": "./plugins/proxy/index.cjs", + "import": "./plugins/proxy/index.mjs", + "module": "./plugins/proxy/index.esm.js" + }, ".": { "types": "./dist/types/index.d.ts", "require": "./dist/index.cjs", @@ -74,7 +80,8 @@ "bunchee": "^4.4.8", "lfs-auto-track": "^1.1.0", "isomorphic-unfetch": "^4.0.2", - "promise-polyfill": "^8.3.0" + "promise-polyfill": "^8.3.0", + "express-basic-auth": "^1.2.1" }, "prettier": { "printWidth": 100, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4b7824d..7b41944 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,6 +45,9 @@ importers: express: specifier: ^4.18.2 version: 4.18.2 + express-basic-auth: + specifier: ^1.2.1 + version: 1.2.1 husky: specifier: ^9.0.7 version: 9.0.7 @@ -1771,6 +1774,13 @@ packages: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true + /basic-auth@2.0.1: + resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} + engines: {node: '>= 0.8'} + dependencies: + safe-buffer: 5.1.2 + dev: true + /binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} @@ -2829,6 +2839,12 @@ packages: engines: {node: '>=6'} dev: true + /express-basic-auth@1.2.1: + resolution: {integrity: sha512-L6YQ1wQ/mNjVLAmK3AG1RK6VkokA1BIY6wmiH304Xtt/cLTps40EusZsU1Uop+v9lTDPxdtzbFmdXfFO3KEnwA==} + dependencies: + basic-auth: 2.0.1 + dev: true + /express@4.18.2: resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} engines: {node: '>= 0.10.0'} diff --git a/src/plugins/proxy.ts b/src/plugins/proxy.ts new file mode 100644 index 0000000..f175eaf --- /dev/null +++ b/src/plugins/proxy.ts @@ -0,0 +1,76 @@ +import { XiorInterceptorRequestConfig, XiorPlugin, XiorRequestConfig } from '../types'; +import { isAbsoluteURL } from '../utils'; +import { xior } from '../xior'; + +/** @ts-ignore */ +declare module 'xior' { + interface XiorRequestConfig { + proxy?: XiorProxyConfig; + } +} + +export interface XiorBasicCredentials { + username: string; + password: string; +} + +export interface XiorProxyConfig { + host: string; + protocol?: string; + port?: number; + auth?: XiorBasicCredentials; +} + +export default function xiorProxyPlugin(): XiorPlugin { + const proxyInstance = xior.create(); + return function proxyPlugin(adapter) { + return async (config) => { + const proxyConfig = (config as { proxy?: XiorProxyConfig }).proxy; + + if (proxyConfig && proxyConfig.host) { + // url is not absolute url + // url match with baseURL + let isValidUrlToProxy = true; + let isAbsolute = false; + if (config.url && isAbsoluteURL(config.url)) { + isValidUrlToProxy = !!(config.baseURL && config.url.startsWith(config.baseURL)); + isAbsolute = true; + } + if (isValidUrlToProxy) { + const { protocol, host, port, auth } = proxyConfig; + let proxyUrl = ''; + if (protocol) { + proxyUrl += `${protocol}://`; + } + proxyUrl += host; + if (port) { + proxyUrl += `:${port}`; + } + const url = isAbsolute + ? config.url?.replace(config.baseURL as string, proxyUrl) + : proxyUrl + (config.url || ''); + + const proxyHeaders: Record = {}; + + if (auth) { + proxyHeaders['Authorization'] = `Basic ${btoa(auth.username + ':' + auth.password)}`; + } + + const proxyRequestConfig = { + ...config, + _url: '', + url, + proxy: undefined, + baseURL: proxyUrl, + headers: { + ...config.headers, + ...proxyHeaders, + }, + } as XiorInterceptorRequestConfig; + return proxyInstance.request(proxyRequestConfig); + } + } + return adapter(config); + }; + }; +} diff --git a/src/tests/axios-compatible.test.ts b/src/tests/axios-compatible.test.ts index 1e98ec5..faed8e9 100644 --- a/src/tests/axios-compatible.test.ts +++ b/src/tests/axios-compatible.test.ts @@ -24,6 +24,11 @@ describe('axios compatible tests', () => { paramsSerializer(data) { return JSON.stringify(data); }, + // proxy: { + // protocol: '', + // host: '', + // port: 1, + // }, }); const xiorInstance = xior.create({ withCredentials: true, diff --git a/src/tests/plugins/error-retry.test.ts b/src/tests/plugins/error-retry.test.ts index 9ef604e..872c535 100644 --- a/src/tests/plugins/error-retry.test.ts +++ b/src/tests/plugins/error-retry.test.ts @@ -1,7 +1,5 @@ import assert from 'node:assert'; import { after, before, describe, it } from 'node:test'; -// @ts-ignore -import stringify from 'qs/lib/stringify'; import xiorErrorRetryPlugin from '../../plugins/error-retry'; import { XiorError } from '../../utils'; diff --git a/src/tests/plugins/proxy.test.ts b/src/tests/plugins/proxy.test.ts new file mode 100644 index 0000000..4828ee5 --- /dev/null +++ b/src/tests/plugins/proxy.test.ts @@ -0,0 +1,68 @@ +import assert from 'node:assert'; +import { after, before, describe, it } from 'node:test'; + +import xiorProxyPlugin from '../../plugins/proxy'; +import { XiorRequestConfig } from '../../types'; +import { xior } from '../../xior'; +import { startServer } from '../server'; + +let close: Function; +const port = 7980; +const baseURL = `http://localhost:${port}`; +before(async () => { + close = await startServer(port); +}); + +after(async () => { + return close(1); +}); + +describe('xior proxy plguins tests', () => { + const instance = xior.create({ + baseURL, + proxy: { host: 'https://hacker-news.firebaseio.com' }, + } as XiorRequestConfig); + instance.plugins.use(xiorProxyPlugin()); + + it('should work with proxy plugin', async () => { + const { data } = await instance.get('/v0/item/121003.json'); + assert.strictEqual(data.id, 121003); + assert.strictEqual(data.time, 1203647620); + assert.strictEqual(data.title, 'Ask HN: The Arc Effect'); + }); + + it('should work with `proxy: undefined`', async () => { + const { data } = await instance.get('/get', { baseURL, proxy: undefined } as XiorRequestConfig); + assert.strictEqual(data.method, 'get'); + }); + + it('should work with basic auth', async () => { + const { data } = await instance.get('/basic-auth', { + baseURL: 'https://hacker-news.firebaseio.com', + proxy: { + host: baseURL, + auth: { + username: 'test', + password: '123456', + }, + }, + } as XiorRequestConfig); + assert.strictEqual(data.method, 'get'); + assert.strictEqual(data.msg, 'ok'); + }); + + it('should work with basic auth: complex characters username/password', async () => { + const { data } = await instance.get('/basic-auth', { + baseURL: 'https://hacker-news.firebaseio.com', + proxy: { + host: baseURL, + auth: { + username: '.(*-?', + password: '.(*-?/', + }, + }, + } as XiorRequestConfig); + assert.strictEqual(data.method, 'get'); + assert.strictEqual(data.msg, 'ok'); + }); +}); diff --git a/src/tests/server.ts b/src/tests/server.ts index ec8333f..53b0d52 100644 --- a/src/tests/server.ts +++ b/src/tests/server.ts @@ -1,4 +1,5 @@ import express from 'express'; +import basicAuth from 'express-basic-auth'; import multer from 'multer'; export async function startServer(port: number) { @@ -6,6 +7,21 @@ export async function startServer(port: number) { app.use(express.urlencoded({ extended: true })); app.use(express.json()); + app.get( + '/basic-auth', + basicAuth({ + users: { test: '123456', '.(*-?': '.(*-?/' }, + challenge: true, + realm: 'Imb4T3st4pp', + }), + (req, res) => { + res.send({ + method: 'get', + msg: 'ok', + }); + } + ); + (['get', 'post', 'patch', 'put', 'delete', 'head', 'options'] as const).forEach((method) => { app[method](`/${method}`, (req, res) => { if (req.headers['x-custom-value'] === 'error') { diff --git a/src/xior.ts b/src/xior.ts index e7a1179..294d726 100644 --- a/src/xior.ts +++ b/src/xior.ts @@ -144,7 +144,7 @@ export class xior { return finalPlugin(requestConfig); } - private async handlerFetch(requestConfig: XiorRequestConfig): Promise> { + async handlerFetch(requestConfig: XiorRequestConfig): Promise> { const { url, method, headers, timeout, signal: reqSignal, data, ...rest } = requestConfig; /** timeout */ diff --git a/vite-example/src/main.ts b/vite-example/src/main.ts index 74f469c..ddb74d2 100644 --- a/vite-example/src/main.ts +++ b/vite-example/src/main.ts @@ -3,6 +3,7 @@ import xior, { merge, delay, buildSortedURL } from 'xior'; import cachePlugin from 'xior/plugins/cache'; import errorRetryPlugin from 'xior/plugins/error-retry'; import uploadDownloadProgressPlugin from 'xior/plugins/progress'; +import proxyPlugin from 'xior/plugins/proxy'; import throttlePlugin from 'xior/plugins/throttle'; console.log(merge, delay, buildSortedURL); @@ -12,6 +13,7 @@ instance.plugins.use(errorRetryPlugin({})); instance.plugins.use(cachePlugin({})); instance.plugins.use(throttlePlugin({})); instance.plugins.use(uploadDownloadProgressPlugin({})); +instance.plugins.use(proxyPlugin()); instance.plugins.use((adapter) => { return async (config) => { From 2ea7b31244b07f6a674097cde49bcf0ce673f3b5 Mon Sep 17 00:00:00 2001 From: suhaotian Date: Fri, 8 Mar 2024 21:17:21 +1100 Subject: [PATCH 2/2] fix(tests): fix tests 403 --- src/tests/axios-compatible.test.ts | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/tests/axios-compatible.test.ts b/src/tests/axios-compatible.test.ts index faed8e9..32bd663 100644 --- a/src/tests/axios-compatible.test.ts +++ b/src/tests/axios-compatible.test.ts @@ -58,30 +58,26 @@ describe('axios compatible tests', () => { }); it('should work with baseURL', async () => { - const axiosInstance = axios.create({ baseURL: 'https://github.com' }); - const xiorInstance = xior.create({ baseURL: 'https://github.com/' }); - const { data } = await axiosInstance.get('/'); - const { data: axiorData } = await xiorInstance.get('/'); - assert.strictEqual(data.slice(0, 10), axiorData?.slice(0, 10)); - assert.strictEqual(data.slice(-10), axiorData?.slice(-10)); + const axiosInstance = axios.create({ baseURL }); + const xiorInstance = xior.create({ baseURL }); + const { data } = await axiosInstance.get('/get'); + const { data: axiorData } = await xiorInstance.get('/get'); + assert.strictEqual(data.method, axiorData.method); }); it('should work with params', async () => { - const axiosInstance = axios.create({ baseURL: 'https://api.github.com' }); - const xiorInstance = xior.create({ baseURL: 'https://api.github.com/' }); - const { data } = await axiosInstance.get('/orgs/tsdk-monorepo/repos', { + const axiosInstance = axios.create({ baseURL }); + const xiorInstance = xior.create({ baseURL }); + const { data } = await axiosInstance.get('/get', { data: { page: 1 }, - params: { page: 100 }, + params: { page: 100, size: 100 }, }); - const { data: axiorData } = await xiorInstance.get('/orgs/tsdk-monorepo/repos', { + const { data: axiorData } = await xiorInstance.get('/get', { data: { page: 1 }, - params: { page: 100 }, + params: { page: 100, size: 100 }, }); - assert.strictEqual(data.length, 0); - assert.strictEqual(axiorData.length, 0); - }); - it('should work with post/delete/put/patch/head/options', () => { - // + assert.strictEqual(data.query.page, axiorData.query.page); + assert.strictEqual(data.query.size, axiorData.query.size); }); it('should work with timeout', async () => { const axiosInstance = axios.create({