cloud: add cloudflare tunnel token option

This commit is contained in:
Koushik Dutta
2023-08-17 10:19:09 -07:00
parent fbbbdd8ab5
commit 4b62bceede
4 changed files with 119 additions and 24 deletions

View File

@@ -3,8 +3,33 @@
1. Log into Scrypted Cloud using the login button.
2. This Scrypted server is now available at https://home.scrypted.app.
See below for additional recommendations.
## Optional but Recommended
## Port Forwarding
1. Set up Port Forwarding with UPNP or Router Forwarding.
2. Use the Advanced Tab to verify Port Forwarding is correctly configured.
1. Open the Firewall and Port Forwarding Settings on the network's router.
2. Use the ports shown in Settings to configure a Port Forwarding rule on the router.
Use the `Test Port Forward` buttin in `Advanced` Settings tab to verify the configuration is correct.
## Custom Domains
Custom Domains can be used with the Cloud Plugin.
Set up a reverse proxy to the https Forward Port shown in settings.
## Cloudflare Tunnels
Scrypted Cloud automatically creates a login free tunnel for remote access.
The following steps are only necessary if you want to associate the tunnel with your existing Cloudflare account to manage it remotely.
1. Create the Tunnel in the [Cloudflare Zero Trust Dashboard](https://one.dash.cloudflare.com).
2. Copy the token shown for the tunnel shown in the `install [token]` command. E.g. `cloudflared service install eyJhI344aA...`.
3. Paste the token into the Cloud Plugin Advanced Settings.
4. Add a `Public Hostname` to the tunnel.
* Choose a (sub)domain.
* Service `Type` is `HTTPS` and `URL` is `localhost:port`. Replace the port with `Forward Port` from Cloud Plugin Settings.
5. Reload Cloud Plugin.
6. Verify Cloudflare successfully connected by observing the `Console` Logs.

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/cloud",
"version": "0.1.30",
"version": "0.1.32",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/cloud",
"version": "0.1.30",
"version": "0.1.32",
"dependencies": {
"@eneris/push-receiver": "^3.1.4",
"@scrypted/common": "file:../../common",

View File

@@ -54,5 +54,5 @@
"@types/nat-upnp": "^1.1.2",
"@types/node": "^20.4.5"
},
"version": "0.1.30"
"version": "0.1.32"
}

View File

@@ -21,6 +21,7 @@ import * as cloudflared from 'cloudflared';
import fs, { mkdirSync } from 'fs';
import { backOff } from "exponential-backoff";
import ip from 'ip';
import { Deferred } from "@scrypted/common/src/deferred";
// import { registerDuckDns } from "./greenlock";
@@ -51,6 +52,7 @@ class ScryptedPush extends ScryptedDeviceBase implements BufferConverter {
class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings, BufferConverter, DeviceProvider, HttpRequestHandler {
cloudflareTunnel: string;
cloudflared: Awaited<ReturnType<typeof cloudflared.tunnel>>;
manager = new PushManager(DEFAULT_SENDER_ID);
server: http.Server;
secureServer: https.Server;
@@ -123,7 +125,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
},
securePort: {
title: 'Forward Port',
description: 'The internal network port used by the Scrypted Cloud plugin. The router must forward connections on the From Port using UPNP or port forwarding to this port.',
description: 'The internal https port used by the Scrypted Cloud plugin. The router must forward connections to this port number on this server\'s internal IP address.',
type: 'number',
onPut: (ov, nv) => {
if (ov && ov !== nv)
@@ -163,6 +165,14 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
onPut: () => this.testPortForward(),
description: 'Test the port forward connection from Scrypted Cloud.',
},
cloudflaredTunnelToken: {
group: 'Advanced',
title: 'Cloudflare Tunnel Token',
description: 'Optional: Enter the Cloudflare token from the Cloudflare Dashbaord to track and manage the tunnel remotely.',
onPut: () => {
this.cloudflared?.child.kill();
},
}
});
upnpInterval: NodeJS.Timeout;
upnpClient = upnp.createClient();
@@ -779,10 +789,14 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
}
});
this.startCloudflared();
}
backOff(async () => {
while (true) {
try {
async startCloudflared() {
while (true) {
try {
this.console.log('starting cloudflared');
this.cloudflared = await backOff(async () => {
const pluginVolume = process.env.SCRYPTED_PLUGIN_VOLUME;
const cloudflareD = path.join(pluginVolume, 'cloudflare.d');
mkdirSync(cloudflareD, {
@@ -792,22 +806,78 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
if (!fs.existsSync(cloudflared.bin))
await cloudflared.install(cloudflared.bin);
const insecureUrl = `http://127.0.0.1:${port}`;
const cloudflareTunnel = cloudflared.tunnel({
'--url': insecureUrl,
const secureUrl = `https://127.0.0.1:${this.securePort}`;
const args: any = {};
if (this.storageSettings.values.cloudflaredTunnelToken) {
args['run'] = null;
args['--token'] = this.storageSettings.values.cloudflaredTunnelToken;
}
else {
args['--no-tls-verify'] = null;
args['--url'] = secureUrl;
}
const deferred = new Deferred<string>();
const cloudflareTunnel = cloudflared.tunnel(args);
cloudflareTunnel.child.stdout.on('data', data => this.console.log(data.toString()));
cloudflareTunnel.child.stderr.on('data', data => {
const string: string = data.toString();
this.console.error(string);
const lines = string.split('\n');
for (const line of lines) {
if (line.includes('hostname'))
this.console.log(line);
const config = line.split(' ').find(part => part.startsWith('config='));
if (config) {
const [, json] = config.split('config=');
this.console.log(json);
try {
// the config is already json stringified and needs to be double parsed.
// "{\"ingress\":[{\"hostname\":\"tunnel.example.com\",\"originRequest\":{\"noTLSVerify\":true},\"service\":\"https://localhost:52960\"},{\"service\":\"http_status:404\"}],\"warp-routing\":{\"enabled\":false}}"
const parsed = JSON.parse(JSON.parse(json));
const hostname = parsed.ingress?.[0]?.hostname;
if (!hostname)
deferred.resolve(undefined)
else
deferred.resolve(`https://${hostname}`)
}
catch (e) {
this.console.error("Error parsing config", e);
}
}
}
});
this.cloudflareTunnel = await cloudflareTunnel.url;
this.console.log(`cloudflare url mapped ${this.cloudflareTunnel} to ${insecureUrl}`);
await once(cloudflareTunnel.child, 'exit');
throw new Error('cloudflared exited.');
}
catch (e) {
this.console.error('cloudlfared failed', e);
this.cloudflareTunnel = undefined;
throw e;
}
cloudflareTunnel.child.on('exit', () => deferred.resolve(undefined));
try {
this.cloudflareTunnel = await Promise.any([deferred.promise, cloudflareTunnel.url]);
if (!this.cloudflareTunnel)
throw new Error('cloudflared exited, the provided cloudflare tunnel token may be invalid.')
}
catch (e) {
this.console.error('cloudflared error', e);
throw e;
}
this.console.log(`cloudflare url mapped ${this.cloudflareTunnel} to ${secureUrl}`);
return cloudflareTunnel;
}, {
startingDelay: 60000,
timeMultiple: 1.2,
numOfAttempts: 1000,
maxDelay: 300000,
});
await once(this.cloudflared.child, 'exit');
throw new Error('cloudflared exited.');
}
});
catch (e) {
this.console.error('cloudflared error', e);
}
finally {
this.cloudflared = undefined;
this.cloudflareTunnel = undefined;
}
}
}
ensureReverseConnections(registrationId: string) {