From afbf3097f190a49c7b4b211cc9156540cc95f7ef Mon Sep 17 00:00:00 2001 From: Sambo Chea Date: Wed, 4 Jan 2023 11:52:20 +0700 Subject: [PATCH] Add api --- bin/hlt | 2 +- package.json | 9 +- src/api.ts | 336 ++++++++++++++++++++++++++++++ src/cli.ts | 214 ++++++++++++++++++++ src/client.ts | 517 ----------------------------------------------- src/constant.ts | 5 + src/interface.ts | 21 ++ src/test.ts | 9 + 8 files changed, 591 insertions(+), 522 deletions(-) create mode 100644 src/api.ts create mode 100644 src/cli.ts delete mode 100644 src/client.ts create mode 100644 src/constant.ts create mode 100644 src/interface.ts create mode 100644 src/test.ts diff --git a/bin/hlt b/bin/hlt index 9fc2969..952b3db 100755 --- a/bin/hlt +++ b/bin/hlt @@ -1,3 +1,3 @@ #!/usr/bin/env node -require('../dist/client'); \ No newline at end of file +require('../dist/cli'); \ No newline at end of file diff --git a/package.json b/package.json index 56638bc..076671e 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,15 @@ { "name": "@cubetiq/hlt", - "version": "0.1.5", + "version": "0.1.6", "description": "A lightweight http tunnel client using nodejs and socket.io client", - "main": "dist/client.js", + "main": "dist/cli.js", "bin": { "hlt": "bin/hlt" }, "scripts": { - "start": "ts-node-dev --respawn --transpile-only src/client.ts", - "local": "ts-node-dev --respawn --transpile-only src/client.ts start -p local", + "start": "ts-node-dev --respawn --transpile-only src/cli.ts", + "test": "ts-node-dev --respawn --transpile-only src/test.ts", + "local": "ts-node-dev --respawn --transpile-only src/cli.ts start -p local", "build": "tsc" }, "repository": { diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..9b8873e --- /dev/null +++ b/src/api.ts @@ -0,0 +1,336 @@ +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, +}; diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..dd321d3 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,214 @@ +import { Argument, InvalidArgumentError, program } from "commander"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import { initClient, startClient } from "./api"; +import { PROFILE_DEFAULT, PROFILE_PATH, SERVER_DEFAULT_URL, TOKEN_FREE } from "./constant"; +import { getTokenFree } from './sdk'; +import { generateUUID } from "./util"; + +const packageInfo = require("../package.json"); + +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) + .option("-f --force", "force to generate new client and token", false) + .action(async (options) => { + initClient(options); + }); + +// 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) => { + startClient({ + port, + 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; + } + + // Error Code status + let errorCode = 0; + + 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 { + 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 (!config.clientId && config.apiKey) { + config.clientId = config.apiKey; + } + + if (errorCode === 0) { + 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(); diff --git a/src/client.ts b/src/client.ts deleted file mode 100644 index 4d20b60..0000000 --- a/src/client.ts +++ /dev/null @@ -1,517 +0,0 @@ -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.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(); -} - -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) - .option("-f --force", "force to generate new client and token", false) - .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")); - } - - // 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 -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; - } - - // Error Code status - let errorCode = 0; - - 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 { - 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 (!config.clientId && config.apiKey) { - config.clientId = config.apiKey; - } - - if (errorCode === 0) { - 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(); diff --git a/src/constant.ts b/src/constant.ts new file mode 100644 index 0000000..0fb6e78 --- /dev/null +++ b/src/constant.ts @@ -0,0 +1,5 @@ +// constants +export const PROFILE_DEFAULT = "default"; +export const PROFILE_PATH = ".hlt"; +export const SERVER_DEFAULT_URL = "https://lt.ctdn.net"; +export const TOKEN_FREE = "FREE"; \ No newline at end of file diff --git a/src/interface.ts b/src/interface.ts new file mode 100644 index 0000000..b658f3f --- /dev/null +++ b/src/interface.ts @@ -0,0 +1,21 @@ +export interface Options { + server?: string; + profile?: string; + key?: string; + apiKey?: string; + access?: string; + suffix?: string; + clientId?: string; + keep_connection?: boolean; + token?: string; + origin?: string; + port?: number; + host?: string; + + // [key: string]: any; +} + +export interface ClientOptions { + port: number; + options?: Options; +} \ No newline at end of file diff --git a/src/test.ts b/src/test.ts new file mode 100644 index 0000000..89f55b5 --- /dev/null +++ b/src/test.ts @@ -0,0 +1,9 @@ +import { startClient, stopClient } from './api'; + +startClient({ + port: 3001, +}); + +setTimeout(() => { + stopClient(); +}, 5000); \ No newline at end of file