const os = require("os"); const fs = require("fs"); const path = require("path"); const http = require("http"); const { io } = require("socket.io-client"); const HttpsProxyAgent = require("https-proxy-agent"); const { program, InvalidArgumentError, Argument } = require("commander"); const { TunnelRequest, TunnelResponse } = require("./lib"); let socket = null; function keepAlive() { setTimeout(() => { if (socket && socket.connected) { socket.send("ping"); } keepAlive(); }, 5000); } function initClient(options) { const initParams = { path: "/$web_tunnel", transports: ["websocket"], auth: { token: options.token, }, }; const http_proxy = process.env.https_proxy || process.env.http_proxy; if (http_proxy) { initParams.agent = new HttpsProxyAgent(http_proxy); } socket = io(options.server, initParams); socket.on("connect", () => { if (socket.connected) { console.log("client connect to server successfully"); } }); socket.on("connect_error", (e) => { console.log("connect error", e && e.message); }); socket.on("disconnect", () => { console.log("client disconnected"); }); socket.on("request", (requestId, request) => { const isWebSocket = request.headers.upgrade === "websocket"; console.log(`${isWebSocket ? "WS" : request.method}: `, request.path); request.port = options.port; request.hostname = options.host; if (options.origin) { request.headers.host = options.origin; } const tunnelRequest = new TunnelRequest({ requestId, socket: socket, }); const localReq = http.request(request); tunnelRequest.pipe(localReq); const onTunnelRequestError = (e) => { tunnelRequest.off("end", onTunnelRequestEnd); localReq.destroy(e); }; const onTunnelRequestEnd = () => { tunnelRequest.off("error", onTunnelRequestError); }; tunnelRequest.once("error", onTunnelRequestError); tunnelRequest.once("end", onTunnelRequestEnd); const onLocalResponse = (localRes) => { localReq.off("error", onLocalError); if (isWebSocket && localRes.upgrade) { return; } const tunnelResponse = new TunnelResponse({ responseId: requestId, socket: socket, }); tunnelResponse.writeHead( localRes.statusCode, localRes.statusMessage, localRes.headers, localRes.httpVersion ); localRes.pipe(tunnelResponse); }; const onLocalError = (error) => { console.log(error); localReq.off("response", onLocalResponse); socket.emit("request-error", requestId, error && error.message); tunnelRequest.destroy(error); }; const onUpgrade = (localRes, localSocket, localHead) => { // localSocket.once('error', onTunnelRequestError); if (localHead && localHead.length) localSocket.unshift(localHead); const tunnelResponse = new TunnelResponse({ responseId: requestId, socket: socket, duplex: true, }); tunnelResponse.writeHead(null, null, localRes.headers); localSocket.pipe(tunnelResponse).pipe(localSocket); }; localReq.once("error", onLocalError); localReq.once("response", onLocalResponse); if (isWebSocket) { localReq.on("upgrade", onUpgrade); } }); keepAlive(); } program.name("http-tunnel").description("HTTP tunnel client"); program .command("start") .argument("", "local server port number", (value) => { const port = parseInt(value, 10); if (isNaN(port)) { throw new InvalidArgumentError("Not a number."); } return port; }) .option("-p, --profile ", "setting profile name", "default") .option("-h, --host ", "local host value", "localhost") .option("-o, --origin ", "change request origin") .action((port, options) => { const configDir = path.resolve(os.homedir(), ".http-tunnel"); if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir); } let config = {}; const configFilename = `${options.profile}.json`; const configFilePath = path.resolve(configDir, configFilename); if (fs.existsSync(configFilePath)) { config = JSON.parse(fs.readFileSync(configFilePath, "utf8")); } if (!config.server) { console.log("Please set remote tunnel server firstly"); return; } if (!config.token) { console.log(`Please set jwt token for ${config.server} firstly`); return; } options.port = port; options.token = config.token; options.server = config.server; initClient(options); }); program .command("config") .addArgument(new Argument("", "config type").choices(["jwt", "server"])) .argument("", "config value") .option("-p --profile ", "setting profile name", "default") .action((type, value, options) => { const configDir = path.resolve(os.homedir(), ".http-tunnel"); if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir); } let config = {}; const configFilename = `${options.profile}.json`; const configFilePath = path.resolve(configDir, configFilename); if (fs.existsSync(configFilePath)) { config = JSON.parse(fs.readFileSync(configFilePath, "utf8")); } if (type === "jwt") { config.token = value; } if (type === "server") { config.server = value; } fs.writeFileSync(configFilePath, JSON.stringify(config, null, 2)); console.log(`${type} config saved successfully to: ${configFilePath}`); }); program.parse();