Skip to content

Commit

Permalink
feat: awaitable .web and .ws`
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 committed Aug 23, 2023
1 parent 4ceca85 commit e4dad27
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 90 deletions.
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,19 @@ import { createProxyServer } from "httpxy";

const proxy = createProxyServer({});

createServer((req, res) => {
proxy.web(req, res, {
target: "http://example.com",
headers: { host: "example.com" },
});
}).listen(3000, () => {
const server = createServer(async (req, res) => {
try {
await httpProxy.web(req, res, {
target: main.url,
});
} catch (error) {
console.error(error);
res.statusCode = 500;
res.end("Proxy error: " + error.toString());
}
});

server.listen(3000, () => {
console.log("Proxy is listening on http://localhost:3000");
});
```
Expand Down
16 changes: 11 additions & 5 deletions playground/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,19 @@ async function main() {
{ port: 3000, name: "main" },
);

const httpProxy = createProxyServer({
target: main.url,
});
const httpProxy = createProxyServer();

await listen(
(req, res) => {
httpProxy.web(req, res, { target: main.url });
async (req, res) => {
try {
await httpProxy.web(req, res, {
target: main.url,
});
} catch (error) {
console.error(error);
res.statusCode = 500;
res.end("Proxy error: " + error.toString());
}
},
{ port: 3001, name: "proxy" },
);
Expand Down
141 changes: 64 additions & 77 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,24 @@ import { ProxyMiddleware } from "./middleware/_utils";
export class ProxyServer extends EventEmitter {
_server?: http.Server | https.Server;

webPasses: readonly ProxyMiddleware[] = webIncomingMiddleware;
wsPasses: readonly ProxyMiddleware[] = websocketIncomingMiddleware;
webPasses: ProxyMiddleware[] = [...webIncomingMiddleware];
wsPasses: ProxyMiddleware[] = [...websocketIncomingMiddleware];

options: ProxyServerOptions;

web: (
req: http.IncomingMessage,
res: http.OutgoingMessage,
opts?: ProxyServerOptions,
) => any;
head?: any,
) => Promise<void>;

ws: (
req: http.IncomingMessage,
socket: http.OutgoingMessage,
opts: ProxyServerOptions,
) => any;
head?: any,
) => Promise<void>;

/**
* Creates the proxy server with specified options.
Expand All @@ -38,8 +40,8 @@ export class ProxyServer extends EventEmitter {
this.options = options || {};
this.options.prependPath = options.prependPath !== false;

this.web = _createRightProxy("web")(this);
this.ws = _createRightProxy("ws")(this);
this.web = _createProxyFn("web", this);
this.ws = _createProxyFn("ws", this);
}

/**
Expand Down Expand Up @@ -83,44 +85,38 @@ export class ProxyServer extends EventEmitter {
}
}

before(type, passName, callback) {
before(type: "ws" | "web", passName: string, pass: ProxyMiddleware) {
if (type !== "ws" && type !== "web") {
throw new Error("type must be `web` or `ws`");
}
const passes = [...(type === "ws" ? this.wsPasses : this.webPasses)];
const passes = type === "ws" ? this.wsPasses : this.webPasses;
let i: false | number = false;

for (const [idx, v] of passes.entries()) {
if (v.name === passName) {
i = idx;
}
}

if (i === false) {
throw new Error("No such pass");
}

passes.splice(i, 0, callback);
passes.splice(i, 0, pass);
}

after(type, passName, callback) {
after(type: "ws" | "web", passName: string, pass: ProxyMiddleware) {
if (type !== "ws" && type !== "web") {
throw new Error("type must be `web` or `ws`");
}
const passes = [...(type === "ws" ? this.wsPasses : this.webPasses)];
const passes = type === "ws" ? this.wsPasses : this.webPasses;
let i: boolean | number = false;

for (const [idx, v] of passes.entries()) {
if (v.name === passName) {
i = idx;
}
}

if (i === false) {
throw new Error("No such pass");
}

passes.splice(i++, 0, callback);
passes.splice(i++, 0, pass);
}
}

Expand All @@ -144,69 +140,60 @@ export function createProxyServer(options: ProxyServerOptions = {}) {

// --- Internal ---

/**
* Returns a function that creates the loader for
* either `ws` or `web`'s passes.
*
* Examples:
*
* httpProxy.createRightProxy('ws')
* // => [Function]
*
* @param {String} Type Either 'ws' or 'web'
*
* @return {Function} Loader Function that when called returns an iterator for the right passes
*
* @api private
*/

