UNPKG

4.87 kBJavaScriptView Raw
1#!/usr/bin/env node
2
3import chalk from "chalk";
4import eventToPromise from "event-to-promise";
5import execPromise from "exec-promise";
6import getStream from "get-stream";
7import minimist from "minimist";
8import { create as createHttpServer } from "http-server-plus";
9import { genSelfSignedCert } from "@xen-orchestra/self-signed";
10import { inspect } from "util";
11import { load as loadConfig } from "app-conf";
12import { parse } from "json-rpc-protocol";
13import { readFileSync } from "node:fs";
14
15import {
16 createReadableCopies,
17 proxyHttpsRequest,
18 splitHost,
19} from "./utils.mjs";
20import { isXmlRpcRequest, parseRequest } from "./xml-rpc.mjs";
21
22// ===================================================================
23
24const paintArg = chalk.yellow;
25
26function pick(obj, keys) {
27 const result = {};
28 for (const key of keys) {
29 result[key] = obj[key];
30 }
31 return result;
32}
33
34const requiredArg = (name) => {
35 const message = `Missing argument: <${paintArg(name)}>`;
36
37 throw message;
38};
39
40const 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
50const 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
146const { name: pkgName, version: pkgVersion } = JSON.parse(
147 readFileSync(new URL("package.json", import.meta.url))
148);
149
150const 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
161execPromise(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 // Work around https://github.com/substack/minimist/issues/71
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});