From 9235d190c68baf982da0f2e684b73e864a542627 Mon Sep 17 00:00:00 2001 From: Pawan Osman Date: Sun, 14 Apr 2024 21:05:48 +0300 Subject: [PATCH] Add: cloudflared tunnel --- .env.example | 1 + .gitignore | 3 +- src/app.ts | 123 ++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 121 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index 17b8d70..97ac499 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ SERVER_PORT=3040 +CLOUDFLARED=true PROXY=false PROXY_HOST=proxy.example.com diff --git a/.gitignore b/.gitignore index 4b3fa99..d3f239f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ dist/ *.js.map .parcel-cache db.json -.env \ No newline at end of file +.env +cloudflared \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index 17be5ea..07e4601 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,7 +1,11 @@ import express, { Request, Response, NextFunction } from "express"; +import { ChildProcessWithoutNullStreams, spawn } from "child_process"; +import fs from "fs"; +import path from "path"; import bodyParser from "body-parser"; import axios from "axios"; import https from "https"; +import os from "os"; import { encode } from "gpt-3-encoder"; import { randomUUID } from "crypto"; import { config } from "dotenv"; @@ -14,6 +18,7 @@ const baseUrl = "https://chat.openai.com"; const apiUrl = `${baseUrl}/backend-anon/conversation`; const refreshInterval = 60000; // Interval to refresh token in ms const errorWait = 120000; // Wait time in ms after an error +let cloudflared: ChildProcessWithoutNullStreams; // Initialize global variables to store the session token and device ID let token: string; @@ -133,7 +138,7 @@ async function handleChatCompletion(req: Request, res: Response) { try { const body = { action: "next", - messages: req.body.messages.map((message) => ({ + messages: req.body.messages.map((message: { role: any; content: any }) => ({ author: { role: message.role }, content: { content_type: "text", parts: [message.content] }, })), @@ -322,17 +327,125 @@ app.use((req, res) => }) ); +async function DownloadCloudflared(): Promise { + const platform = os.platform(); + let url: string; + + if (platform === "win32") { + const arch = os.arch() === "x64" ? "amd64" : "386"; + url = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-${arch}.exe`; + } else { + let arch = os.arch(); + switch (arch) { + case "x64": + arch = "amd64"; + break; + case "arm": + case "arm64": + break; + default: + arch = "amd64"; // Default to amd64 if unknown architecture + } + const platformLower = platform.toLowerCase(); + url = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-${platformLower}-${arch}`; + } + + const fileName = platform === "win32" ? "cloudflared.exe" : "cloudflared"; + const filePath = path.resolve(fileName); + + if (fs.existsSync(filePath)) { + return filePath; + } + + try { + const response = await axiosInstance({ + method: "get", + url: url, + responseType: "stream", + }); + + const writer = fs.createWriteStream(filePath); + + response.data.pipe(writer); + + return new Promise((resolve, reject) => { + writer.on("finish", () => { + if (platform !== "win32") { + fs.chmodSync(filePath, 0o755); + } + resolve(filePath); + }); + + writer.on("error", reject); + }); + } catch (error: any) { + // console.error("Failed to download file:", error.message); + return null; + } +} + +async function StartCloudflaredTunnel(cloudflaredPath: string): Promise { + const localUrl = `http://localhost:${port}`; + return new Promise((resolve, reject) => { + cloudflared = spawn(cloudflaredPath, ["tunnel", "--url", localUrl]); + + cloudflared.stdout.on("data", (data: any) => { + const output = data.toString(); + // console.log("Cloudflared Output:", output); + + // Adjusted regex to specifically match URLs that end with .trycloudflare.com + const urlMatch = output.match(/https:\/\/[^\s]+\.trycloudflare\.com/); + if (urlMatch) { + let url = urlMatch[0]; + resolve(url); + } + }); + + cloudflared.stderr.on("data", (data: any) => { + const output = data.toString(); + // console.error("Error from cloudflared:", output); + + const urlMatch = output.match(/https:\/\/[^\s]+\.trycloudflare\.com/); + if (urlMatch) { + let url = urlMatch[0]; + resolve(url); + } + }); + + cloudflared.on("close", (code: any) => { + resolve(null); + // console.log(`Cloudflared tunnel process exited with code ${code}`); + }); + }); +} + // Start the server and the session ID refresh loop -app.listen(port, () => { +app.listen(port, async () => { + if (process.env.CLOUDFLARED === undefined) process.env.CLOUDFLARED = "true"; + let cloudflared = process.env.CLOUDFLARED === "true"; + let filePath: string; + let publicURL: string; + if (cloudflared) { + filePath = await DownloadCloudflared(); + publicURL = await StartCloudflaredTunnel(filePath); + } + console.log(`💡 Server is running at http://localhost:${port}`); console.log(); - console.log(`🔗 Base URL: http://localhost:${port}/v1`); - console.log(`🔗 ChatCompletion Endpoint: http://localhost:${port}/v1/chat/completions`); + console.log(`🔗 Local Base URL: http://localhost:${port}/v1`); + console.log(`🔗 Local Endpoint: http://localhost:${port}/v1/chat/completions`); console.log(); + if (cloudflared && publicURL) console.log(`🔗 Public Base URL: ${publicURL}/v1`); + if (cloudflared && publicURL) console.log(`🔗 Public Endpoint: ${publicURL}/v1/chat/completions`); + else if (cloudflared && !publicURL) { + console.log("🔗 Public Endpoint: (Failed to start cloudflared tunnel, please restart the server.)"); + if (filePath) fs.unlinkSync(filePath); + } + if (cloudflared && publicURL) console.log(); console.log("📝 Author: Pawan.Krd"); console.log(`🌐 Discord server: https://discord.gg/pawan`); console.log("🌍 GitHub Repository: https://github.com/PawanOsman/ChatGPT"); - console.log(`Don't forget to ⭐ star the repository if you like this project!`); + console.log(`💖 Don't forget to star the repository if you like this project!`); console.log(); setTimeout(async () => {