function _createRightProxy(type) {
return function (server: ProxyServer) {
return function (
req: http.IncomingMessage,
res: http.OutgoingMessage,
opts: ProxyServerOptions,
) {
const passes = type === "ws" ? this.wsPasses : this.webPasses;

const requestOptions = { ...opts, ...server.options };
function _createProxyFn(type: "web" | "ws", server: ProxyServer) {
return function (
req: http.IncomingMessage,
res: http.OutgoingMessage,
opts: ProxyServerOptions,
head: any,
): Promise<void> {
const requestOptions = { ...opts, ...server.options };

for (const key of ["target", "forward"]) {
if (typeof requestOptions[key] === "string") {
requestOptions[key] = new URL(requestOptions[key]);
}
for (const key of ["target", "forward"]) {
if (typeof requestOptions[key] === "string") {
requestOptions[key] = new URL(requestOptions[key]);
}
}

if (!requestOptions.target && !requestOptions.forward) {
return this.emit(
"error",
new Error("Must provide a proper URL as target"),
);
}
if (!requestOptions.target && !requestOptions.forward) {
return this.emit(
"error",
new Error("Must provide a proper URL as target"),
);
}

for (const pass of passes) {
/**
* Call of passes functions
* pass(req, res, options, head)
*
* In WebSockets case the `res` variable
* refer to the connection socket
* pass(req, socket, options, head)
*/
if (
pass(
req,
res,
requestOptions,
server,
undefined /* head */,
undefined /* cb */,
)
) {
// passes can return a truthy value to halt the loop
break;
}
let _resolve: () => void;
let _reject: (error: any) => void;
const callbackPromise = new Promise<void>((resolve, reject) => {
_resolve = resolve;
_reject = reject;
});

res.on("close", () => {
_resolve();
});
res.on("error", (error: any) => {
_reject(error);
});

for (const pass of type === "ws" ? server.wsPasses : server.webPasses) {
const stop = pass(
req,
res,
requestOptions as ProxyServerOptions & { target: URL; forward: URL },
server,
head,
(error) => {
_reject(error);
},
);
// Passes can return a truthy value to halt the loop
if (stop) {
_resolve();
break;
}
};
}

return callbackPromise;
};
}
19 changes: 17 additions & 2 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ describe("httpxy", () => {
let proxyListener: Listener;
let proxy: ProxyServer;

let lastResolved: any;
let lastRejected: any;

beforeAll(async () => {
mainListener = await listen((req, res) => {
res.end(
Expand All @@ -21,8 +24,17 @@ describe("httpxy", () => {

proxy = createProxyServer({});

proxyListener = await listen((req, res) => {
proxy.web(req, res, { target: mainListener.url });
proxyListener = await listen(async (req, res) => {
lastResolved = false;
lastRejected = undefined;
try {
await proxy.web(req, res, { target: mainListener.url });
lastResolved = true;
} catch (error) {
lastRejected = error;
res.statusCode = 500;
res.end("Proxy error: " + error.toString());
}
});
});

Expand All @@ -38,5 +50,8 @@ describe("httpxy", () => {
expect(maskResponse(await mainResponse)).toMatchObject(
maskResponse(proxyResponse),
);

expect(lastResolved).toBe(true);
expect(lastRejected).toBe(undefined);
});
});

0 comments on commit e4dad27

Please sign in to comment.