mirror of
https://github.com/koush/scrypted.git
synced 2026-06-20 16:40:30 +01:00
cloud: add cloudflare tunnel token option
This commit is contained in:
@@ -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.
|
||||
4
plugins/cloud/package-lock.json
generated
4
plugins/cloud/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
"@types/nat-upnp": "^1.1.2",
|
||||
"@types/node": "^20.4.5"
|
||||
},
|
||||
"version": "0.1.30"
|
||||
"version": "0.1.32"
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user