UNPKG

@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
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