diff --git a/docker/debian-base.dockerfile b/docker/debian-base.dockerfile index 9a8c759b..62889dc9 100644 --- a/docker/debian-base.dockerfile +++ b/docker/debian-base.dockerfile @@ -1,8 +1,11 @@ # DON'T UPDATE TO node:14-bullseye-slim, see #372. # If the image changed, the second stage image should be changed too FROM node:16-buster-slim +ARG TARGETPLATFORM + WORKDIR /app +# Install Curl # Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv # Stupid python3 and python3-pip actually install a lot of useless things into Debian, specify --no-install-recommends to skip them, make the base even smaller than alpine! RUN apt update && \ @@ -10,3 +13,14 @@ RUN apt update && \ sqlite3 iputils-ping util-linux dumb-init && \ pip3 --no-cache-dir install apprise==0.9.7 && \ rm -rf /var/lib/apt/lists/* + +# Install cloudflared +# dpkg --add-architecture arm: cloudflared do not provide armhf, this is workaround. Read more: https://github.com/cloudflare/cloudflared/issues/583 +COPY extra/download-cloudflared.js ./extra/download-cloudflared.js +RUN node ./extra/download-cloudflared.js $TARGETPLATFORM && \ + dpkg --add-architecture arm && \ + apt update && \ + apt --yes --no-install-recommends install ./cloudflared.deb && \ + rm -rf /var/lib/apt/lists/* && \ + rm -f cloudflared.deb + diff --git a/extra/download-cloudflared.js b/extra/download-cloudflared.js new file mode 100644 index 00000000..41519b7c --- /dev/null +++ b/extra/download-cloudflared.js @@ -0,0 +1,44 @@ +// + +const http = require("https"); // or 'https' for https:// URLs +const fs = require("fs"); + +const platform = process.argv[2]; + +if (!platform) { + console.error("No platform??"); + process.exit(1); +} + +let arch = null; + +if (platform === "linux/amd64") { + arch = "amd64"; +} else if (platform === "linux/arm64") { + arch = "arm64"; +} else if (platform === "linux/arm/v7") { + arch = "arm"; +} else { + console.error("Invalid platform?? " + platform); +} + +const file = fs.createWriteStream("cloudflared.deb"); +get("https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-" + arch + ".deb"); + +function get(url) { + http.get(url, function (res) { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + console.log("Redirect to " + res.headers.location); + get(res.headers.location); + } else if (res.statusCode >= 200 && res.statusCode < 300) { + res.pipe(file); + + res.on("end", function () { + console.log("Downloaded"); + }); + } else { + console.error(res.statusCode); + process.exit(1); + } + }); +} diff --git a/package-lock.json b/package-lock.json index 1d30ce07..e4fa75ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "uptime-kuma", - "version": "1.12.1", + "version": "1.13.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "uptime-kuma", - "version": "1.12.1", + "version": "1.13.1", "license": "MIT", "dependencies": { "@fortawesome/fontawesome-svg-core": "~1.2.36", @@ -36,6 +36,7 @@ "jsonwebtoken": "~8.5.1", "jwt-decode": "^3.1.2", "limiter": "^2.1.0", + "node-cloudflared-tunnel": "~1.0.9", "nodemailer": "~6.6.5", "notp": "~2.0.3", "password-hash": "~1.2.2", @@ -11160,6 +11161,14 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==" }, + "node_modules/node-cloudflared-tunnel": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/node-cloudflared-tunnel/-/node-cloudflared-tunnel-1.0.9.tgz", + "integrity": "sha512-d0mhIM5P2ldE2yHChehC6EvnpFCkifWRzWrW81gVWdcCWqNcyISXuDdOYzRW5mwmjWuT6WNtLJoGQ84uqS4EmA==", + "dependencies": { + "command-exists": "^1.2.9" + } + }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -24071,6 +24080,14 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==" }, + "node-cloudflared-tunnel": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/node-cloudflared-tunnel/-/node-cloudflared-tunnel-1.0.9.tgz", + "integrity": "sha512-d0mhIM5P2ldE2yHChehC6EvnpFCkifWRzWrW81gVWdcCWqNcyISXuDdOYzRW5mwmjWuT6WNtLJoGQ84uqS4EmA==", + "requires": { + "command-exists": "^1.2.9" + } + }, "node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", diff --git a/package.json b/package.json index 134271c0..3857aaac 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "jsonwebtoken": "~8.5.1", "jwt-decode": "^3.1.2", "limiter": "^2.1.0", + "node-cloudflared-tunnel": "~1.0.9", "nodemailer": "~6.6.5", "notp": "~2.0.3", "password-hash": "~1.2.2", diff --git a/server/server.js b/server/server.js index 345b91c3..a3777c8c 100644 --- a/server/server.js +++ b/server/server.js @@ -91,6 +91,7 @@ const port = parseInt(process.env.UPTIME_KUMA_PORT || process.env.PORT || args.p const sslKey = process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || args["ssl-key"] || undefined; const sslCert = process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || args["ssl-cert"] || undefined; const disableFrameSameOrigin = !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || args["disable-frame-sameorigin"] || false; +const cloudflaredToken = args["cloudflared-token"] || process.env.UPTIME_KUMA_CLOUDFLARED_TOKEN || undefined; // 2FA / notp verification defaults const twofa_verification_opts = { @@ -133,6 +134,7 @@ const { statusPageSocketHandler } = require("./socket-handlers/status-page-socke const databaseSocketHandler = require("./socket-handlers/database-socket-handler"); const TwoFA = require("./2fa"); const StatusPage = require("./model/status_page"); +const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart } = require("./socket-handlers/cloudflared-socket-handler"); app.use(express.json()); @@ -1362,6 +1364,7 @@ exports.entryPage = "dashboard"; // Status Page Socket Handler for admin only statusPageSocketHandler(socket); + cloudflaredSocketHandler(socket); databaseSocketHandler(socket); debug("added all socket handlers"); @@ -1404,6 +1407,9 @@ exports.entryPage = "dashboard"; initBackgroundJobs(args); + // Start cloudflared at the end if configured + await cloudflaredAutoStart(cloudflaredToken); + })(); async function updateMonitorNotification(monitorID, notificationIDList) { diff --git a/server/socket-handlers/cloudflared-socket-handler.js b/server/socket-handlers/cloudflared-socket-handler.js new file mode 100644 index 00000000..3f4a26e5 --- /dev/null +++ b/server/socket-handlers/cloudflared-socket-handler.js @@ -0,0 +1,84 @@ +const { checkLogin, setSetting, setting, doubleCheckPassword } = require("../util-server"); +const { CloudflaredTunnel } = require("node-cloudflared-tunnel"); +const { io } = require("../server"); + +const prefix = "cloudflared_"; +const cloudflared = new CloudflaredTunnel(); + +cloudflared.change = (running, message) => { + io.to("cloudflared").emit(prefix + "running", running); + io.to("cloudflared").emit(prefix + "message", message); +}; + +cloudflared.error = (errorMessage) => { + io.to("cloudflared").emit(prefix + "errorMessage", errorMessage); +}; + +module.exports.cloudflaredSocketHandler = (socket) => { + + socket.on(prefix + "join", async () => { + try { + checkLogin(socket); + socket.join("cloudflared"); + io.to(socket.userID).emit(prefix + "installed", cloudflared.checkInstalled()); + io.to(socket.userID).emit(prefix + "running", cloudflared.running); + io.to(socket.userID).emit(prefix + "token", await setting("cloudflaredTunnelToken")); + } catch (error) { } + }); + + socket.on(prefix + "leave", async () => { + try { + checkLogin(socket); + socket.leave("cloudflared"); + } catch (error) { } + }); + + socket.on(prefix + "start", async (token) => { + try { + checkLogin(socket); + if (token && typeof token === "string") { + cloudflared.token = token; + } else { + cloudflared.token = null; + } + cloudflared.start(); + } catch (error) { } + }); + + socket.on(prefix + "stop", async (currentPassword, callback) => { + try { + checkLogin(socket); + await doubleCheckPassword(socket, currentPassword); + cloudflared.stop(); + } catch (error) { + callback({ + ok: false, + msg: error.message, + }); + } + }); + + socket.on(prefix + "removeToken", async () => { + try { + checkLogin(socket); + await setSetting("cloudflaredTunnelToken", ""); + } catch (error) { } + }); + +}; + +module.exports.autoStart = async (token) => { + if (!token) { + token = await setting("cloudflaredTunnelToken"); + } else { + // Override the current token via args or env var + await setSetting("cloudflaredTunnelToken", token); + console.log("Use cloudflared token from args or env var"); + } + + if (token) { + console.log("Start cloudflared"); + cloudflared.token = token; + cloudflared.start(); + } +}; diff --git a/src/components/settings/ReverseProxy.vue b/src/components/settings/ReverseProxy.vue new file mode 100644 index 00000000..d35d5353 --- /dev/null +++ b/src/components/settings/ReverseProxy.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/src/mixins/socket.js b/src/mixins/socket.js index 7d1bbea5..d8b1ad22 100644 --- a/src/mixins/socket.js +++ b/src/mixins/socket.js @@ -42,6 +42,14 @@ export default { statusPageList: [], connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...", showReverseProxyGuide: true, + cloudflared: { + cloudflareTunnelToken: "", + installed: null, + running: false, + message: "", + errorMessage: "", + currentPassword: "", + } }; }, @@ -231,6 +239,12 @@ export default { this.socket.firstConnect = false; }); + // cloudflared + socket.on("cloudflared_installed", (res) => this.cloudflared.installed = res); + socket.on("cloudflared_running", (res) => this.cloudflared.running = res); + socket.on("cloudflared_message", (res) => this.cloudflared.message = res); + socket.on("cloudflared_errorMessage", (res) => this.cloudflared.errorMessage = res); + socket.on("cloudflared_token", (res) => this.cloudflared.cloudflareTunnelToken = res); }, storage() { diff --git a/src/pages/Settings.vue b/src/pages/Settings.vue index 5b54e424..bd8ade5a 100644 --- a/src/pages/Settings.vue +++ b/src/pages/Settings.vue @@ -75,6 +75,9 @@ export default { notifications: { title: this.$t("Notifications"), }, + "reverse-proxy": { + title: this.$t("Reverse Proxy"), + }, "monitor-history": { title: this.$t("Monitor History"), }, diff --git a/src/router.js b/src/router.js index f59192d3..7ab75939 100644 --- a/src/router.js +++ b/src/router.js @@ -14,6 +14,7 @@ import Entry from "./pages/Entry.vue"; import Appearance from "./components/settings/Appearance.vue"; import General from "./components/settings/General.vue"; import Notifications from "./components/settings/Notifications.vue"; +import ReverseProxy from "./components/settings/ReverseProxy.vue"; import MonitorHistory from "./components/settings/MonitorHistory.vue"; import Security from "./components/settings/Security.vue"; import Backup from "./components/settings/Backup.vue"; @@ -83,6 +84,10 @@ const routes = [ path: "notifications", component: Notifications, }, + { + path: "reverse-proxy", + component: ReverseProxy, + }, { path: "monitor-history", component: MonitorHistory,