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"); const sdk = require("./sdk"); // constants const PROFILE_DEFAULT = "default"; const PROFILE_PATH = ".hlt"; const SERVER_DEFAULT_URL = "https://lt.ctdn.net"; const TOKEN_FREE = "FREE"; // 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 || options.apiKey || generateUUID()}`; const clientIdSub = profile === PROFILE_DEFAULT ? `${clientId}-` : `${clientId}-${profile}-`; const serverUrl = addPrefixOnHttpSchema(options.server, clientIdSub); const defaultParams = { apiKey: options.apiKey, clientId: options.clientId, profile: options.profile, clientIdSub: clientIdSub, serverUrl: serverUrl, access: options.access, }; const initParams = { path: "/$cubetiq_http_tunnel", transports: ["websocket"], auth: { token: options.token, ...defaultParams, }, headers: { ...defaultParams, }, }; 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", (reason) => { console.log(`${clientLogPrefix} disconnected: ${reason}!`); }); 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("hlt").description("CUBETIQ HTTP tunnel client"); // init program .command("init") .option("-s --server ", "setting server url", SERVER_DEFAULT_URL) .option( "-t --token ", "setting token (default generate FREE access token)", "" ) .option("-a --access ", "setting token access type", TOKEN_FREE) .option("-c --client ", "setting client (auto generate uuid)", "") .option("-p --profile ", "setting profile name", PROFILE_DEFAULT) .action(async (options) => { const configDir = path.resolve(os.homedir(), PROFILE_PATH); 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 (options.server) { config.server = options.server; } if (options.token) { config.token = options.token; } if (options.access) { config.access = options.access; } if (options.client) { config.clientId = options.clientId; } if (!config.server) { config.server = SERVER_DEFAULT_URL; } if (!config.clientId) { config.clientId = generateUUID(); } if (!config.access) { config.access = TOKEN_FREE; } if (!config.token) { console.log("Generating token..."); await sdk .getTokenFree(config.server) .then((resp) => { if (resp.data?.token) { config.token = resp.data?.token; } else { console.log("free token return with null or empty from server"); return; } }) .catch((err) => { console.error("cannot get free token from server", err); return; }); } fs.writeFileSync(configFilePath, JSON.stringify(config, null, 2)); console.log(`initialized config saved successfully to: ${configFilePath}`); }); // start 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("-a, --access ", "setting access type (FREE)", TOKEN_FREE) .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(), PROFILE_PATH); 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) { config.server = SERVER_DEFAULT_URL; } if (!config.token) { console.info(`please set token for ${config.server}`); console.info( "if you don't have token yet, please contact to: sombochea@cubetiqs.com" ); return; } if (!config.clientId) { if (!config.apiKey) { console.info(`please create client for ${config.server}`); } else { config.clientId = config.apiKey; } return; } options.port = port; options.token = config.token; options.access = config.access; options.server = config.server; options.clientId = config.clientId; options.apiKey = config.apiKey; initClient(options); }); // config program .command("config") .addArgument( new Argument("", "config type").choices([ "access", "token", "server", "client", "key", ]) ) .argument("", "config value") .option("-p --profile ", "setting profile name", PROFILE_DEFAULT) .action(async (type, value, options) => { if (!type) { console.error("type config is required!"); return; } const configDir = path.resolve(os.homedir(), PROFILE_PATH); 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 (!config.server) { config.server = SERVER_DEFAULT_URL; } 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 if (type === "access") { config.access = (value && value.toUpperCase().trim()) || TOKEN_FREE; // FREE if (config.access === TOKEN_FREE) { await sdk .getTokenFree(config.server) .then((resp) => { if (resp.data?.token) { config.token = resp.data?.token; } else { console.log("free token return with null or empty from server"); return; } }) .catch((err) => { console.error("cannot get free token from server", err); return; }); } } if (!config.clientId && config.apiKey) { config.clientId = config.apiKey; } fs.writeFileSync(configFilePath, JSON.stringify(config, null, 2)); console.log(`${type} config saved successfully to: ${configFilePath}`); }); program.parse();