Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Built-in ease-to-use reverse proxy with Cloudflare Tunnel #1427

Merged
merged 7 commits into from
Mar 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docker/debian-base.dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
# 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 && \
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
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

44 changes: 44 additions & 0 deletions extra/download-cloudflared.js
Original file line number Diff line number Diff line change
@@ -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);
}
});
}
21 changes: 19 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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());

Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -1404,6 +1407,9 @@ exports.entryPage = "dashboard";

initBackgroundJobs(args);

// Start cloudflared at the end if configured
await cloudflaredAutoStart(cloudflaredToken);

})();

async function updateMonitorNotification(monitorID, notificationIDList) {
Expand Down
84 changes: 84 additions & 0 deletions server/socket-handlers/cloudflared-socket-handler.js
Original file line number Diff line number Diff line change
@@ -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();
}
};
139 changes: 139 additions & 0 deletions src/components/settings/ReverseProxy.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<template>
<div>
<h4 class="mt-4">Cloudflare Tunnel</h4>

<div class="my-3">
<div>
cloudflared:
<span v-if="installed === true" class="text-primary">{{ $t("Installed") }}</span>
<span v-else-if="installed === false" class="text-danger">{{ $t("Not installed") }}</span>
</div>

<div>
{{ $t("Status") }}:
<span v-if="running" class="text-primary">{{ $t("Running") }}</span>
<span v-else-if="!running" class="text-danger">{{ $t("Not running") }}</span>
</div>

<div v-if="false">
{{ message }}
</div>

<div v-if="errorMessage" class="mt-3">
Message:
<textarea v-model="errorMessage" class="form-control" readonly></textarea>
</div>

<p v-if="installed === false">(Download cloudflared from <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation/">Cloudflare Website</a>)</p>
</div>

<!-- If installed show token input -->
<div v-if="installed" class="mb-2">
<div class="mb-4">
<label class="form-label" for="cloudflareTunnelToken">
Cloudflare Tunnel {{ $t("Token") }}
</label>
<HiddenInput
id="cloudflareTunnelToken"
v-model="cloudflareTunnelToken"
autocomplete="one-time-code"
:readonly="running"
/>
<div class="form-text">
<div v-if="cloudflareTunnelToken" class="mb-3">
<span v-if="!running" class="remove-token" @click="removeToken">{{ $t("Remove Token") }}</span>
</div>

Don't know how to get the token? Please read the guide:<br />
<a href="https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy-with-Cloudflare-Tunnel" target="_blank">
https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy-with-Cloudflare-Tunnel
</a>
</div>
</div>

<div>
<button v-if="!running" class="btn btn-primary" type="submit" @click="start">
{{ $t("Start") }} cloudflared
</button>

<button v-if="running" class="btn btn-danger" type="submit" @click="$refs.confirmStop.show();">
{{ $t("Stop") }} cloudflared
</button>

<Confirm ref="confirmStop" btn-style="btn-danger" :yes-text="$t('Stop') + ' cloudflared'" :no-text="$t('Cancel')" @yes="stop">
The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.

<div class="mt-3">
<label for="current-password2" class="form-label">
{{ $t("Current Password") }}
</label>
<input
id="current-password2"
v-model="currentPassword"
type="password"
class="form-control"
required
/>
</div>
</Confirm>
</div>
</div>

<h4 class="mt-4">Other Software</h4>
<div>
For example: nginx, Apache and Traefik. <br />
Please read <a href="https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy" target="_blank">https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy</a>.
</div>
</div>
</template>

<script>
import HiddenInput from "../../components/HiddenInput.vue";
import Confirm from "../Confirm.vue";

const prefix = "cloudflared_";

export default {
components: {
HiddenInput,
Confirm
},
data() {
// See /src/mixins/socket.js
return this.$root.cloudflared;
},
computed: {

},
watch: {

},
created() {
this.$root.getSocket().emit(prefix + "join");
},
unmounted() {
this.$root.getSocket().emit(prefix + "leave");
},
methods: {
start() {
this.$root.getSocket().emit(prefix + "start", this.cloudflareTunnelToken);
},
stop() {
this.$root.getSocket().emit(prefix + "stop", this.currentPassword, (res) => {
this.$root.toastRes(res);
});
},
removeToken() {
this.$root.getSocket().emit(prefix + "removeToken");
this.cloudflareTunnelToken = "";
}
}
};
</script>

<style lang="scss" scoped>
.remove-token {
text-decoration: underline;
cursor: pointer;
}
</style>
Loading