import * as fs from "fs"; import * as http from "http"; import { HttpsProxyAgent } from "https-proxy-agent"; import * as os from "os"; import * as path from "path"; import { Socket, io } from "socket.io-client"; import { TunnelRequest, TunnelResponse } from "./lib"; import { addPrefixOnHttpSchema, generateUUID } from "./util"; import { PROFILE_DEFAULT, PROFILE_PATH, SERVER_DEFAULT_URL, TOKEN_FREE } from "./constant"; import { ClientOptions, Options } from "./interface"; import { getTokenFree } from './sdk'; // create socket instance let socket: Socket | null = null; let keepAliveTimer: NodeJS.Timeout | null = null; function keepAlive() { keepAliveTimer = setTimeout(() => { if (socket && socket.connected) { socket.send("ping"); } keepAlive(); }, 5000); } // Init the Client const initClient = async (options: any) => { 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")); } // Force reset config server from client init if (!config.server || options.force) { 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; } let errorCode = 0; if (!config.token || options.force) { console.log(`Generating token from server: ${config.server}`); await getTokenFree(config.server, { timestamp: (new Date().getTime()), clientId: config.clientId, apiKey: config.apiKey, }) .then((resp: any) => { if (resp.data?.token) { console.log("Token generated successfully!"); config.token = resp.data?.token; } else { errorCode = 1; console.error("Generate free token failed, return with null or empty from server!", resp); return; } }) .catch((err: any) => { errorCode = 1; console.error("cannot get free token from server", err); return; }); } if (errorCode === 0) { fs.writeFileSync(configFilePath, JSON.stringify(config, null, 2)); console.log(`initialized config saved successfully to: ${configFilePath}`); } }; // Start Client const initStartClient = async (options: 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.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 || SERVER_DEFAULT_URL, 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.error( `${clientLogPrefix} connect error:`, (e && e.message) || "something wrong" ); if (e && e.message && e.message.startsWith("[40")) { process.exit(1); } }); socket.on("disconnect", (reason) => { console.warn(`${clientLogPrefix} disconnected: ${reason}!`); }); socket.on("disconnect_exit", (reason) => { console.warn(`${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) => { console.error("tunnel request error: ", 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: 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.error("local error:", 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(); }; const startClient = (clientOptions: ClientOptions) => { const { port, options = {} } = clientOptions; const configDir = path.resolve(os.homedir(), PROFILE_PATH); if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir); } let config: any = {}; const configFilename = `${options.profile || PROFILE_DEFAULT}.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(); } initStartClient(options); }; const stopClient = () => { if (socket) { socket.disconnect(); socket.close(); socket = null; keepAliveTimer && clearInterval(keepAliveTimer); console.log("client stopped"); } }; export { initClient, startClient, stopClient, };