@smj0x/homebridge-shortcuts-buttons
Version:
Run any Apple Shortcut with just the tap of a button, and execute a custom unix command (or another shortcut!) after completion to handle its success/failure, using x-callback-url.
122 lines • 5.06 kB
JavaScript
import { createServer } from 'http';
import { HSBXCallbackUrlServerCommand } from './command.js';
import { HSBXCallbackUrlRequiredSearchParamsKeys, HSBXCallbackUrlSearchParams } from './params.js';
import { PLATFORM_NAME } from '../settings.js';
import { createRequestValidators } from './validators.js';
export class HSBXCallbackUrlServer {
config;
log;
utils;
api;
pathname = '/x-callback-url';
tokens = new Set();
proto;
hostname;
port;
server;
constructor(config, log, utils, api) {
this.config = config;
this.log = log;
this.utils = utils;
this.api = api;
this.proto = config.callbackServerProtocol;
this.hostname = config.callbackServerHostname;
this.port = config.callbackServerPort;
this.server = this.create();
api.on('shutdown', this.destroy.bind(this));
}
get baseUrl() {
return `${this.proto}://${this.hostname}:${this.port}${this.pathname}`;
}
issueToken() {
const token = this.api.hap.uuid.generate(`${HSBXCallbackUrlServer.name}_${Date.now().toString()}`);
this.tokens.add(token);
return token;
}
isValidToken(token) {
return typeof token === 'string' && this.tokens.delete(token);
}
areValidSearchParams(params) {
return Object.values(HSBXCallbackUrlRequiredSearchParamsKeys).every((key) => params[key] !== null);
}
create() {
if (this.config.callbackServerEnabled !== true) {
this.log.error('Server::create Attemped to create server when waitForShortcutResult is off');
return null;
}
const server = createServer({ requestTimeout: 30000 });
server.listen(this.port, this.hostname, () => {
this.log.info(`XCallbackUrlServer listening at ${this.hostname}:${this.port}`);
});
server.on('request', this.requestListener.bind(this));
server.on('error', (error) => {
this.log.error('XCallbackUrlServer::on(error)', error);
});
return server;
}
destroy() {
this.log.debug('XCallbackUrlServer::destroy', 'Closing server connections');
this.server?.closeAllConnections();
this.log.debug('XCallbackUrlServer::destroy', 'Removing server listeners');
this.server?.removeAllListeners();
}
async requestListener(req, res) {
this.log.debug('XCallbackUrlServer::requestListener', 'Incoming request, starting validation');
const url = new URL(req.url || '', `${this.proto}://${req.headers.host}`);
const searchParams = new HSBXCallbackUrlSearchParams(url.searchParams);
const requestValidators = createRequestValidators({
hasValidMethod: {
condition: () => req.method === 'GET',
errorMessage: `Unsupported request: ${req.method} ${req.url}`,
errorCode: 405,
},
hasValidPathname: {
condition: () => url.pathname === this.pathname,
errorMessage: `Invalid url pathname: ${url.pathname}`,
errorCode: 404,
},
hasValidSearchParams: {
condition: () => this.areValidSearchParams(searchParams),
errorMessage: `Missing required search params (${JSON.stringify(searchParams)})`,
errorCode: 400,
},
hasValidAuthToken: {
condition: () => this.isValidToken(searchParams.token),
errorMessage: 'Authorization token invalid or already consumed',
errorCode: 403,
},
});
for (const validator of requestValidators) {
if (!validator.test()) {
return this.endWithError(res, validator.errorCode, validator.errorMessage);
}
}
this.log.debug('XCallbackUrlServer::requestListener Request validators passed');
try {
await new HSBXCallbackUrlServerCommand(searchParams, this.config, this.utils).run();
}
catch (e) {
return this.endWithError(res, 500, 'Failed to run callback command', e);
}
this.log.success('XCallbackUrlServer::requestListener', `Executed callback command for shortcut "${searchParams.shortcut}"`);
return this.endWithStatusAndHtml(res, 200);
}
endWithError(res, statusCode, errorMessage, errorPayload) {
this.log.error(`XCallbackUrlServer::requestListener StatusCode=${statusCode}`, errorMessage, errorPayload);
this.endWithStatusAndHtml(res, statusCode);
}
endWithStatusAndHtml(res, statusCode) {
res.writeHead(statusCode);
res.write(CALLBACK_HTML_CONTENT);
res.end();
}
}
const CALLBACK_HTML_CONTENT = `<!DOCTYPE html>
<html class="default" lang="en">
<head>
<meta charset="utf-8">
<title>${PLATFORM_NAME} - X-Callback-Url Server</title>
<script>typeof window !== "undefined" && window.close()</script>
</head>
</html>`;
//# sourceMappingURL=index.js.map