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"); const { generateUUID, addPrefixOnHttpSchema } = require("./util"); // constants const PROFILE_DEFAULT = "default"; // create socket instance let socket = null; function keepAlive() { setTimeout(() => { if (socket && socket.connected) { socket.send("ping"); } keepAlive(); }, 5000); } function initClient(options) { // Please change this if your domain goes wrong here // Current style using sub-domain: https://{{clientId}}-tunnel.myhostingdomain.com // (Original server: https://tunnel.myhostingdomain.com) const profile = options.profile || PROFILE_DEFAULT; const clientId = `${options.clientId || generateUUID()}`; const clientIdSub = `${clientId}-${profile}-`; const serverUrl = addPrefixOnHttpSchema(options.server, clientIdSub); const initParams = { path: "/$cubetiq_http_tunnel", transports: ["websocket"], auth: { token: options.token, apiKey: options.apiKey, clientId: options.clientId, profile: options.profile, clientIdSub: clientIdSub, serverUrl: serverUrl, }, }; const http_proxy = process.env.https_proxy || process.env.http_proxy; if (http_proxy) { initParams.agent = new HttpsProxyAgent(http_proxy); } // Connecting to socket server and agent here... console.log(`client connecting to server: ${serverUrl}`); socket = io(serverUrl, initParams); const clientLogPrefix = `client: ${clientId} on profile: ${profile}`; socket.on("connect", () => { if (socket.connected) { console.log(`${clientLogPrefix} is connected to server successfully!`); } }); socket.on("connect_error", (e) => { console.log(`${clientLogPrefix} connect error!`, e && e.message); }); socket.on("disconnect", () => { console.log(`${clientLogPrefix} 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("CUBETIQ 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", PROFILE_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.error("Please set remote tunnel server firstly!"); return; } if (!config.token) { console.error(`Please set jwt token for ${config.server} firstly!`); return; } if (!config.clientId) { console.error(`Please create client for ${config.server} firstly!`); return; } options.port = port; options.token = config.token; options.server = config.server; options.clientId = config.clientId; options.apiKey = config.apiKey; initClient(options); }); program .command("config") .addArgument( new Argument("", "config type").choices([ "token", "server", "client", "key", ]) ) .argument("", "config value") .option("-p --profile ", "setting profile name", PROFILE_DEFAULT) .action((type, value, options) => { if (!type) { console.error("type config is required!"); return; } const configDir = path.resolve(os.homedir(), ".http-tunnel"); if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir); console.log(`config file ${configDir} was created!`); } 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 === "token" || type === "jwt") { config.token = value; } else if (type === "server") { config.server = value; } else if (type === "clientId" || type === "client") { if (!value || value === "" || value === "new") { config.clientId = generateUUID(); } else { config.clientId = value; } console.log(`client: ${config.clientId} was set to config!`); } else if (type === "apiKey" || type === "key") { config.apiKey = value; } else { config[key] = value; } fs.writeFileSync(configFilePath, JSON.stringify(config, null, 2)); console.log(`${type} config saved successfully to: ${configFilePath}!`); }); program.parse();