server/various: scaffolding for multi-user and non-admin users

This commit is contained in:
Koushik Dutta
2022-12-17 23:44:13 -08:00
parent 714a36b7d4
commit 4b9082b6df
23 changed files with 273 additions and 150 deletions

View File

@@ -240,9 +240,7 @@ class CastDevice extends ScryptedDeviceBase implements MediaPlayer, Refresh, Eng
reject: any;
};
async onConnection(request: HttpRequest, webSocketUrl: string) {
const ws = new WebSocket(webSocketUrl);
async onConnection(request: HttpRequest, ws: WebSocket) {
ws.onmessage = async (message) => {
const json = JSON.parse(message.data as string);
const { token } = json;

View File

@@ -129,6 +129,9 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
return this.aggregateCore;
}
async releaseDevice(id: string, nativeId: string, device: any): Promise<void> {
}
checkEngineIoEndpoint(request: HttpRequest, name: string) {
const check = `/endpoint/@scrypted/core/engine.io/${name}/`;
if (!request.url.startsWith(check))
@@ -154,9 +157,7 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
return true;
}
async onConnection(request: HttpRequest, webSocketUrl: string): Promise<void> {
const ws = new WebSocket(webSocketUrl);
async onConnection(request: HttpRequest, ws: WebSocket): Promise<void> {
if (await this.checkService(request, ws, 'console') || await this.checkService(request, ws, 'repl')) {
return;
}

View File

@@ -194,9 +194,7 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin
this.throttleSync();
}
async onConnection(request: HttpRequest, webSocketUrl: string) {
const ws = new WebSocket(webSocketUrl);
async onConnection(request: HttpRequest, ws: WebSocket) {
ws.onmessage = async (message) => {
const json = JSON.parse(message.data as string);
const { token } = json;

View File

@@ -4,7 +4,7 @@ import { Deferred } from '@scrypted/common/src/deferred';
import { listenZeroSingleClient } from '@scrypted/common/src/listen-cluster';
import { createBrowserSignalingSession } from "@scrypted/common/src/rtc-connect";
import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from '@scrypted/common/src/settings-mixin';
import sdk, { BufferConverter, BufferConvertorOptions, DeviceCreator, DeviceCreatorSettings, DeviceProvider, FFmpegInput, HttpRequest, Intercom, MediaObject, MixinProvider, RequestMediaStream, RequestMediaStreamOptions, ResponseMediaStreamOptions, RTCSessionControl, RTCSignalingChannel, RTCSignalingClient, RTCSignalingSession, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera } from '@scrypted/sdk';
import sdk, { BufferConverter, BufferConvertorOptions, ConnectOptions, DeviceCreator, DeviceCreatorSettings, DeviceProvider, FFmpegInput, HttpRequest, Intercom, MediaObject, MixinProvider, RequestMediaStream, RequestMediaStreamOptions, ResponseMediaStreamOptions, RTCSessionControl, RTCSignalingChannel, RTCSignalingClient, RTCSignalingSession, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera } from '@scrypted/sdk';
import { StorageSettings } from '@scrypted/sdk/storage-settings';
import crypto from 'crypto';
import net from 'net';
@@ -402,7 +402,7 @@ export class WebRTCPlugin extends AutoenableMixinProvider implements DeviceCreat
const message = await new Promise<{
connectionManagementId: string,
updateSessionId: string,
}>((resolve, reject) => {
} & ConnectOptions>((resolve, reject) => {
const close = () => {
const str = 'Connection closed while waiting for message';
reject(new Error(str));
@@ -416,6 +416,8 @@ export class WebRTCPlugin extends AutoenableMixinProvider implements DeviceCreat
}
});
message.username = request.username;
const { connectionManagementId, updateSessionId } = message;
if (connectionManagementId) {
cleanup.promise.finally(async () => {
@@ -457,7 +459,12 @@ export class WebRTCPlugin extends AutoenableMixinProvider implements DeviceCreat
const cp = await client.clientPromise;
cp.on('close', () => cleanup.resolve('socket client closed'));
process.send(message, cp);
// TODO: remove process.send hack
// 12/16/2022
if (sdk.connect)
sdk.connect(cp, message);
else
process.send(message, cp);
}
catch (e) {
console.error("error negotiating browser RTCC signaling", e);
@@ -492,10 +499,10 @@ export async function fork() {
if (port) {
const socket = net.connect(port, '127.0.0.1');
cleanup.promise.finally(() => socket.destroy());
const dc = pc.createDataChannel('rpc');
dc.message.subscribe(message => socket.write(message));
const debouncer = new DataChannelDebouncer({
send: u8 => dc.send(Buffer.from(u8)),
}, e => {

4
sdk/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/sdk",
"version": "0.2.28",
"version": "0.2.33",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/sdk",
"version": "0.2.28",
"version": "0.2.33",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.16.7",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/sdk",
"version": "0.2.28",
"version": "0.2.33",
"description": "",
"main": "dist/src/index.js",
"exports": {

3
sdk/src/acl.ts Normal file
View File

@@ -0,0 +1,3 @@
export function mergeScryptedAccessControl() {
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/types",
"version": "0.2.25",
"version": "0.2.30",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/types",
"version": "0.2.25",
"version": "0.2.30",
"license": "ISC",
"devDependencies": {
"@types/rimraf": "^3.0.2",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/types",
"version": "0.2.25",
"version": "0.2.30",
"description": "",
"main": "dist/index.js",
"author": "",

View File

@@ -73,7 +73,6 @@ class ScryptedInterface(Enum):
AirQualitySensor = "AirQualitySensor"
AmbientLightSensor = "AmbientLightSensor"
AudioSensor = "AudioSensor"
Authenticator = "Authenticator"
Battery = "Battery"
BinarySensor = "BinarySensor"
Brightness = "Brightness"
@@ -130,9 +129,9 @@ class ScryptedInterface(Enum):
Scriptable = "Scriptable"
ScryptedDevice = "ScryptedDevice"
ScryptedPlugin = "ScryptedPlugin"
ScryptedUser = "ScryptedUser"
SecuritySystem = "SecuritySystem"
Settings = "Settings"
SoftwareUpdate = "SoftwareUpdate"
StartStop = "StartStop"
TamperSensor = "TamperSensor"
TemperatureSetting = "TemperatureSetting"
@@ -235,6 +234,12 @@ class PictureDimensions(TypedDict):
width: float
pass
class ScryptedDeviceAccessControl(TypedDict):
id: str
methods: list[str]
properties: list[str]
pass
class VideoStreamOptions(TypedDict):
bitrate: float
bitrateControl: Any | Any
@@ -559,6 +564,10 @@ class ScriptSource(TypedDict):
script: str
pass
class ScryptedUserAccessControl(TypedDict):
devicesAccessControls: list[ScryptedDeviceAccessControl]
pass
class SecuritySystemState(TypedDict):
mode: SecuritySystemMode
obstruction: SecuritySystemObstruction
@@ -582,6 +591,18 @@ class Setting(TypedDict):
value: SettingValue
pass
class TemperatureCommand(TypedDict):
mode: ThermostatMode
setpoint: float | tuple[float, float]
pass
class TemperatureSettingStatus(TypedDict):
activeMode: ThermostatMode
availableModes: list[ThermostatMode]
mode: ThermostatMode
setpoint: float | tuple[float, float]
pass
class VideoClip(TypedDict):
description: str
detectionClasses: list[str]
@@ -616,11 +637,6 @@ class AudioSensor:
audioDetected: bool
pass
class Authenticator:
async def checkPassword(self, password: str) -> bool:
pass
pass
class Battery:
batteryLevel: float
pass
@@ -697,7 +713,7 @@ class DeviceProvider:
class Display:
async def startDisplay(self, media: MediaObject) -> None:
pass
async def stopDisplay(self, media: MediaObject) -> None:
async def stopDisplay(self) -> None:
pass
pass
@@ -708,7 +724,7 @@ class Dock:
pass
class EngineIOHandler:
async def onConnection(self, request: HttpRequest, webSocketUrl: str) -> None:
async def onConnection(self, request: HttpRequest, webScoket: WebSocket) -> None:
pass
pass
@@ -964,6 +980,11 @@ class ScryptedPlugin:
pass
pass
class ScryptedUser:
async def getScryptedUserAccessControl(self) -> ScryptedUserAccessControl:
pass
pass
class SecuritySystem:
securitySystemState: SecuritySystemState
async def armSecuritySystem(self, mode: SecuritySystemMode) -> None:
@@ -979,14 +1000,6 @@ class Settings:
pass
pass
class SoftwareUpdate:
updateAvailable: bool
async def checkForUpdate(self) -> bool:
pass
async def installUpdate(self) -> None:
pass
pass
class StartStop:
running: bool
async def start(self) -> None:
@@ -1000,12 +1013,15 @@ class TamperSensor:
pass
class TemperatureSetting:
temperatureSetting: TemperatureSettingStatus
thermostatActiveMode: ThermostatMode
thermostatAvailableModes: list[ThermostatMode]
thermostatMode: ThermostatMode
thermostatSetpoint: float
thermostatSetpointHigh: float
thermostatSetpointLow: float
async def setTemperature(self, command: TemperatureCommand) -> None:
pass
async def setThermostatMode(self, mode: ThermostatMode) -> None:
pass
async def setThermostatSetpoint(self, degrees: float) -> None:
@@ -1215,6 +1231,7 @@ class ScryptedInterfaceProperty(Enum):
running = "running"
paused = "paused"
docked = "docked"
temperatureSetting = "temperatureSetting"
thermostatActiveMode = "thermostatActiveMode"
thermostatAvailableModes = "thermostatAvailableModes"
thermostatMode = "thermostatMode"
@@ -1229,7 +1246,6 @@ class ScryptedInterfaceProperty(Enum):
entryOpen = "entryOpen"
batteryLevel = "batteryLevel"
online = "online"
updateAvailable = "updateAvailable"
fromMimeType = "fromMimeType"
toMimeType = "toMimeType"
binaryState = "binaryState"
@@ -1407,6 +1423,13 @@ class DeviceState:
def docked(self, value: bool):
self.setScryptedProperty("docked", value)
@property
def temperatureSetting(self) -> TemperatureSettingStatus:
return self.getScryptedProperty("temperatureSetting")
@temperatureSetting.setter
def temperatureSetting(self, value: TemperatureSettingStatus):
self.setScryptedProperty("temperatureSetting", value)
@property
def thermostatActiveMode(self) -> ThermostatMode:
return self.getScryptedProperty("thermostatActiveMode")
@@ -1505,13 +1528,6 @@ class DeviceState:
def online(self, value: bool):
self.setScryptedProperty("online", value)
@property
def updateAvailable(self) -> bool:
return self.getScryptedProperty("updateAvailable")
@updateAvailable.setter
def updateAvailable(self, value: bool):
self.setScryptedProperty("updateAvailable", value)
@property
def fromMimeType(self) -> str:
return self.getScryptedProperty("fromMimeType")
@@ -1794,12 +1810,14 @@ ScryptedInterfaceDescriptors = {
"TemperatureSetting": {
"name": "TemperatureSetting",
"methods": [
"setTemperature",
"setThermostatMode",
"setThermostatSetpoint",
"setThermostatSetpointHigh",
"setThermostatSetpointLow"
],
"properties": [
"temperatureSetting",
"thermostatActiveMode",
"thermostatAvailableModes",
"thermostatMode",
@@ -1926,13 +1944,6 @@ ScryptedInterfaceDescriptors = {
],
"properties": []
},
"Authenticator": {
"name": "Authenticator",
"methods": [
"checkPassword"
],
"properties": []
},
"Scene": {
"name": "Scene",
"methods": [
@@ -2013,16 +2024,6 @@ ScryptedInterfaceDescriptors = {
"online"
]
},
"SoftwareUpdate": {
"name": "SoftwareUpdate",
"methods": [
"checkForUpdate",
"installUpdate"
],
"properties": [
"updateAvailable"
]
},
"BufferConverter": {
"name": "BufferConverter",
"methods": [
@@ -2285,6 +2286,13 @@ ScryptedInterfaceDescriptors = {
"properties": [
"applicationInfo"
]
},
"ScryptedUser": {
"name": "ScryptedUser",
"methods": [
"getScryptedUserAccessControl"
],
"properties": []
}
}

View File

@@ -31,6 +31,7 @@ for (const name of Object.values(ScryptedInterface)) {
}
const properties = Object.values(ScryptedInterfaceDescriptors).map(d => d.properties).flat();
const methods = Object.values(ScryptedInterfaceDescriptors).map(d => d.methods).flat();
const deviceStateContents = `
export interface DeviceState {
@@ -48,11 +49,18 @@ ${properties.map(property => ' ' + property + ' = \"' + property + '",\n').join
}
`;
const methodContents = `
export enum ScryptedInterfaceMethod {
${methods.map(method => ' ' + method + ' = \"' + method + '",\n').join('')}
}
`;
const contents = `
export const TYPES_VERSION = "${typesVersion}";
${deviceStateContents}
${propertyContents}
${methodContents}
export const ScryptedInterfaceDescriptors: { [scryptedInterface: string]: ScryptedInterfaceDescriptor } = ${stringifyObject(ScryptedInterfaceDescriptors, { indent: ' ' })}

View File

@@ -1,4 +1,5 @@
import type { Worker as NodeWorker } from 'worker_threads';
import type { Socket as NodeNetSocket } from 'net';
export type ScryptedNativeId = string | undefined;
@@ -253,23 +254,77 @@ export interface Dock {
docked?: boolean;
}
export interface TemperatureCommand {
mode?: ThermostatMode;
setpoint?: number | [number, number];
}
export interface TemperatureSettingStatus {
availableModes?: ThermostatMode[];
mode?: ThermostatMode;
activeMode?: ThermostatMode;
setpoint?: number | [number, number];
}
/**
* TemperatureSetting represents a thermostat device.
*/
export interface TemperatureSetting {
temperatureSetting?: TemperatureSettingStatus;
setTemperature(command: TemperatureCommand): Promise<void>;
/**
* @deprecated
* @param mode
*/
setThermostatMode(mode: ThermostatMode): Promise<void>;
/**
* @deprecated
* @param mode
*/
setThermostatSetpoint(degrees: number): Promise<void>;
/**
* @deprecated
* @param mode
*/
setThermostatSetpointHigh(high: number): Promise<void>;
/**
* @deprecated
* @param mode
*/
setThermostatSetpointLow(low: number): Promise<void>;
/**
* @deprecated
* @param mode
*/
thermostatAvailableModes?: ThermostatMode[];
/**
* @deprecated
* @param mode
*/
thermostatMode?: ThermostatMode;
/**
* @deprecated
* @param mode
*/
thermostatActiveMode?: ThermostatMode;
/**
* @deprecated
* @param mode
*/
thermostatSetpoint?: number;
/**
* @deprecated
* @param mode
*/
thermostatSetpointHigh?: number;
/**
* @deprecated
* @param mode
*/
thermostatSetpointLow?: number;
}
export enum HumidityMode {
@@ -683,7 +738,7 @@ export interface PanTiltZoom {
*/
export interface Display {
startDisplay(media: MediaObject): Promise<void>;
stopDisplay(media: MediaObject): Promise<void>;
stopDisplay(): Promise<void>;
}
/**
@@ -712,13 +767,7 @@ export interface PasswordStore {
removePassword(password: string): Promise<void>;
}
/**
* Authenticator can be used to require a password before allowing interaction with a security device.
*/
export interface Authenticator {
checkPassword(password: string): Promise<boolean>;
}
/**
* Scenes control multiple different devices into a given state.
*/
@@ -947,16 +996,6 @@ export interface Scriptable {
loadScripts(): Promise<{ [filename: string]: ScriptSource }>;
eval(source: ScriptSource, variables?: { [name: string]: any }): Promise<any>;
}
/**
* SoftwareUpdate provides a way to check for updates and install them. This may be a Scrypted Plugin or device firmware.
*/
export interface SoftwareUpdate {
checkForUpdate(): Promise<boolean>;
installUpdate(): Promise<void>;
updateAvailable?: boolean;
}
export interface BufferConvertorOptions {
sourceId?: string;
@@ -1566,7 +1605,7 @@ export interface HttpResponseOptions {
headers?: object;
}
export interface EngineIOHandler {
onConnection(request: HttpRequest, webSocketUrl: string): Promise<void>;
onConnection(request: HttpRequest, webScoket: WebSocket): Promise<void>;
}
/**
@@ -1664,7 +1703,6 @@ export enum ScryptedInterface {
Intercom = "Intercom",
Lock = "Lock",
PasswordStore = "PasswordStore",
Authenticator = "Authenticator",
Scene = "Scene",
Entry = "Entry",
EntrySensor = "EntrySensor",
@@ -1675,7 +1713,6 @@ export enum ScryptedInterface {
Refresh = "Refresh",
MediaPlayer = "MediaPlayer",
Online = "Online",
SoftwareUpdate = "SoftwareUpdate",
BufferConverter = "BufferConverter",
Settings = "Settings",
BinarySensor = "BinarySensor",
@@ -1711,6 +1748,7 @@ export enum ScryptedInterface {
RTCSignalingChannel = "RTCSignalingChannel",
RTCSignalingClient = "RTCSignalingClient",
LauncherApplication = "LauncherApplication",
ScryptedUser = "ScryptedUser",
}
/**
@@ -1869,6 +1907,62 @@ export interface PluginFork<T> {
worker: NodeWorker;
}
export declare interface DeviceState {
id?: string;
setState?(property: string, value: any): Promise<void>;
}
export interface ScryptedInterfaceDescriptor {
name: string;
properties: string[];
methods: string[];
}
/**
* ScryptedDeviceAccessControl describes the methods and properties on a device
* that will be visible to the user.
* If methods is null, the user will be granted full access to all methods.
* If properties is null, the user will be granted full access to all properties.
*/
export interface ScryptedDeviceAccessControl {
id: string;
methods?: string[];
properties?: string[];
}
/**
* ScryptedUserAccessControl describes the list of devices that
* may be accessed by the user.
*/
export interface ScryptedUserAccessControl {
/**
* If devicesAccessControls is null, the user has full access to all devices.
*/
devicesAccessControls: ScryptedDeviceAccessControl[] | null;
}
/**
* ScryptedUser represents a user managed by Scrypted.
* This interface can not be implemented, only extended by Mixins.
*/
export interface ScryptedUser {
/**
* Retrieve the ScryptedUserAccessControl for a user. If no access control object is returned
* the user has full access to all devices. This differs from an admin user that can also
* access admin related system services.
*/
getScryptedUserAccessControl(): Promise<ScryptedUserAccessControl | null>;
}
export interface APIOptions {
username?: string;
accessControls?: ScryptedUserAccessControl;
}
export interface ConnectOptions extends APIOptions {
pluginId: string;
}
export interface ScryptedStatic {
/**
* @deprecated
@@ -1883,16 +1977,15 @@ export interface ScryptedStatic {
pluginHostAPI: any;
pluginRemoteAPI: any;
/**
* Start a new instance of the plugin, returning an instance of the new process
* and the result of the fork method.
*/
fork?<T>(): PluginFork<T>;
}
export declare interface DeviceState {
id?: string;
setState?(property: string, value: any): Promise<void>;
}
export interface ScryptedInterfaceDescriptor {
name: string;
properties: string[];
methods: string[];
/**
* Initiate the Scrypted RPC wire protocol on a socket.
* @param socket
* @param options
*/
connect?(socket: NodeNetSocket, options?: ConnectOptions): void;
}

View File

@@ -11,7 +11,7 @@
"dependencies": {
"@ffmpeg-installer/ffmpeg": "^1.1.0",
"@mapbox/node-pre-gyp": "^1.0.10",
"@scrypted/types": "^0.2.18",
"@scrypted/types": "^0.2.29",
"adm-zip": "^0.5.9",
"axios": "^0.21.4",
"body-parser": "^1.19.0",
@@ -245,9 +245,9 @@
}
},
"node_modules/@scrypted/types": {
"version": "0.2.18",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.18.tgz",
"integrity": "sha512-LEhdAgpWZbVMDt74zM/jqBQr42xQl4fDaGwAGtwz0XJ1xnx/hXBXVLs+SdP+Gtnoujjqd1kWTdZCDrnPP4/luw=="
"version": "0.2.29",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.29.tgz",
"integrity": "sha512-l6BCe+2jHPnLaKbUfNxjtgfBkzzIVDhUo8iTjAKyJUfdWZsHZ8IFkzP9GTwenUHmOapDwYDboRum9NCxnjt7AA=="
},
"node_modules/@tootallnate/once": {
"version": "1.1.2",
@@ -3281,9 +3281,9 @@
}
},
"@scrypted/types": {
"version": "0.2.18",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.18.tgz",
"integrity": "sha512-LEhdAgpWZbVMDt74zM/jqBQr42xQl4fDaGwAGtwz0XJ1xnx/hXBXVLs+SdP+Gtnoujjqd1kWTdZCDrnPP4/luw=="
"version": "0.2.29",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.29.tgz",
"integrity": "sha512-l6BCe+2jHPnLaKbUfNxjtgfBkzzIVDhUo8iTjAKyJUfdWZsHZ8IFkzP9GTwenUHmOapDwYDboRum9NCxnjt7AA=="
},
"@tootallnate/once": {
"version": "1.1.2",

View File

@@ -5,7 +5,7 @@
"dependencies": {
"@ffmpeg-installer/ffmpeg": "^1.1.0",
"@mapbox/node-pre-gyp": "^1.0.10",
"@scrypted/types": "^0.2.18",
"@scrypted/types": "^0.2.29",
"adm-zip": "^0.5.9",
"axios": "^0.21.4",
"body-parser": "^1.19.0",

View File

@@ -28,7 +28,7 @@ export class ScryptedAlert extends ScryptedDocument {
title: string;
path: string;
message: string;
}``
}
export class PluginDevice extends ScryptedDocument {
constructor(id?: string) {

View File

@@ -22,9 +22,6 @@ export interface PluginAPI {
listen(EventListener: (id: string, eventDetails: EventDetails, eventData: any) => void): Promise<EventListenerRegister>;
listenDevice(id: string, event: string | EventListenerOptions, callback: (eventDetails: EventDetails, eventData: any) => void): Promise<EventListenerRegister>;
ioClose(id: string): Promise<void>;
ioSend(id: string, message: string): Promise<void>;
deliverPush(endpoint: string, request: HttpRequest): Promise<void>;
getLogger(nativeId: ScryptedNativeId): Promise<PluginLogger>;
@@ -113,12 +110,6 @@ export class PluginAPIProxy extends PluginAPIManagedListeners implements PluginA
async listenDevice(id: string, event: string | EventListenerOptions, callback: (eventDetails: EventDetails, eventData: any) => void): Promise<EventListenerRegister> {
return this.manageListener(await this.api.listenDevice(id, event, callback));
}
ioClose(id: string): Promise<void> {
return this.api.ioClose(id);
}
ioSend(id: string, message: string): Promise<void> {
return this.api.ioSend(id, message);
}
deliverPush(endpoint: string, request: HttpRequest): Promise<void> {
return this.api.deliverPush(endpoint, request);
}

View File

@@ -20,8 +20,6 @@ export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAP
'onMixinEvent',
'onDeviceEvent',
'setStorage',
'ioSend',
'ioClose',
'setDeviceProperty',
'deliverPush',
'requestRestart',
@@ -105,18 +103,6 @@ export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAP
}
}
async ioClose(id: string) {
// @ts-expect-error
this.pluginHost.io.clients[id]?.close();
this.pluginHost.ws[id]?.close();
}
async ioSend(id: string, message: string) {
// @ts-expect-error
this.pluginHost.io.clients[id]?.send(message);
this.pluginHost.ws[id]?.send(message);
}
async setState(nativeId: ScryptedNativeId, key: string, value: any) {
checkProperty(key, value);
this.scrypted.stateManager.setPluginState(this.pluginId, nativeId, this.propertyInterfaces?.[key], key, value);

View File

@@ -168,7 +168,14 @@ export class PluginHost {
});
// @ts-expect-error
await handler.onConnection(endpointRequest, new WebSocketConnection(`io://${id}`));
await handler.onConnection(endpointRequest, new WebSocketConnection(`io://${id}`, {
send(message) {
socket.send(message);
},
close(message) {
socket.close();
},
}));
}
catch (e) {
console.error('engine.io plugin error', e);
@@ -304,7 +311,6 @@ export class PluginHost {
});
this.worker.setupRpcPeer(this.peer);
this.peer.addSerializer(WebSocketConnection, WebSocketConnection.name, new WebSocketSerializer());
this.worker.stdout.on('data', data => console.log(data.toString()));
this.worker.stderr.on('data', data => console.error(data.toString()));
@@ -373,7 +379,6 @@ export class PluginHost {
}
});
serializer.setupRpcPeer(rpcPeer);
rpcPeer.addSerializer(WebSocketConnection, WebSocketConnection.name, new WebSocketSerializer());
// wrap the host api with a connection specific api that can be torn down on disconnect
const createMediaManager = await this.peer.getParam('createMediaManager');
@@ -390,7 +395,6 @@ export class PluginHost {
async createRpcPeer(duplex: Duplex) {
const rpcPeer = createDuplexRpcPeer(`api/${this.pluginId}`, 'duplex', duplex, duplex);
rpcPeer.addSerializer(WebSocketConnection, WebSocketConnection.name, new WebSocketSerializer());
// wrap the host api with a connection specific api that can be torn down on disconnect
const createMediaManager = await this.peer.getParam('createMediaManager');

View File

@@ -62,7 +62,7 @@ export interface WebSocketConnectCallbacks {
}
export interface WebSocketConnect {
(url: string, callbacks: WebSocketConnectCallbacks): void;
(connection: WebSocketConnection, callbacks: WebSocketConnectCallbacks): void;
}
export interface WebSocketMethods {
@@ -76,15 +76,14 @@ export function createWebSocketClass(__websocketConnect: WebSocketConnect): any
_url: string;
_protocols: string[];
readyState: number;
_ws: WebSocketMethods;
constructor(url: string, protocols?: string[]) {
constructor(public connection: WebSocketConnection, protocols?: string[]) {
super();
this._url = url;
this._url = connection.url;
this._protocols = protocols;
this.readyState = 0;
__websocketConnect(url, {
__websocketConnect(connection, {
connect: (e, ws) => {
// connect
if (e != null) {
@@ -95,7 +94,6 @@ export function createWebSocketClass(__websocketConnect: WebSocketConnect): any
return;
}
this._ws = ws;
this.readyState = 1;
this.dispatchEvent({
type: 'open',
@@ -129,7 +127,7 @@ export function createWebSocketClass(__websocketConnect: WebSocketConnect): any
}
send(message: string | ArrayBufferLike) {
this._ws.send(message);
this.connection.send(message);
}
get url() {
@@ -141,7 +139,7 @@ export function createWebSocketClass(__websocketConnect: WebSocketConnect): any
}
close(reason: string) {
this._ws.close(reason);
this.connection.close(reason);
}
}
@@ -153,10 +151,25 @@ export function createWebSocketClass(__websocketConnect: WebSocketConnect): any
return WebSocket;
}
export class WebSocketConnection {
export class WebSocketConnection implements WebSocketMethods {
[RpcPeer.PROPERTY_PROXY_PROPERTIES]: any;
constructor(public url: string) {
[RpcPeer.PROPERTY_PROXY_ONEWAY_METHODS] = [
"send",
"close",
];
constructor(public url: string, public websocketMethods: WebSocketMethods) {
this[RpcPeer.PROPERTY_PROXY_PROPERTIES] = {
url,
}
}
send(message: string | ArrayBufferLike): void {
return this.websocketMethods.send(message);
}
close(message: string): void {
return this.websocketMethods.close(message);
}
}
@@ -164,16 +177,12 @@ export class WebSocketSerializer implements RpcSerializer {
WebSocket: ReturnType<typeof createWebSocketClass>;
serialize(value: any, serializationContext?: any) {
const connection = value as WebSocketConnection;
connection[RpcPeer.PROPERTY_PROXY_PROPERTIES] = {
url: connection.url,
}
return connection;
throw new Error("WebSocketSerializer should only be used for deserialization.");
}
deserialize(serialized: any, serializationContext?: any) {
deserialize(serialized: WebSocketConnection, serializationContext?: any) {
if (!this.WebSocket)
return undefined;
return new this.WebSocket(serialized.url);
return new this.WebSocket(serialized);
}
}

View File

@@ -336,6 +336,9 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
pluginReader = undefined;
const script = main.toString();
scrypted.connect = (socket, options) => {
process.send(options, socket);
}
const forks = new Set<PluginRemote>();

View File

@@ -429,15 +429,16 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
const retPromise = new Promise<ScryptedStatic>(resolve => done = resolve);
peer.params.getRemote = async (api: PluginAPI, pluginId: string) => {
websocketSerializer.WebSocket = createWebSocketClass((url: string, callbacks: WebSocketConnectCallbacks) => {
websocketSerializer.WebSocket = createWebSocketClass((connection, callbacks) => {
const {url} = connection;
if (url.startsWith('io://') || url.startsWith('ws://')) {
const id = url.substring('xx://'.length);
ioSockets[id] = callbacks;
callbacks.connect(undefined, {
close: () => api.ioClose(id),
send: (message: string) => api.ioSend(id, message),
close: (message) => connection.close(message),
send: (message) => connection.send(message),
});
}
else {

View File

@@ -393,6 +393,12 @@ export class RpcPeer {
if (!proxy)
proxy = this.newProxy(__remote_proxy_id, __remote_constructor_name, __remote_proxy_props, __remote_proxy_oneway_methods);
proxy[RpcPeer.finalizerIdSymbol] = __remote_proxy_finalizer_id;
const deserializer = this.nameDeserializerMap.get(__remote_constructor_name);
if (deserializer) {
return deserializer.deserialize(proxy, deserializationContext);
}
return proxy;
}

View File

@@ -345,7 +345,14 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
});
// @ts-expect-error
await handler.onConnection(httpRequest, new WebSocketConnection(`ws://${id}`));
await handler.onConnection(httpRequest, new WebSocketConnection(`ws://${id}`, {
send(message) {
ws.send(message);
},
close(message) {
ws.close();
},
}));
}
async getComponent(componentId: string): Promise<any> {