@homebridge-plugins/homebridge-cloudflared-tunnel
Version:
The Cloudflared Tunnel plugin allows you to run a Cloudflare-Tunnel for exposing your homebridge instance for remote access.
290 lines • 13.3 kB
JavaScript
import { readFileSync } from 'node:fs';
import { argv } from 'node:process';
import { startTunnel } from 'untun';
import { CloudflaredTunnel } from './cloudflared-tunnel.js';
/**
* HomebridgePlatform
* This class is the main constructor for your plugin, this is where you should
* parse the user config and discover/register accessories with Homebridge.
*/
export class CloudflaredTunnelPlatform {
accessories;
api;
log;
hap;
config;
platformConfig;
platformLogging;
platformRefreshRate;
platformUpdateRate;
platformPushRate;
debugMode;
version;
constructor(log, config, api) {
this.accessories = [];
this.api = api;
this.hap = this.api.hap;
this.log = log;
// only load if configured
if (!config) {
return;
}
// Plugin options into our config variables.
this.config = {
platform: 'CloudflaredTunnel',
name: config.name,
domain: config.domain,
token: config.token,
url: config.url,
port: config.port,
hostname: config.hostname,
protocol: config.protocol,
verifyTLS: config.verifyTLS,
logging: config.logging,
acceptCloudflareNotice: config.acceptCloudflareNotice,
};
// Plugin Configuration
this.getPlatformLogSettings();
this.getPlatformRateSettings();
this.getPlatformConfigSettings();
this.getVersion();
// Finish initializing the platform
this.debugLog(`Finished initializing platform: ${config.name}`);
// verify the config
(async () => {
try {
await this.verifyConfig();
await this.debugLog('Config OK');
}
catch (e) {
this.errorLog(`Verify Config, Error Message: ${e.message}, Submit Bugs Here: https://bit.ly/homebridge-cloudflared-tunnel-bug-report`);
this.debugErrorLog(`Verify Config, Error: ${e}`);
}
})();
// When this event is fired it means Homebridge has restored all cached accessories from disk.
// Dynamic Platform plugins should only register new accessories after this event was fired,
// in order to ensure they weren't added to homebridge already. This event can also be used
// to start discovery of new accessories.
this.api.on('didFinishLaunching', async () => {
log.debug('Executed didFinishLaunching callback');
// run the method to discover / register your devices as accessories
try {
if (this.config.domain) {
await this.existingTunnel();
}
else {
await this.createTunnel();
}
}
catch (e) {
this.errorLog(`Failed to Start Tunnel, Error Message: ${JSON.stringify(e.message)}`);
this.debugErrorLog(JSON.stringify(e));
}
});
}
/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to setup event handlers for characteristics and update respective values.
*/
configureAccessory(accessory) {
// this.infoLog(`Loading accessory from cache: ${accessory.displayName}`);
// add the restored accessory to the accessories cache so we can track if it has already been registered
this.accessories.push(accessory);
}
/**
* Verify the config passed to the plugin is valid
*/
async verifyConfig() {
if (!this.config.url && (!this.config.protocol && !this.config.hostname && !this.config.port) && (!this.config.domain && !this.config.token)) {
throw new Error('Missing required config: url or {protocol}://{hostname}:{port}, please check your config.json');
}
if (this.config.url && this.config.hostname) {
throw new Error('Cannot have both url and hostname in config. Please remove one.');
}
if (this.config.domain && !this.config.token) {
throw new Error('Missing required config: token, please check your config.json');
}
if (this.config.token && !this.config.domain) {
this.warnLog('Missing config: domain, this is is needed to display in the logs which domain is being tunneled, please check your config.json');
}
if (!this.config.logging) {
this.config.logging = 'standard';
}
if (!this.config.acceptCloudflareNotice) {
this.config.acceptCloudflareNotice = false;
}
}
async existingTunnel() {
const tunnel = new CloudflaredTunnel();
tunnel.token = this.config.token;
await this.infoLog(`Starting Tunnel with Domain: ${this.config.domain}`);
tunnel.start();
}
async createTunnel() {
await this.debugLog(JSON.stringify(this.config));
// The local server URL to tunnel.
const options = {
url: this.config.url,
protocol: this.config.protocol,
hostname: this.config.hostname,
port: this.config.port,
verifyTLS: this.config.verifyTLS,
acceptCloudflareNotice: this.config.acceptCloudflareNotice,
};
await this.debugWarnLog(`Starting Tunnel with Options: ${JSON.stringify(options)}`);
const autoTunnel = await startTunnel(options);
if (autoTunnel) {
const tunnelURL = await autoTunnel.getURL();
await this.infoLog(`Tunnel URL: ${JSON.stringify(tunnelURL)}`);
}
}
async getPlatformLogSettings() {
this.debugMode = argv.includes('-D') ?? argv.includes('--debug');
this.platformLogging = (this.config.options?.logging === 'debug' || this.config.options?.logging === 'standard'
|| this.config.options?.logging === 'none')
? this.config.options.logging
: this.debugMode ? 'debugMode' : 'standard';
const logging = this.config.options?.logging ? 'Platform Config' : this.debugMode ? 'debugMode' : 'Default';
await this.debugLog(`Using ${logging} Logging: ${this.platformLogging}`);
}
async getPlatformRateSettings() {
// RefreshRate
this.platformRefreshRate = this.config.options?.refreshRate ? this.config.options.refreshRate : undefined;
const refreshRate = this.config.options?.refreshRate ? 'Using Platform Config refreshRate' : 'Platform Config refreshRate Not Set';
await this.debugLog(`${refreshRate}: ${this.platformRefreshRate}`);
// UpdateRate
this.platformUpdateRate = this.config.options?.updateRate ? this.config.options.updateRate : undefined;
const updateRate = this.config.options?.updateRate ? 'Using Platform Config updateRate' : 'Platform Config updateRate Not Set';
await this.debugLog(`${updateRate}: ${this.platformUpdateRate}`);
// PushRate
this.platformPushRate = this.config.options?.pushRate ? this.config.options.pushRate : undefined;
const pushRate = this.config.options?.pushRate ? 'Using Platform Config pushRate' : 'Platform Config pushRate Not Set';
await this.debugLog(`${pushRate}: ${this.platformPushRate}`);
}
async getPlatformConfigSettings() {
if (this.config.options) {
const platformConfig = {
platform: 'CloudflaredTunnel',
};
platformConfig.logging = this.config.options.logging ? this.config.options.logging : undefined;
platformConfig.refreshRate = this.config.options.refreshRate ? this.config.options.refreshRate : undefined;
platformConfig.updateRate = this.config.options.updateRate ? this.config.options.updateRate : undefined;
platformConfig.pushRate = this.config.options.pushRate ? this.config.options.pushRate : undefined;
if (Object.entries(platformConfig).length !== 0) {
await this.debugLog(`Platform Config: ${JSON.stringify(platformConfig)}`);
}
this.platformConfig = platformConfig;
}
}
/**
* Asynchronously retrieves the version of the plugin from the package.json file.
*
* This method reads the package.json file located in the parent directory,
* parses its content to extract the version, and logs the version using the debug logger.
* The extracted version is then assigned to the `version` property of the class.
*
* @returns {Promise<void>} A promise that resolves when the version has been retrieved and logged.
*/
async getVersion() {
const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
this.debugLog(`Plugin Version: ${version}`);
this.version = version;
}
/**
* Validate and clean a string value for a Name Characteristic.
* @param displayName - The display name of the accessory.
* @param name - The name of the characteristic.
* @param value - The value to be validated and cleaned.
* @returns The cleaned string value.
*/
async validateAndCleanDisplayName(displayName, name, value) {
if (this.config.options?.allowInvalidCharacters) {
return value;
}
else {
const validPattern = /^[\p{L}\p{N}][\p{L}\p{N} ']*[\p{L}\p{N}]$/u;
const invalidCharsPattern = /[^\p{L}\p{N} ']/gu;
const invalidStartEndPattern = /^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu;
if (typeof value === 'string' && !validPattern.test(value)) {
this.warnLog(`WARNING: The accessory '${displayName}' has an invalid '${name}' characteristic ('${value}'). Please use only alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent the accessory from being added in the Home App or cause unresponsiveness.`);
// Remove invalid characters
if (invalidCharsPattern.test(value)) {
const before = value;
this.warnLog(`Removing invalid characters from '${name}' characteristic, if you feel this is incorrect, please enable \'allowInvalidCharacter\' in the config to allow all characters`);
value = value.replace(invalidCharsPattern, '');
this.warnLog(`${name} Before: '${before}' After: '${value}'`);
}
// Ensure it starts and ends with an alphanumeric character
if (invalidStartEndPattern.test(value)) {
const before = value;
this.warnLog(`Removing invalid starting or ending characters from '${name}' characteristic, if you feel this is incorrect, please enable \'allowInvalidCharacter\' in the config to allow all characters`);
value = value.replace(invalidStartEndPattern, '');
this.warnLog(`${name} Before: '${before}' After: '${value}'`);
}
}
return value;
}
}
/**
* If device level logging is turned on, log to log.warn
* Otherwise send debug logs to log.debug
*/
async infoLog(...log) {
if (await this.enablingPlatformLogging()) {
this.log.info(String(...log));
}
}
async successLog(...log) {
if (await this.enablingPlatformLogging()) {
this.log.success(String(...log));
}
}
async debugSuccessLog(...log) {
if (await this.enablingPlatformLogging()) {
if (await this.loggingIsDebug()) {
this.log.success('[DEBUG]', String(...log));
}
}
}
async warnLog(...log) {
if (await this.enablingPlatformLogging()) {
this.log.warn(String(...log));
}
}
async debugWarnLog(...log) {
if (await this.enablingPlatformLogging()) {
if (await this.loggingIsDebug()) {
this.log.warn('[DEBUG]', String(...log));
}
}
}
async errorLog(...log) {
if (await this.enablingPlatformLogging()) {
this.log.error(String(...log));
}
}
async debugErrorLog(...log) {
if (await this.enablingPlatformLogging()) {
if (await this.loggingIsDebug()) {
this.log.error('[DEBUG]', String(...log));
}
}
}
async debugLog(...log) {
if (await this.enablingPlatformLogging()) {
if (this.platformLogging === 'debugMode') {
this.log.debug(String(...log));
}
else if (this.platformLogging === 'debug') {
this.log.info('[DEBUG]', String(...log));
}
}
}
async loggingIsDebug() {
return this.platformLogging === 'debugMode' || this.platformLogging === 'debug';
}
async enablingPlatformLogging() {
return this.platformLogging === 'debugMode' || this.platformLogging === 'debug' || this.platformLogging === 'standard';
}
}
//# sourceMappingURL=platform.js.map