Skip to content

Commit

Permalink
feat: Secure mqtt with certificates
Browse files Browse the repository at this point in the history
  • Loading branch information
svrooij committed Mar 30, 2024
1 parent 53b22e1 commit fb2e9bd
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 23 deletions.
4 changes: 4 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ services:
- SONOS2MQTT_DEVICE=192.168.50.4 # Service discovery doesn't work very well inside docker, so start with one device.
- SONOS2MQTT_MQTT=mqtt:https://emqx:1883 # EMQX is a nice mqtt broker
# - SONOS2MQTT_DISTINCT=true # if your want distinct topics
# - SONOS2MQTT_MQTT_CA_PATH=/path/to/ca.crt # If you use a self-signed certificate
# - SONOS2MQTT_MQTT_CERT_PATH=/path/to/cert.crt # If you want a secure connection
# - SONOS2MQTT_MQTT_KEY_PATH=/path/to/key.key # If you want a secure connection
# - SONOS2MQTT_MQTT_REJECT_UNAUTHORIZED=true # If you use official signed certificates
- SONOS_LISTENER_HOST=192.168.50.44 # Docker host IP
- SONOS_TTS_ENDPOINT=http:https://sonos-tts:5601/api/generate # If you deployed the TTS with the same docker-compose
depends_on:
Expand Down
41 changes: 35 additions & 6 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ export interface Config {
tvUuid?: string;
tvVolume?: number;
experimental?: boolean;
secure?: SecureConfig;
}

export interface SecureConfig {
key?: string | string[] | Buffer | Buffer[] | any[];
keyPath?: string;
cert?: string | string[] | Buffer | Buffer[];
certPath?: string;
ca?: string | string[] | Buffer | Buffer[];
caPaths?: string | string[];
rejectUnauthorized?: boolean;
}

