UNPKG

publichost

Version:

Make your localhost public. Tunnel HTTP and WebSocket traffic to your local machine from any domain name.

237 lines (231 loc) 8.5 kB
// src/constants.ts var DEFAULT_CLIENT_PORT = "3000"; var CONFIG_FILE_NAME = process.env.IS_LOCAL_SERVER === "true" || process.env.IS_TEST === "true" ? ".publichost.dev.json" : ".publichost.json"; var INITIAL_CONFIG = { subdomains: {} }; var DEFAULT_START_OPTIONS = { isHttps: false, localhostAppPort: DEFAULT_CLIENT_PORT }; // src/libs/ConfigManager.ts import { homedir } from "os"; import { join } from "path"; import { pathExistsSync, readJSONSync, writeJSONSync } from "fs-extra/esm"; var ConfigManager = class { configPath; constructor() { this.configPath = join(homedir(), CONFIG_FILE_NAME); } getWorkspaceConfig(workspacePath) { const config = this.#load(); const configEntry = Object.entries(config.subdomains).find( ([, workspaceConfig]) => workspaceConfig.workspacePath === workspacePath ); if (configEntry) { return configEntry[1]; } if (workspacePath.includes("/")) { const parentDirectory = workspacePath.split("/").slice(0, -1).join("/"); return this.getWorkspaceConfig(parentDirectory); } return void 0; } setWorkspaceConfig(newOrNextWorkspaceConfig) { const config = { ...this.#load() }; config.subdomains[newOrNextWorkspaceConfig.subdomain] = newOrNextWorkspaceConfig; this.#save(config); } #load() { if (!pathExistsSync(this.configPath)) { writeJSONSync(this.configPath, INITIAL_CONFIG, { spaces: 2 }); } return readJSONSync(this.configPath); } #save(nextConfig) { writeJSONSync(this.configPath, nextConfig, { spaces: 2 }); } }; var configManager = new ConfigManager(); // src/commands/start.ts import axios, { AxiosError } from "axios"; import { B } from "bhala"; // ../common/src/ClientMessage.types.ts var ClientMessage; ((ClientMessage2) => { let Type; ((Type2) => { Type2["ERROR"] = "ERROR"; Type2["REGISTER"] = "REGISTER"; Type2["RESPONSE"] = "RESPONSE"; })(Type = ClientMessage2.Type || (ClientMessage2.Type = {})); })(ClientMessage || (ClientMessage = {})); // ../common/src/ServerMessage.types.ts var ServerMessage; ((ServerMessage2) => { let Type; ((Type2) => { Type2["ERROR"] = "ERROR"; Type2["REGISTERED"] = "REGISTERED"; Type2["REQUEST"] = "REQUEST"; })(Type = ServerMessage2.Type || (ServerMessage2.Type = {})); })(ServerMessage || (ServerMessage = {})); // src/commands/start.ts import { WebSocket } from "ws"; function start(publicHostServerHost, subdomain, apiKey, options) { const controlledOptions = { ...DEFAULT_START_OPTIONS, ...options }; const { isHttps, localhostAppPort } = controlledOptions; const localhostAppBaseUrl = process.env.IS_TEST === "true" ? "https://jsonplaceholder.typicode.com" : `http${isHttps ? "s" : ""}://localhost:${localhostAppPort}`; const webSocketScheme = process.env.IS_LOCAL_SERVER === "true" || process.env.IS_TEST === "true" ? "ws" : "wss"; const webSocketUrl = `${webSocketScheme}://${publicHostServerHost}/${subdomain}`; const ws = new WebSocket(webSocketUrl, { headers: { "x-api-key": apiKey } }); ws.on("open", () => { B.log( "[PublicHost Client]", `[${subdomain}]`, `Connected to PublicHost Server on ${webSocketUrl}.`, "Registering subdomain..." ); ws.send(JSON.stringify({ type: ClientMessage.Type.REGISTER, subdomain })); }); ws.on("ping", () => { ws.pong(void 0, void 0, () => { }); }); ws.on("message", async (data) => { try { const serverMessage = JSON.parse(data); switch (serverMessage.type) { case ServerMessage.Type.REGISTERED: { B.success("[PublicHost Client]", `[${subdomain}]`, "Subdomain registered."); B.info( "[PublicHost Client]", `[${subdomain}]`, `You can now access your localhost app at https://${subdomain}.${publicHostServerHost}.` ); return; } case ServerMessage.Type.ERROR: { B.error("[PublicHost Client]", `[${subdomain}]`, `PublicHost Server sent an error: ${serverMessage.error}.`); return; } case ServerMessage.Type.REQUEST: break; default: { B.error("[PublicHost Client]", `[${subdomain}]`, `Invalid message type: ${serverMessage.type}.`); return; } } const serverRequestMessage = serverMessage; B.log( "[PublicHost Client]", `[${subdomain}]`, `[${serverRequestMessage.id}]`, `\u27A1\uFE0F Forwarding HTTP Request ${serverMessage.request.method} ${serverMessage.request.url} to Localhost App.` ); const cleanHeaders = Object.fromEntries( Object.entries(serverRequestMessage.request.headers).filter(([key]) => !["host"].includes(key)) ); try { const localhostAppResponse = await axios({ method: serverRequestMessage.request.method, url: `${localhostAppBaseUrl}${serverRequestMessage.request.url}`, headers: cleanHeaders, data: serverRequestMessage.request.rawBody }); B.log( "[PublicHost Client]", `[${subdomain}]`, `[${serverRequestMessage.id}]`, `\u2B05\uFE0F Forwarding HTTP Response ${localhostAppResponse.status} for ${serverMessage.request.method} ${serverMessage.request.url} to PublicHost Server.` ); const clientResponseMessage = { id: serverRequestMessage.id, type: ClientMessage.Type.RESPONSE, response: { status: localhostAppResponse.status, headers: localhostAppResponse.headers, rawBody: localhostAppResponse.data } }; ws.send(JSON.stringify(clientResponseMessage)); } catch (err) { if (!(err instanceof AxiosError && err.response)) { B.error("[PublicHost Client]", `[${subdomain}]`, `[${serverRequestMessage.id}]`, "An unknown error occurred."); const clientResponseMessage2 = { id: serverRequestMessage.id, type: ClientMessage.Type.RESPONSE, response: { status: 500, headers: {}, rawBody: "" } }; ws.send(JSON.stringify(clientResponseMessage2)); return; } B.warn( "[PublicHost Client]", `[${subdomain}]`, `[${serverRequestMessage.id}]`, `\u2B05\uFE0F Forwarding HTTP Response ${err.response.status ?? 500} to PublicHost Server.` ); B.debug( "[PublicHost Client]", `[${subdomain}]`, `[${serverRequestMessage.id}]`, `Error reponse body: ${err.response.data ? JSON.stringify(err.response.data) : err}` ); const clientResponseMessage = { id: serverRequestMessage.id, type: ClientMessage.Type.RESPONSE, response: { status: err.response.status, headers: err.response.headers ?? {}, rawBody: err.response.data } }; ws.send(JSON.stringify(clientResponseMessage)); } } catch (err) { B.error("[PublicHost Client]", `[${subdomain}]`, "An unknown error occurred."); B.debug("[PublicHost Client]", `[${subdomain}]`, `Error: ${err}.`); } }); ws.on("close", () => { B.log("[PublicHost Client]", `[${subdomain}]`, "Connection closed by PublicHost Server.", "Reconnecting in 5s..."); setTimeout(() => { B.log("[PublicHost Client]", `[${subdomain}]`, "Reconnecting to PublicHost Server..."); start(publicHostServerHost, subdomain, apiKey, options); }, 5e3); }); ws.on("error", (error) => { B.error("[PublicHost Client]", `[${subdomain}]`, `PublicHost Server connection error: ${error}.`); }); } function startFromConfig() { const workspacePath = process.cwd(); const workspaceConfig = configManager.getWorkspaceConfig(workspacePath); if (!workspaceConfig) { B.error( "[PublicHost Client]", `No configuration found for the current workspace or any of its parents: \`${workspacePath}\`.` ); B.info("[PublicHost Client]", "Run `ph init` to initialize a new configuration for this workspace."); return; } const { apiKey, options, publicHostServerHost, subdomain } = workspaceConfig; start(publicHostServerHost, subdomain, apiKey, options); } export { DEFAULT_CLIENT_PORT, DEFAULT_START_OPTIONS, configManager, start, startFromConfig }; //# sourceMappingURL=chunk-7NWSCVNS.js.map