import { Level } from './level'; import { PluginHost } from './plugin/plugin-host'; import cluster from 'cluster'; import { ScryptedNativeId, Device, EngineIOHandler, HttpRequest, HttpRequestHandler, OauthClient, PushHandler, ScryptedDevice, ScryptedInterface, ScryptedInterfaceProperty } from '@scrypted/sdk/types'; import { PluginDeviceProxyHandler } from './plugin/plugin-device'; import { Plugin, PluginDevice, ScryptedAlert } from './db-types'; import { getState, ScryptedStateManager, setState } from './state'; import { Request, Response, Router } from 'express'; import { createResponseInterface } from './http-interfaces'; import bodyParser from 'body-parser'; import http, { ServerResponse } from 'http'; import https from 'https'; import express from 'express'; import { LogEntry, Logger, makeAlertId } from './logger'; import { getDisplayName, getDisplayRoom, getDisplayType, getProvidedNameOrDefault, getProvidedRoomOrDefault, getProvidedTypeOrDefault } from './infer-defaults'; import { URL } from "url"; import qs from "query-string"; import { PluginComponent } from './services/plugin'; import { Server as WebSocketServer } from "ws"; import axios from 'axios'; import tar from 'tar'; import { once } from 'events'; import { PassThrough } from 'stream'; import { PluginDebug } from './plugin/plugin-debug'; import { getIpAddress, SCRYPTED_INSECURE_PORT, SCRYPTED_SECURE_PORT } from './server-settings'; import semver from 'semver'; import { ServiceControl } from './services/service-control'; import { Alerts } from './services/alerts'; import { Info } from './services/info'; import io from 'engine.io'; import {spawn as ptySpawn} from 'node-pty'; interface DeviceProxyPair { handler: PluginDeviceProxyHandler; proxy: ScryptedDevice; } const MIN_SCRYPTED_CORE_VERSION = 'v0.0.146'; const PLUGIN_DEVICE_STATE_VERSION = 2; export class ScryptedRuntime { datastore: Level; plugins: { [id: string]: PluginHost } = {}; pluginDevices: { [id: string]: PluginDevice } = {}; devices: { [id: string]: DeviceProxyPair } = {}; stateManager = new ScryptedStateManager(this); app: Router; logger = new Logger(this, '', 'Scrypted'); devicesLogger = this.logger.getLogger('device', 'Devices'); wss = new WebSocketServer({ noServer: true }); wsAtomic = 0; shellio = io(undefined, { pingTimeout: 120000, }); constructor(datastore: Level, insecure: http.Server, secure: https.Server, app: express.Application) { this.datastore = datastore; this.app = app; app.disable('x-powered-by'); app.all(['/endpoint/@:owner/:pkg/public/engine.io/*', '/endpoint/:pkg/public/engine.io/*'], (req, res) => { this.endpointHandler(req, res, true, true, this.handleEngineIOEndpoint.bind(this)) }); app.all(['/endpoint/@:owner/:pkg/engine.io/*', '/endpoint/@:owner/:pkg/engine.io/*'], (req, res) => { this.endpointHandler(req, res, false, true, this.handleEngineIOEndpoint.bind(this)) }); // stringify all http endpoints app.all(['/endpoint/@:owner/:pkg/public', '/endpoint/@:owner/:pkg/public/*', '/endpoint/:pkg', '/endpoint/:pkg/*'], bodyParser.text() as any); app.all(['/endpoint/@:owner/:pkg/public', '/endpoint/@:owner/:pkg/public/*', '/endpoint/:pkg/public', '/endpoint/:pkg/public/*'], (req, res) => { this.endpointHandler(req, res, true, false, this.handleRequestEndpoint.bind(this)) }); app.all(['/endpoint/@:owner/:pkg', '/endpoint/@:owner/:pkg/*', '/endpoint/:pkg', '/endpoint/:pkg/*'], (req, res) => { this.endpointHandler(req, res, false, false, this.handleRequestEndpoint.bind(this)) }); app.get('/web/oauth/callback', (req, res) => { this.oauthCallback(req, res); }); app.all('/engine.io/shell', (req, res) => { this.shellHandler(req, res); }); this.shellio.on('connection', connection => { const cp = ptySpawn(process.env.SHELL, [], { }); cp.onData(data => connection.send(data)); connection.on('message', message => cp.write(message.toString())); connection.on('close', () => cp.kill()); }) insecure.on('upgrade', (req, socket, upgradeHead) => { (req as any).upgradeHead = upgradeHead; (app as any).handle(req, { socket, upgradeHead }) }) secure.on('upgrade', (req, socket, upgradeHead) => { (req as any).upgradeHead = upgradeHead; (app as any).handle(req, { socket, upgradeHead }) }) this.logger.on('log', (logEntry: LogEntry) => { if (logEntry.level !== 'a') return; console.log('alert', logEntry); const alert = new ScryptedAlert(); alert._id = makeAlertId(logEntry.path, logEntry.message); alert.message = logEntry.message; alert.timestamp = logEntry.timestamp; alert.path = logEntry.path; alert.title = logEntry.title; datastore.upsert(alert); this.stateManager.notifyInterfaceEvent(null, 'Logger' as any, logEntry); }); // purge logs older than 2 hours every hour setInterval(() => { this.logger.purge(Date.now() - 48 * 60 * 60 * 1000); }, 60 * 60 * 1000); } getDeviceLogger(device: PluginDevice): Logger { return this.devicesLogger.getLogger(device._id, getState(device, ScryptedInterfaceProperty.name)); } async oauthCallback(req: Request, res: Response) { try { const { callback_url } = req.query; if (!callback_url) { const html = "
\n" + " \n" + "\n" + "\n" + "