import * as os from "os"; import * as fs from "fs"; import * as path from "path"; import * as http from "http"; import { io } from "socket.io-client"; import { HttpsProxyAgent } from "https-proxy-agent"; import { program, InvalidArgumentError, Argument } from "commander"; import { TunnelRequest, TunnelResponse } from "./lib"; import { generateUUID, addPrefixOnHttpSchema } from "./util"; import { Socket } from 'socket.io-client'; import { getTokenFree } from './sdk' const packageInfo = require("../package.json"); // 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: Socket | null = null; function keepAlive() { setTimeout(() => { if (socket && socket.connected) { socket.send("ping"); } keepAlive(); }, 5000); } function initClient(options: any) { // 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.apiKey || options.clientId || generateUUID()}`; const clientIdSub = profile === PROFILE_DEFAULT ? `${clientId}-` : `${clientId}-${profile}-`; const clientEndpoint = ( options.suffix ? `${clientIdSub}${options.suffix}-` : clientIdSub ) .toLowerCase() .trim(); const serverUrl = addPrefixOnHttpSchema(options.server, clientEndpoint); // extra options for socket to identify the client (authentication and options of tunnel) const defaultParams = { apiKey: options.apiKey, clientId: options.clientId, profile: options.profile, clientIdSub: clientIdSub, clientEndpoint: clientEndpoint, serverUrl: serverUrl, access: options.access, keep_connection: options.keep_connection || true, }; // extra info for notify about the running of the tunnel (it's private info, other platfom cannot access this) // this using for internal only (don't worry about this) const osInfo = { hostname: os.hostname(), platform: os.platform(), arch: os.arch(), release: os.release(), }; const initParams: any = { path: "/$cubetiq_http_tunnel", transports: ["websocket"], auth: { token: options.token, ...defaultParams, }, headers: { ...defaultParams, os: osInfo, }, // reconnection: true, }; 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) || "something wrong" ); if (e && e.message && e.message.startsWith("[40")) { process.exit(1); } }); socket.on("disconnect", (reason) => { console.log(`${clientLogPrefix} disconnected: ${reason}!`); }); socket.on("disconnect_exit", (reason) => { console.log(`${clientLogPrefix} disconnected and exited ${reason}!`); socket?.disconnect(); process.exit(1); }); 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(socket!, requestId); const localReq = http.request(request); tunnelRequest.pipe(localReq); const onTunnelRequestError = (e: any) => { tunnelRequest.off("end", onTunnelRequestEnd); localReq.destroy(e); }; const onTunnelRequestEnd = () => { tunnelRequest.off("error", onTunnelRequestError); }; tunnelRequest.once("error", onTunnelRequestError); tunnelRequest.once("end", onTunnelRequestEnd); const onLocalResponse = (localRes: any) => { localReq.off("error", onLocalError); if (isWebSocket && localRes.upgrade) { return; } const tunnelResponse = new TunnelResponse(socket!, requestId); tunnelResponse.writeHead( localRes.statusCode, localRes.statusMessage, localRes.headers, localRes.httpVersion ); localRes.pipe(tunnelResponse); }; const onLocalError = (error: any) => { console.log(error); localReq.off("response", onLocalResponse); socket?.emit("request-error", requestId, error && error.message); tunnelRequest.destroy(error); }; const onUpgrade = (localRes: any, localSocket: any, localHead: any) => { // localSocket.once('error', onTunnelRequestError); if (localHead && localHead.length) localSocket.unshift(localHead); const tunnelResponse = new TunnelResponse(socket!, requestId, 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); } }); // reconnect manually const tryReconnect = () => { setTimeout(() => { socket!.io.open((err) => { if (err) { tryReconnect(); } }); }, 2000); }; // socket.io.on("close", tryReconnect); keepAlive(); } program .name("hlt") .description( "CUBETIQ HTTP tunnel client with free access for local tunneling" ) .version(`v${packageInfo.version}`); // init program .command("init") .description("generate a new client and token with free access") .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( "-k --key ", "setting client api key for authentication access" ) .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: any = {}; 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 = options.server || SERVER_DEFAULT_URL; } if (!config.token && options.token) { config.token = options.token; } if (!config.access) { config.access = options.access || TOKEN_FREE; } if (!config.clientId) { config.clientId = options.client || generateUUID(); } if (!config.apiKey && options.key) { config.apiKey = options.key; } if (!config.token) { console.log("Generating token..."); await getTokenFree(config.server) .then((resp: any) => { if (resp.data?.token) { config.token = resp.data?.token; } else { console.log("free token return with null or empty from server"); return; } }) .catch((err: any) => { 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") .description("start a connection with specific port") .argument("", "local server port number", (value) => { const port = parseInt(value, 10); if (isNaN(port)) { throw new InvalidArgumentError("Not a number."); } return port; }) .option("-s, --suffix ", "suffix for client name") .option( "-K, --keep_connection ", "keep connection for client and old connection will be closed (override connection)", true ) .option( "-k --key ", "setting client api key for authentication access" ) .option("-a, --access ", "access type (FREE)", TOKEN_FREE) .option("-p, --profile ", "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: any = {}; 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 init or set token for ${config.server}`); return; } if (!config.clientId) { if (!config.apiKey) { console.info(`please init or create a 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 = options.key || config.apiKey; if (options.suffix === "port" || options.suffix === "true") { options.suffix = `${port}`; } else if (options.suffix === "false") { options.suffix = undefined; } else if (options.suffix === "gen" || options.suffix === "uuid") { options.suffix = generateUUID(); } initClient(options); }); // config program .command("config") .description("create and update config file for connection") .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: any = {}; 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 getTokenFree(config.server) .then((resp: any) => { if (resp.data?.token) { config.token = resp.data?.token; } else { console.log("free token return with null or empty from server"); return; } }) .catch((err: any) => { 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}`); }); // config program .command("config-get") .description("get type from config file") .addArgument( new Argument("", "config type").choices([ "access", "token", "server", "client", "key", ]) ) .option("-p --profile ", "setting profile name", PROFILE_DEFAULT) .action(async (type, options) => { if (!type) { console.error("type config is required!"); return; } const configDir = path.resolve(os.homedir(), PROFILE_PATH); if (!fs.existsSync(configDir)) { console.log(`config file ${configDir} not found`); return; } let config: any = {}; const configFilename = `${options.profile}.json`; const configFilePath = path.resolve(configDir, configFilename); if (fs.existsSync(configFilePath)) { config = JSON.parse(fs.readFileSync(configFilePath, "utf8")); } else { console.log(`config file ${configFilePath} not found`); return; } if (type === "token" || type === "jwt") { console.log(config.token); } else if (type === "server") { console.log(config.server); } else if (type === "clientId" || type === "client") { console.log(config.clientId); } else if (type === "apiKey" || type === "key") { console.log(config.apiKey); } else if (type === "access") { console.log(config.access); } else { console.log('no config found for type: "' + type + '"'); } }); program.parse();