const defaultConfig: Config = {
Expand All @@ -34,9 +45,10 @@ const defaultConfig: Config = {
}

export class ConfigLoader {
static LoadConfig(): Config {
const config = {...defaultConfig, ...(ConfigLoader.LoadConfigFromFile() ?? ConfigLoader.LoadConfigFromArguments())};

static async LoadConfig(): Promise<Config> {
const extraConfig = ConfigLoader.LoadConfigFromFile() ?? await ConfigLoader.LoadConfigFromArguments();
const config = {...defaultConfig, ...extraConfig};

if (config.ttsendpoint !== undefined && process.env.SONOS_TTS_ENDPOINT === undefined) {
process.env.SONOS_TTS_ENDPOINT = config.ttsendpoint
}
Expand All @@ -60,12 +72,18 @@ export class ConfigLoader {
return;
}

private static LoadConfigFromArguments(): Partial<Config> {
private static async LoadConfigFromArguments(): Promise<Partial<Config>> {
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json')).toString())
return yargs
const config = await yargs
.usage(pkg.name + ' ' + pkg.version + '\n' + pkg.description + '\n\nUsage: $0 [options]')
.describe('prefix', 'instance name. used as prefix for all topics')
.describe('mqtt', 'mqtt broker url. See https://sonos2mqtt.svrooij.io/getting-started.html#configuration')
.describe('mqtt_cert_path', 'Path to the certificate file for secure mqtt connections.')

.describe('mqtt_key_path', 'Path to the key file for secure mqtt connections.')
.describe('mqtt_ca_path', 'Path to the ca file for secure mqtt connections.')
.describe('mqtt_reject_unauthorized', 'Reject unauthorized connections.')
.boolean('mqtt_reject_unauthorized')
.describe('clientid', 'Specify the client id to be used')
.describe('wait', 'Number of seconds to search for speakers')
.describe('log', 'Set the loglevel')
Expand Down Expand Up @@ -103,6 +121,17 @@ export class ConfigLoader {
.version()
.help('help')
.env('SONOS2MQTT')
.argv as Partial<Config>
.argv;


if (config.mqtt_cert_path !== undefined || config.mqtt_key_path !== undefined || config.mqtt_ca_path !== undefined || config.mqtt_reject_unauthorized === true) {
config.secure = {
keyPath: config.mqtt_key_path,
certPath: config.mqtt_cert_path,
caPaths: config.mqtt_ca_path,
rejectUnauthorized: config.mqtt_reject_unauthorized
}
}
return config as Partial<Config>;
}
}
28 changes: 15 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,26 @@ import {SonosToMqtt} from './sonos-to-mqtt'
import {ConfigLoader} from './config'
import { StaticLogger } from './static-logger'

const sonosToMqtt = new SonosToMqtt(ConfigLoader.LoadConfig())
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json')).toString())
StaticLogger.Default().info(`Starting ${pkg.name} v${pkg.version}`)

const stop = function () {
StaticLogger.Default().info('Shutdown sonos2mqtt, please wait.')
sonosToMqtt.stop()
setTimeout(() => { process.exit(0) }, 800)
async function main() {
const sonosToMqtt = new SonosToMqtt(await ConfigLoader.LoadConfig())
const result = await sonosToMqtt.start();
if (!result) {
StaticLogger.Default().fatal('Failed to start sonos2mqtt')
process.exit(1)
}
function stop() {
StaticLogger.Default().info('Shutdown sonos2mqtt, please wait.')
sonosToMqtt.stop()
setTimeout(() => { process.exit(0) }, 800)
}
process.on('SIGINT', stop)
process.on('SIGTERM', stop)
}

sonosToMqtt
.start()
.then(success => {
if(success) {
process.on('SIGINT', () => stop())
process.on('SIGTERM', () => stop())
}
})
main()
.catch(err => {
StaticLogger.Default().fatal(err, 'Error starting sonos2mqtt')
})
Expand Down
13 changes: 11 additions & 2 deletions src/smarthome-mqtt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {EventEmitter} from 'events'
import { DeviceControl } from './device-control'
import {StaticLogger} from './static-logger'
import { AutoDiscoveryMessage } from './ha-discovery';
import { SecureConfig } from './config';

type MqttEvents = {
connected: (connected: boolean) => void;
Expand All @@ -16,8 +17,9 @@ export class SmarthomeMqtt{
private readonly log = StaticLogger.CreateLoggerForSource('sonos2mqtt.SmarthomeMqtt')
private readonly uri: URL
private mqttClient?: MqttClient;
private readonly security?: SecureConfig;
public readonly Events: TypedEmitter<MqttEvents> = new EventEmitter() as TypedEmitter<MqttEvents>;
constructor(mqttUrl: string, private readonly prefix: string = 'sonos', private readonly clientId?: string) {
constructor(mqttUrl: string, private readonly prefix: string = 'sonos', private readonly clientId?: string, security?: SecureConfig) {
this.uri = new URL(mqttUrl)
}

Expand All @@ -30,7 +32,14 @@ export class SmarthomeMqtt{
retain: true
},
keepalive: 60000,
clientId: this.clientId
clientId: this.clientId,
rejectUnauthorized: (this.uri.protocol === 'mqtts' || this.uri.protocol === 'ssl') && this.security?.rejectUnauthorized === true,
ca: this.security?.ca,
key: this.security?.key,
cert: this.security?.cert,
caPaths: this.security?.caPaths,
keyPath: this.security?.keyPath,
certPath: this.security?.certPath,
});
this.mqttClient.on('connect',() => {
this.log.debug('Connected to server {server}', this.uri.host)
Expand Down
2 changes: 1 addition & 1 deletion src/sonos-to-mqtt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class SonosToMqtt {
private readonly _soundbarTrackUri = 'x-sonos-htastream:RINCON_'
constructor(private config: Config) {
this.sonosManager = new SonosManager();
this.mqtt = new SmarthomeMqtt(config.mqtt, config.prefix, config.clientid);
this.mqtt = new SmarthomeMqtt(config.mqtt, config.prefix, config.clientid, config.secure);
}

async start(): Promise<boolean> {
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@

/* Advanced Options */
// "resolveJsonModule": true,
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
"forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
"skipLibCheck": true, /* Skip type checking of declaration files. */
},
"include": ["src"],
"exclude": ["node_modules", "**/__tests__/*"],
Expand Down

0 comments on commit fb2e9bd

Please sign in to comment.