1 | #!/usr/bin/env node
|
2 |
|
3 | import chalk from "chalk";
|
4 | import eventToPromise from "event-to-promise";
|
5 | import execPromise from "exec-promise";
|
6 | import getStream from "get-stream";
|
7 | import minimist from "minimist";
|
8 | import { create as createHttpServer } from "http-server-plus";
|
9 | import { genSelfSignedCert } from "@xen-orchestra/self-signed";
|
10 | import { inspect } from "util";
|
11 | import { load as loadConfig } from "app-conf";
|
12 | import { parse } from "json-rpc-protocol";
|
13 | import { readFileSync } from "node:fs";
|
14 |
|
15 | import {
|
16 | createReadableCopies,
|
17 | proxyHttpsRequest,
|
18 | splitHost,
|
19 | } from "./utils.mjs";
|
20 | import { isXmlRpcRequest, parseRequest } from "./xml-rpc.mjs";
|
21 |
|
22 |
|
23 |
|
24 | const paintArg = chalk.yellow;
|
25 |
|
26 | function pick(obj, keys) {
|
27 | const result = {};
|
28 | for (const key of keys) {
|
29 | result[key] = obj[key];
|
30 | }
|
31 | return result;
|
32 | }
|
33 |
|
34 | const requiredArg = (name) => {
|
35 | const message = `Missing argument: <${paintArg(name)}>`;
|
36 |
|
37 | throw message;
|
38 | };
|
39 |
|
40 | const invalidArg = (name, value) => {
|
41 | const message = `Invalid value ${chalk.bold(value)} for argument: <${paintArg(
|
42 | name
|
43 | )}>`;
|
44 |
|
45 | throw message;
|
46 | };
|
47 |
|
48 |
|
49 |
|
50 | const COMMANDS = {
|
51 | async proxy(args) {
|
52 | let {
|
53 | bind = "0",
|
54 | _: [remote = requiredArg("remote address")],
|
55 | } = minimist(args, {
|
56 | string: "bind",
|
57 | });
|
58 |
|
59 | bind = {
|
60 | ...(await genSelfSignedCert()),
|
61 | ...splitHost(bind),
|
62 | };
|
63 | remote = {
|
64 | protocol: "https:",
|
65 | ...splitHost(remote),
|
66 | };
|
67 |
|
68 |
|
69 |
|
70 | const logRpcCall = (url, method, params) =>
|
71 | console.log(
|
72 | "[%s] %s(%s)",
|
73 | chalk.blue(url),
|
74 | chalk.bold.red(method),
|
75 | inspect(params, {
|
76 | colors: true,
|
77 | depth: null,
|
78 | })
|
79 | );
|
80 |
|
81 | const handleJsonRpcRequest = async (req, res) => {
|
82 | const [req1, req2] = createReadableCopies(2, req);
|
83 | const res1 = await proxyHttpsRequest(
|
84 | {
|
85 | ...pick(req, ["headers", "method", "url"]),
|
86 | ...remote,
|
87 | },
|
88 | req1
|
89 | );
|
90 | res.writeHead(res1.statusCode, res1.statusMessage, res1.headers);
|
91 | res1.pipe(res);
|
92 |
|
93 | const { method, params } = parse(await getStream(req2));
|
94 |
|
95 | logRpcCall(req.url, method, params);
|
96 | };
|
97 |
|
98 | const handleRequest = async (req, res) => {
|
99 | console.log("[%s] - Not XML-RPC", chalk.blue(req.url));
|
100 |
|
101 | (await proxyHttpsRequest(remote, req)).pipe(res);
|
102 | };
|
103 |
|
104 | const handleXmlRpcRequest = async (req, res) => {
|
105 | const [req1, req2] = createReadableCopies(2, req);
|
106 | const res1 = await proxyHttpsRequest(
|
107 | {
|
108 | ...pick(req, ["headers", "method", "url"]),
|
109 | ...remote,
|
110 | },
|
111 | req1
|
112 | );
|
113 | res.writeHead(res1.statusCode, res1.statusMessage, res1.headers);
|
114 | res1.pipe(res);
|
115 |
|
116 | const { method, params } = await parseRequest(req2);
|
117 | logRpcCall(req.url, method, params);
|
118 | };
|
119 |
|
120 |
|
121 |
|
122 | const server = createHttpServer(async (req, res) => {
|
123 | try {
|
124 | if (req.url.startsWith("/jsonrpc")) {
|
125 | await handleJsonRpcRequest(req, res);
|
126 | } else if (isXmlRpcRequest(req, res)) {
|
127 | await handleXmlRpcRequest(req, res);
|
128 | } else {
|
129 | await handleRequest(req, res);
|
130 | }
|
131 | } catch (error) {
|
132 | console.error(error.stack || error);
|
133 |
|
134 | throw error;
|
135 | }
|
136 | });
|
137 |
|
138 | console.log(await server.listen(bind));
|
139 |
|
140 | await eventToPromise(server, "close");
|
141 | },
|
142 | };
|
143 |
|
144 |
|
145 |
|
146 | const { name: pkgName, version: pkgVersion } = JSON.parse(
|
147 | readFileSync(new URL("package.json", import.meta.url))
|
148 | );
|
149 |
|
150 | const usage = `Usage: ${pkgName} proxy [--bind <local address>] <remote address>
|
151 |
|
152 | Create a XML-RPC proxy which forward requests from <local address>
|
153 | to <remote address>.
|
154 |
|
155 | <local address>: [<hostname>]:<port>
|
156 | <remote address>: <hostname>[:<port = 443>]
|
157 |
|
158 | ${pkgName} v${pkgVersion}
|
159 | `.replace(/<([^>]+)>/g, (_, arg) => `<${paintArg(arg)}>`);
|
160 |
|
161 | execPromise(async (args) => {
|
162 | const {
|
163 | help = false,
|
164 | _: restArgs,
|
165 | "--": restRestArgs,
|
166 | } = minimist(args, {
|
167 | boolean: "help",
|
168 | alias: {
|
169 | help: "h",
|
170 | },
|
171 | stopEarly: true,
|
172 | "--": true,
|
173 | });
|
174 |
|
175 | if (help) {
|
176 | return usage;
|
177 | }
|
178 |
|
179 |
|
180 | restArgs.push("--");
|
181 | [].push.apply(restArgs, restRestArgs);
|
182 |
|
183 | const [commandName, ...commandArgs] = restArgs;
|
184 |
|
185 | if (commandName === "--") {
|
186 | throw usage;
|
187 | }
|
188 |
|
189 | const command = COMMANDS[commandName];
|
190 | if (!command) {
|
191 | invalidArg("command", commandName);
|
192 | }
|
193 |
|
194 | return command.call(
|
195 | {
|
196 | config: await loadConfig("xapi-inspector", {
|
197 | appDir: new URL(".", import.meta.url).pathname,
|
198 | }),
|
199 | },
|
200 | commandArgs
|
201 | );
|
202 | });
|