Compare commits

..

22 Commits

Author SHA1 Message Date
Koushik Dutta
7614d12363 prepublish 2023-02-25 14:25:24 -08:00
Koushik Dutta
3189317b2d server: additional admin login traps for HA ingress 2023-02-25 14:25:16 -08:00
Koushik Dutta
410d11248f snapshot: improve failure resiliency with prebuffer fallback and ignoring ffpmeg errors when buffers are returned 2023-02-25 13:41:41 -08:00
Koushik Dutta
28e1f5ac8a prepublish 2023-02-25 09:55:03 -08:00
Koushik Dutta
bafe73d296 server: add support for admin configuration via env 2023-02-25 09:54:54 -08:00
Koushik Dutta
f17ce50f17 webrtc: werift config should also respect turn server preference 2023-02-25 09:33:09 -08:00
Koushik Dutta
18f5872be1 core: fix oauth on rehosted urls 2023-02-24 22:00:48 -08:00
Koushik Dutta
fdccaaa65e client: publish 2023-02-24 19:57:45 -08:00
Koushik Dutta
6a55172924 server/rpc: add support for clobbering non-state properties and methods 2023-02-24 19:41:50 -08:00
Koushik Dutta
1e41af77fa rebroadcast: add external url request support 2023-02-24 18:58:28 -08:00
Koushik Dutta
e169a6e02d rebroadcast: add rtsp path to prevent port guessing access 2023-02-24 18:49:16 -08:00
Koushik Dutta
ef55c834af sdk: getVideoStream routing hints 2023-02-24 18:48:39 -08:00
Koushik Dutta
3812ad92ac core: fix safari weirdness on async methods and window open 2023-02-24 16:26:07 -08:00
Koushik Dutta
0bdb402e7b core: use target _blank when starting oauth from inside iframe 2023-02-24 16:11:38 -08:00
Koushik Dutta
1588ea250b server: publish rpc change 2023-02-24 16:01:52 -08:00
Koushik Dutta
47f603877b prepublish 2023-02-24 16:01:29 -08:00
Koushik Dutta
9adc803206 prepublish 2023-02-24 16:00:47 -08:00
Koushik Dutta
031e290017 google-device-access: implement new api 2023-02-24 11:50:19 -08:00
Koushik Dutta
a9ab2d0110 google-device-access: use new state api 2023-02-24 11:42:52 -08:00
Koushik Dutta
9592b6087b Merge branch 'main' of github.com:koush/scrypted 2023-02-24 11:30:21 -08:00
Koushik Dutta
5c34213b5d google-device-access: fixup project 2023-02-24 11:30:15 -08:00
Nick Berardi
4435fe1e0a alexa: ensure we are talking to the correct API endpoint (#580)
* alexa plugin: ensure we are talking to the right endpoint region for a customer

* added the api endpoint as a visible setting
2023-02-24 09:41:11 -08:00
31 changed files with 430 additions and 291 deletions

View File

@@ -1,24 +1,20 @@
{
"name": "@scrypted/client",
"version": "1.1.37",
"version": "1.1.38",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/client",
"version": "1.1.37",
"version": "1.1.38",
"license": "ISC",
"dependencies": {
"@scrypted/types": "^0.2.64",
"adm-zip": "^0.5.9",
"axios": "^0.25.0",
"engine.io-client": "^6.2.2",
"linkfs": "^2.1.0",
"memfs": "^3.4.1",
"rimraf": "^3.0.2"
},
"devDependencies": {
"@types/adm-zip": "^0.4.34",
"@types/ip": "^1.1.0",
"@types/node": "^17.0.17",
"typescript": "^4.7.4"
@@ -63,15 +59,6 @@
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
},
"node_modules/@types/adm-zip": {
"version": "0.4.34",
"resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.4.34.tgz",
"integrity": "sha512-8ToYLLAYhkRfcmmljrKi22gT2pqu7hGMDtORP1emwIEGmgUTZOsaDjzWFzW5N2frcFRz/50CWt4zA1CxJ73pmQ==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/ip": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@types/ip/-/ip-1.1.0.tgz",
@@ -87,14 +74,6 @@
"integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA==",
"dev": true
},
"node_modules/adm-zip": {
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
"integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg==",
"engines": {
"node": ">=6.0"
}
},
"node_modules/axios": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
@@ -177,11 +156,6 @@
}
}
},
"node_modules/fs-monkey": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz",
"integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q=="
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -220,22 +194,6 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/linkfs": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/linkfs/-/linkfs-2.1.0.tgz",
"integrity": "sha512-kmsGcmpvjStZ0ATjuHycBujtNnXiZR28BTivEu0gAMDTT7GEyodcK6zSRtu6xsrdorrPZEIN380x7BD7xEYkew=="
},
"node_modules/memfs": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz",
"integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==",
"dependencies": {
"fs-monkey": "1.0.3"
},
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -340,15 +298,6 @@
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
},
"@types/adm-zip": {
"version": "0.4.34",
"resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.4.34.tgz",
"integrity": "sha512-8ToYLLAYhkRfcmmljrKi22gT2pqu7hGMDtORP1emwIEGmgUTZOsaDjzWFzW5N2frcFRz/50CWt4zA1CxJ73pmQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/ip": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@types/ip/-/ip-1.1.0.tgz",
@@ -364,11 +313,6 @@
"integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA==",
"dev": true
},
"adm-zip": {
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
"integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg=="
},
"axios": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
@@ -426,11 +370,6 @@
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA=="
},
"fs-monkey": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz",
"integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q=="
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -463,19 +402,6 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"linkfs": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/linkfs/-/linkfs-2.1.0.tgz",
"integrity": "sha512-kmsGcmpvjStZ0ATjuHycBujtNnXiZR28BTivEu0gAMDTT7GEyodcK6zSRtu6xsrdorrPZEIN380x7BD7xEYkew=="
},
"memfs": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz",
"integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==",
"requires": {
"fs-monkey": "1.0.3"
}
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/client",
"version": "1.1.37",
"version": "1.1.38",
"description": "",
"main": "dist/packages/client/src/index.js",
"scripts": {
@@ -12,18 +12,14 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@types/adm-zip": "^0.4.34",
"@types/ip": "^1.1.0",
"@types/node": "^17.0.17",
"typescript": "^4.7.4"
},
"dependencies": {
"@scrypted/types": "^0.2.64",
"adm-zip": "^0.5.9",
"axios": "^0.25.0",
"engine.io-client": "^6.2.2",
"linkfs": "^2.1.0",
"memfs": "^3.4.1",
"rimraf": "^3.0.2"
}
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/rpc",
"version": "0.0.6",
"version": "0.0.7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/rpc",
"version": "0.0.6",
"version": "0.0.7",
"license": "ISC",
"devDependencies": {
"@types/node": "^18.11.18",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/rpc",
"version": "0.0.6",
"version": "0.0.7",
"description": "",
"main": "dist/index.js",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/alexa",
"version": "0.0.20",
"version": "0.1.0",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",
@@ -35,7 +35,7 @@
"dependencies": {
"@types/node": "^16.6.1",
"alexa-smarthome-ts": "^0.0.1",
"axios": "^0.24.0",
"axios": "^1.3.4",
"uuid": "^9.0.0"
},
"devDependencies": {

View File

@@ -1,5 +1,5 @@
import axios from 'axios';
import sdk, { HttpRequest, HttpRequestHandler, HttpResponse, MixinProvider, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface } from '@scrypted/sdk';
import sdk, { HttpRequest, HttpRequestHandler, HttpResponse, MixinProvider, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, Setting, SettingValue, Settings } from '@scrypted/sdk';
import { StorageSettings } from '@scrypted/sdk/storage-settings';
import { AutoenableMixinProvider } from '@scrypted/common/src/autoenable-mixin-provider';
import { isSupported } from './types';
@@ -12,16 +12,22 @@ const { systemManager, deviceManager } = sdk;
const client_id = "amzn1.application-oa2-client.3283807e04d8408eb44a698c10f9dd13";
const client_secret = "bed445e2b26730acd818b90e175b275f6b67b18ff8645e571c5b3e311fa75ee9";
class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler, MixinProvider {
class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler, MixinProvider, Settings {
storageSettings = new StorageSettings(this, {
tokenInfo: {
hide: true,
json: true,
json: true
},
syncedDevices: {
multiple: true,
hide: true,
hide: true
},
apiEndpoint: {
title: 'Alexa Endpoint',
description: 'This is the endpoint Alexa will use to send events to. This is set after you login.',
type: 'string',
readonly: true
}
});
handlers = new Map<string, AlexaHandler>();
@@ -85,6 +91,13 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
});
}
getSettings(): Promise<Setting[]> {
return this.storageSettings.getSettings();
}
putSetting(key: string, value: SettingValue): Promise<void> {
return this.storageSettings.putSetting(key, value);
}
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any; }): Promise<any> {
return mixinDevice;
}
@@ -99,11 +112,41 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
deviceManager.requestRestart();
}
readonly endpoints: string[] = [
'api.amazonalexa.com',
'api.eu.amazonalexa.com',
'api.fe.amazonalexa.com'
];
async getAlexaEndpoint() : Promise<string> {
if (this.storageSettings.values.apiEndpoint)
return this.storageSettings.values.apiEndpoint;
try {
const accessToken = await this.getAccessToken();
const response = await axios.get(`https://${this.endpoints[0]}/v1/alexaApiEndpoint`, {
headers: {
'Authorization': 'Bearer ' + accessToken,
}
});
const endpoint: string = response.data.endpoints[0];
this.storageSettings.values.apiEndpoint = endpoint;
return endpoint;
} catch (err) {
this.console.error(err);
// default to NA/RoW endpoint if we can't get the endpoint.
return this.endpoints[0];
}
}
async postEvent(data: any) {
const accessToken = await this.getAccessToken();
const endpoint = await this.getAlexaEndpoint();
const self = this;
return axios.post('https://api.amazonalexa.com/v3/events', data, {
return axios.post(`https://${endpoint}/v3/events`, data, {
headers: {
'Authorization': 'Bearer ' + accessToken,
}
@@ -267,6 +310,7 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
const json = JSON.parse(request.body);
const { grant } = json.directive.payload;
this.storageSettings.values.tokenInfo = grant;
this.storageSettings.values.apiEndpoint = undefined;
this.accessToken = undefined;
const self = this;
@@ -274,6 +318,7 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
self.console.error(`Failed to handle the AcceptGrant directive because ${reason}`);
this.storageSettings.values.tokenInfo = undefined;
this.storageSettings.values.apiEndpoint = undefined;
this.accessToken = undefined;
response.send(JSON.stringify({
@@ -311,6 +356,7 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
this.console.error(`AcceptGrant.Response failed because ${error}`);
this.storageSettings.values.tokenInfo = undefined;
this.storageSettings.values.apiEndpoint = undefined;
this.accessToken = undefined;
throw error;
}
@@ -455,7 +501,7 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
catch (e) {
this.console.error(`request failed due to invalid authorization`, e);
response.send(e.message, {
code: 500,
code: 500
});
return;
}

View File

@@ -113,6 +113,7 @@ export class AlexaSignalingSession implements RTCSignalingSession {
// this could be a low resolution screen, no way of knowing, so never send a
// 1080p+ stream.
screen: {
devicePixelRatio: 1, // TODO: get this from the device
width: 1280,
height: 720,
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/core",
"version": "0.1.91",
"version": "0.1.94",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/core",
"version": "0.1.91",
"version": "0.1.94",
"license": "Apache-2.0",
"dependencies": {
"@scrypted/common": "file:../../common",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/core",
"version": "0.1.91",
"version": "0.1.94",
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
"author": "Scrypted",
"license": "Apache-2.0",

View File

@@ -2,14 +2,23 @@
<v-btn text color="primary" @click="onClick">Login</v-btn>
</template>
<script>
import RPCInterface from "./RPCInterface.vue";
import qs from 'query-string';
import RPCInterface from "./RPCInterface.vue";
export default {
mixins: [RPCInterface],
methods: {
onChange() { },
isIFrame() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
},
onClick: function () {
// https://stackoverflow.com/a/39387533
const windowReference = this.isIFrame() ? window.open(undefined, '_blank') : undefined;
this.rpc()
.getOauthUrl()
.then(data => {
@@ -22,7 +31,9 @@ export default {
u = new URL(redirect_uri);
}
catch (e) {
u = new URL(redirect_uri, window.location.href);
const baseURI = new URL(document.baseURI);
const scryptedRootURI = new URL('../../../../', baseURI);
u = new URL('.' + redirect_uri, scryptedRootURI);
u.hostname = 'localhost';
}
if (u.hostname === 'localhost') {
@@ -40,7 +51,10 @@ export default {
r: window.location.toString(),
});
url.search = qs.stringify(querystring);
window.location = url.toString();
if (windowReference)
windowReference.location = url.toString();
else
window.location = url.toString();
});
}
}

View File

@@ -1 +0,0 @@
../../../common/fs/unavailable.jpg

View File

@@ -1,14 +1,13 @@
import sdk, { DeviceManifest, DeviceProvider, HttpRequest, HttpRequestHandler, HttpResponse, HumiditySensor, MediaObject, MotionSensor, OauthClient, Refresh, ScryptedDeviceType, ScryptedInterface, Setting, Settings, TemperatureSetting, TemperatureUnit, Thermometer, ThermostatMode, VideoCamera, MediaStreamOptions, BinarySensor, DeviceInformation, RTCAVSignalingSetup, Camera, PictureOptions, ObjectsDetected, ObjectDetector, ObjectDetectionTypes, FFmpegInput, RequestMediaStreamOptions, Readme, RTCSignalingChannel, RTCSessionControl, RTCSignalingSession, ResponseMediaStreamOptions, RTCSignalingOptions, RTCSignalingSendIceCandidate, ScryptedMimeTypes, MediaStreamUrl } from '@scrypted/sdk';
import { ScryptedDeviceBase } from '@scrypted/sdk';
import qs from 'query-string';
import ClientOAuth2 from 'client-oauth2';
import { URL } from 'url';
import axios from 'axios';
import throttle from 'lodash/throttle';
import { connectRTCSignalingClients } from '@scrypted/common/src/rtc-signaling';
import { sleep } from '@scrypted/common/src/sleep';
import fs from 'fs';
import sdk, { BinarySensor, Camera, DeviceInformation, DeviceManifest, DeviceProvider, FFmpegInput, HttpRequest, HttpRequestHandler, HttpResponse, HumiditySensor, MediaObject, MediaStreamUrl, MotionSensor, OauthClient, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, PictureOptions, Readme, Refresh, RequestMediaStreamOptions, ResponseMediaStreamOptions, RTCAVSignalingSetup, RTCSessionControl, RTCSignalingChannel, RTCSignalingSendIceCandidate, RTCSignalingSession, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, TemperatureCommand, TemperatureSetting, TemperatureUnit, Thermometer, ThermostatMode, VideoCamera } from '@scrypted/sdk';
import axios from 'axios';
import ClientOAuth2 from 'client-oauth2';
import { randomBytes } from 'crypto';
import fs from 'fs';
import throttle from 'lodash/throttle';
import qs from 'query-string';
import { URL } from 'url';
const { deviceManager, mediaManager, endpointManager, systemManager } = sdk;
@@ -101,6 +100,10 @@ class NestRTCSessionControl implements RTCSessionControl {
constructor(public camera: NestCamera, public options: { streamExtensionToken: string, mediaSessionId: string }) {
}
async setPlayback(options: { audio: boolean; video: boolean; }): Promise<void> {
}
async getRefreshAt(): Promise<number> {
return this.refreshAt;
}
@@ -372,6 +375,52 @@ class NestThermostat extends ScryptedDeviceBase implements HumiditySensor, Therm
this.reload();
}
async setTemperature(command: TemperatureCommand): Promise<void> {
// set this in case round trip is slow.
let { mode, setpoint } = command;
if (mode) {
const nestMode = toNestMode(mode);
this.device.traits['sdm.devices.traits.ThermostatMode'].mode = nestMode;
this.executeCommandSetMode = {
command: 'sdm.devices.commands.ThermostatMode.SetMode',
params: {
mode: nestMode,
},
}
}
if (command.setpoint) {
mode ||= fromNestMode(this.device.traits['sdm.devices.traits.ThermostatMode'].mode);
this.executeCommandSetCelsius = {
command: setpointReverseMap.get(mode),
params: {
},
};
if (typeof command.setpoint === 'number') {
if (mode === ThermostatMode.Heat) {
this.executeCommandSetCelsius.params.heatCelsius = command.setpoint;
}
else if (mode === ThermostatMode.Cool) {
this.executeCommandSetCelsius.params.coolCelsius = command.setpoint;
}
else {
this.executeCommandSetCelsius.params.coolCelsius = command.setpoint;
this.executeCommandSetCelsius.params.heatCelsius = command.setpoint;
}
}
else {
this.executeCommandSetCelsius.params.heatCelsius = command[0];
this.executeCommandSetCelsius.params.coolCelsius = command[1];
}
}
await this.executeThrottle();
await this.refresh(null, true);
}
async setTemperatureUnit(temperatureUnit: TemperatureUnit): Promise<void> {
// not supported by API. throw?
}
@@ -398,26 +447,37 @@ class NestThermostat extends ScryptedDeviceBase implements HumiditySensor, Therm
const heat = device.traits?.['sdm.devices.traits.ThermostatTemperatureSetpoint']?.heatCelsius;
const cool = device.traits?.['sdm.devices.traits.ThermostatTemperatureSetpoint']?.coolCelsius;
let setpoint: number | [number, number];
if (this.thermostatMode === ThermostatMode.Heat) {
this.thermostatSetpoint = heat;
this.thermostatSetpointHigh = undefined;
this.thermostatSetpointLow = undefined;
setpoint = heat;
}
else if (this.thermostatMode === ThermostatMode.Cool) {
this.thermostatSetpoint = cool;
this.thermostatSetpointHigh = undefined;
this.thermostatSetpointLow = undefined;
setpoint = cool;
}
else if (this.thermostatMode === ThermostatMode.HeatCool) {
this.thermostatSetpoint = undefined;
this.thermostatSetpointHigh = heat;
this.thermostatSetpointLow = cool;
setpoint = [heat, cool];
}
else {
this.thermostatSetpoint = undefined;
this.thermostatSetpointHigh = undefined;
this.thermostatSetpointLow = undefined;
}
this.temperatureSetting = {
activeMode: this.thermostatActiveMode,
mode: this.thermostatMode,
setpoint,
availableModes: modes,
}
}
async refresh(refreshInterface: string, userInitiated: boolean): Promise<void> {
@@ -560,6 +620,8 @@ export class GoogleSmartDeviceAccess extends ScryptedDeviceBase implements Oauth
}
})();
}
async releaseDevice(id: string, nativeId: string): Promise<void> {
}
async onRequest(request: HttpRequest, response: HttpResponse): Promise<void> {
const payload = JSON.parse(Buffer.from(JSON.parse(request.body).message.data, 'base64').toString());

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/prebuffer-mixin",
"version": "0.9.72",
"version": "0.9.73",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/prebuffer-mixin",
"version": "0.9.72",
"version": "0.9.73",
"license": "Apache-2.0",
"dependencies": {
"@scrypted/common": "file:../../common",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/prebuffer-mixin",
"version": "0.9.72",
"version": "0.9.73",
"description": "Video Stream Rebroadcast, Prebuffer, and Management Plugin for Scrypted.",
"author": "Scrypted",
"license": "Apache-2.0",

View File

@@ -1,6 +1,6 @@
import { RtspServer, Headers } from "@scrypted/common/src/rtsp-server";
import net from 'net';
import { Headers, RtspServer } from "@scrypted/common/src/rtsp-server";
import fs from 'fs';
import net from 'net';
// non standard extension that dumps the rtp payload to a file.
export class FileRtspServer extends RtspServer {
@@ -8,8 +8,8 @@ export class FileRtspServer extends RtspServer {
segmentBytesWritten = 0;
writeConsole: Console;
constructor(client: net.Socket, sdp?: string) {
super(client, sdp);
constructor(client: net.Socket, sdp?: string, checkRequest?: (method: string, url: string, headers: Headers, rawMessage: string[]) => Promise<boolean>) {
super(client, sdp, undefined, checkRequest);
this.client.on('close', () => {
if (this.writeStream)

View File

@@ -1114,10 +1114,16 @@ class PrebufferSession {
return chunk;
}
const client = await listenZeroSingleClient();
const hostname = options?.route === 'external' ? '0.0.0.0' : undefined;
const client = await listenZeroSingleClient(hostname);
const rtspServerPath = '/' + crypto.randomBytes(8).toString('hex');
socketPromise = client.clientPromise.then(async (socket) => {
sdp = addTrackControls(sdp);
server = new FileRtspServer(socket, sdp);
server = new FileRtspServer(socket, sdp, async (method, url, headers, rawMessage) => {
server.checkRequest = undefined;
const u = new URL(url);
return u.pathname === rtspServerPath;
});
server.writeConsole = this.console;
if (session.parserSpecific) {
const parserSpecific = session.parserSpecific as RtspSessionParserSpecific;
@@ -1142,7 +1148,20 @@ class PrebufferSession {
interleavePassthrough = session.parserSpecific && serverPortMap.size === 0;
return socket;
})
url = client.url.replace('tcp://', 'rtsp://');
url = client.url.replace('tcp://', 'rtsp://') + rtspServerPath;
if (hostname) {
try {
const addresses = await sdk.endpointManager.getLocalAddresses();
const [address] = addresses;
if (address) {
const u = new URL(url);
u.hostname = address;
url = u.toString();
}
}
catch (e) {
}
}
}
else {
const client = await listenZeroSingleClient();
@@ -1252,7 +1271,7 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
const u = new URL(url);
for (const session of this.sessions.values()) {
if (u.pathname.endsWith(session.rtspServerPath)) {
if (u.pathname === '/' + session.rtspServerPath) {
server.console = session.console;
prebufferSession = session;
prebufferSession.ensurePrebufferSession();
@@ -1260,7 +1279,7 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
server.sdp = await prebufferSession.sdp;
return true;
}
if (u.pathname.endsWith(session.rtspServerMutedPath)) {
if (u.pathname === '/' + session.rtspServerMutedPath) {
server.console = session.console;
prebufferSession = session;
prebufferSession.ensurePrebufferSession();
@@ -1326,7 +1345,7 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
}
async getVideoStream(options?: RequestMediaStreamOptions): Promise<MediaObject> {
if (options?.directMediaStream)
if (options?.route === 'direct')
return this.mixinDevice.getVideoStream(options);
await this.ensurePrebufferSessions();

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/snapshot",
"version": "0.0.43",
"version": "0.0.45",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/snapshot",
"version": "0.0.43",
"version": "0.0.45",
"dependencies": {
"@koush/axios-digest-auth": "^0.8.5",
"@types/node": "^16.6.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/snapshot",
"version": "0.0.43",
"version": "0.0.45",
"description": "Snapshot Plugin for Scrypted",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
@@ -22,6 +22,7 @@
"camera"
],
"scrypted": {
"realfs": true,
"name": "Snapshot Plugin",
"type": "API",
"interfaces": [

View File

@@ -1,3 +1,4 @@
import fs from 'fs';
import { addVideoFilterArguments } from '@scrypted/common/src/ffmpeg-helpers';
import { ffmpegLogInitialOutput, safeKillFFmpeg, safePrintFFmpegArguments } from '@scrypted/common/src/media-helpers';
import { sleep } from '@scrypted/common/src/sleep';
@@ -115,7 +116,11 @@ export async function ffmpegFilterImageBuffer(inputJpeg: Buffer, options: FFmpeg
input.write(inputJpeg);
input.end();
return ffmpegFilterImageInternal(cp, options);
return ffmpegFilterImageInternal(cp, options)
.catch(e => {
fs.writeFileSync("/tmp/test.jpg", inputJpeg);
throw e;
})
}
export async function ffmpegFilterImage(inputArguments: string[], options: FFmpegImageFilterOptions) {
@@ -168,14 +173,16 @@ export async function ffmpegFilterImageInternal(cp: ChildProcess, options: FFmpe
cp.stdio[3].on('data', data => buffers.push(data));
const to = options.timeout ? setTimeout(() => {
console.log('ffmpeg stream to image conversion timed out.');
console.log('ffmpeg input to image conversion timed out.');
safeKillFFmpeg(cp);
}, 10000) : undefined;
const [exitCode] = await once(cp, 'exit');
const exit = once(cp, 'exit');
await once(cp.stdio[3], 'end').catch(() => {});
const [exitCode] = await exit;
clearTimeout(to);
if (exitCode && !buffers.length)
throw new Error(`ffmpeg stream to image conversion failed with exit code: ${exitCode}`);
throw new Error(`ffmpeg input to image conversion failed with exit code: ${exitCode}, ${cp.spawnargs.join(' ')}`);
return Buffer.concat(buffers);
}
@@ -207,7 +214,7 @@ export async function ffmpegFilterImageStream(cp: ChildProcess, options: FFmpegI
if (last)
resolve(last);
else
reject(new Error(`ffmpeg stream to image conversion failed with exit code: ${exitCode}`));
reject(new Error(`ffmpeg stream to image conversion failed with exit code: ${exitCode}, ${cp.spawnargs.join(' ')}`));
})
});

View File

@@ -1,10 +1,10 @@
import AxiosDigestAuth from '@koush/axios-digest-auth';
import { AutoenableMixinProvider } from "@scrypted/common/src/autoenable-mixin-provider";
import { createMapPromiseDebouncer, RefreshPromise, singletonPromise, TimeoutError, timeoutPromise } from "@scrypted/common/src/promise-utils";
import { StorageSettings } from "@scrypted/sdk/storage-settings";
import { createMapPromiseDebouncer, RefreshPromise, singletonPromise, TimeoutError } from "@scrypted/common/src/promise-utils";
import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from "@scrypted/common/src/settings-mixin";
import sdk, { BufferConverter, BufferConvertorOptions, Camera, FFmpegInput, MediaObject, MixinProvider, RequestMediaStreamOptions, RequestPictureOptions, ResponsePictureOptions, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera } from "@scrypted/sdk";
import axios, { Axios } from "axios";
import { StorageSettings } from "@scrypted/sdk/storage-settings";
import axios, { AxiosInstance } from "axios";
import https from 'https';
import path from 'path';
import MimeType from 'whatwg-mimetype';
@@ -72,22 +72,12 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
},
snapshotsFromPrebuffer: {
title: 'Snapshots from Prebuffer',
description: 'Prefer snapshots from the Rebroadcast Plugin prebuffer when available. This setting uses considerable CPU to convert a video stream into a snapshot.',
type: 'boolean',
defaultValue: !this.mixinDeviceInterfaces.includes(ScryptedInterface.Camera),
},
snapshotMode: {
title: 'Snapshot Mode',
description: 'Set the snapshot mode to accomodate cameras with slow snapshots that may hang HomeKit.\nSetting the mode to "Never Wait" will only use recently available snapshots.\nSetting the mode to "Timeout" will cancel slow snapshots.',
description: 'Prefer snapshots from the Rebroadcast Plugin prebuffer when available. This setting uses considerable CPU to convert a video stream into a snapshot. The Default setting will use the camera snapshot and fall back to prebuffer on failure.',
choices: [
'Default',
'Never Wait',
'Timeout',
'Enabled',
'Disabled',
],
mapGet(value) {
// renamed the setting value.
return value === 'Normal' ? 'Default' : value;
},
defaultValue: 'Default',
},
snapshotResolution: {
@@ -107,7 +97,6 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
type: 'clippath',
},
});
axiosClient: Axios | AxiosDigestAuth;
snapshotDebouncer = createMapPromiseDebouncer<Buffer>();
errorPicture: RefreshPromise<Buffer>;
timeoutPicture: RefreshPromise<Buffer>;
@@ -117,6 +106,7 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
lastErrorImagesClear = 0;
static lastGeneratedErrorImageTime = 0;
lastAvailablePicture: Buffer;
psos: ResponsePictureOptions[];
constructor(public plugin: SnapshotPlugin, options: SettingsMixinDeviceOptions<Camera>) {
super(options);
@@ -132,7 +122,40 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
let needSoftwareResize = !!(options?.picture?.width || options?.picture?.height);
let takePicture: (options?: RequestPictureOptions) => Promise<Buffer>;
if (this.storageSettings.values.snapshotsFromPrebuffer) {
const { snapshotsFromPrebuffer } = this.storageSettings.values;
let usePrebufferSnapshots: boolean;
switch (snapshotsFromPrebuffer) {
case 'true':
case 'Enabled':
usePrebufferSnapshots = true;
break;
case 'Disabled':
usePrebufferSnapshots = false;
break;
default:
if (!this.mixinDeviceInterfaces.includes(ScryptedInterface.Camera))
usePrebufferSnapshots = true;
break;
}
// unifi cameras send stale snapshots which are unusable for events,
// so force a prebuffer snapshot in this instance.
// if prebuffer is not available, it will fall back.
if (eventSnapshot && usePrebufferSnapshots !== false) {
try {
const psos = await this.getPictureOptions();
if (psos?.[0]?.staleDuration) {
usePrebufferSnapshots = true;
}
}
catch (e) {
}
}
let takePrebufferPicture: () => Promise<Buffer>;
const preparePrebufferSnapshot = async () => {
if (takePrebufferPicture)
return takePrebufferPicture;
try {
const realDevice = systemManager.getDeviceById<VideoCamera>(this.id);
const msos = await realDevice.getVideoStreamOptions();
@@ -148,114 +171,124 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
request.prebuffer = eventSnapshot ? 1000 : 6000;
if (this.lastAvailablePicture)
request.refresh = false;
takePicture = async () => mediaManager.convertMediaObjectToBuffer(await realDevice.getVideoStream(request), 'image/jpeg');
this.console.log('snapshotting active prebuffer');
takePrebufferPicture = async () => {
// this.console.log('snapshotting active prebuffer');
return mediaManager.convertMediaObjectToBuffer(await realDevice.getVideoStream(request), 'image/jpeg');
};
return takePrebufferPicture;
}
}
catch (e) {
}
}
if (usePrebufferSnapshots) {
takePicture = await preparePrebufferSnapshot();
}
if (!takePicture) {
if (!this.storageSettings.values.snapshotUrl) {
if (this.mixinDeviceInterfaces.includes(ScryptedInterface.Camera)) {
takePicture = async (options?: RequestPictureOptions) => {
const internalTakePicture = async () => {
if (!options?.id && this.storageSettings.values.defaultSnapshotChannel !== 'Camera Default') {
try {
if (!psos)
psos = await this.mixinDevice.getPictureOptions();
const pso = psos.find(pso => pso.name === this.storageSettings.values.defaultSnapshotChannel);
if (!options)
options = {};
options.id = pso.id;
}
catch (e) {
}
}
return this.mixinDevice.takePicture(options).then(mo => mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg'))
}
if (this.storageSettings.values.snapshotUrl) {
let username: string;
let password: string;
// full resolution setging ignores resize.
if (this.storageSettings.values.snapshotResolution === 'Full Resolution') {
if (options)
options.picture = undefined;
return internalTakePicture();
}
// if resize wasn't requested, continue as normal.
const resizeRequested = !!options?.picture;
if (!resizeRequested)
return internalTakePicture();
// resize was requested
// crop and scale needs to operate on the full resolution image.
if (this.storageSettings.values.snapshotCropScale?.length) {
options.picture = undefined;
// resize after the cop and scale.
needSoftwareResize = resizeRequested;
return internalTakePicture();
}
// determine see if that can be handled by camera hardware
let psos: ResponsePictureOptions[];
try {
if (!psos)
psos = await this.mixinDevice.getPictureOptions();
if (!psos?.[0]?.canResize) {
needSoftwareResize = true;
}
}
catch (e) {
}
if (needSoftwareResize)
options.picture = undefined;
return internalTakePicture();
};
if (this.mixinDeviceInterfaces.includes(ScryptedInterface.Settings)) {
const settings = await this.mixinDevice.getSettings();
username = settings?.find(setting => setting.key === 'username')?.value?.toString();
password = settings?.find(setting => setting.key === 'password')?.value?.toString();
}
else if (this.storageSettings.values.snapshotsFromPrebuffer) {
takePicture = async () => {
throw new PrebufferUnavailableError();
}
let axiosClient: AxiosDigestAuth | AxiosInstance;
if (username && password) {
axiosClient = new AxiosDigestAuth({
username,
password,
});
}
else {
takePicture = () => {
throw new Error('Snapshot Unavailable (snapshotUrl empty)');
}
}
}
else {
if (!this.axiosClient) {
let username: string;
let password: string;
if (this.mixinDeviceInterfaces.includes(ScryptedInterface.Settings)) {
const settings = await this.mixinDevice.getSettings();
username = settings?.find(setting => setting.key === 'username')?.value?.toString();
password = settings?.find(setting => setting.key === 'password')?.value?.toString();
}
if (username && password) {
this.axiosClient = new AxiosDigestAuth({
username,
password,
});
}
else {
this.axiosClient = axios;
}
axiosClient = axios;
}
takePicture = () => this.axiosClient.request({
takePicture = () => axiosClient.request({
httpsAgent,
method: "GET",
responseType: 'arraybuffer',
url: this.storageSettings.values.snapshotUrl,
}).then(async (response: { data: any; }) => response.data);
}
else if (this.mixinDeviceInterfaces.includes(ScryptedInterface.Camera)) {
takePicture = async (options?: RequestPictureOptions) => {
const internalTakePicture = async () => {
if (!options?.id && this.storageSettings.values.defaultSnapshotChannel !== 'Camera Default') {
try {
const psos = await this.getPictureOptions();
const pso = psos.find(pso => pso.name === this.storageSettings.values.defaultSnapshotChannel);
if (!options)
options = {};
options.id = pso.id;
}
catch (e) {
}
}
return this.mixinDevice.takePicture(options).then(mo => mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg'))
}
// full resolution setging ignores resize.
if (this.storageSettings.values.snapshotResolution === 'Full Resolution') {
if (options)
options.picture = undefined;
return internalTakePicture();
}
// if resize wasn't requested, continue as normal.
const resizeRequested = !!options?.picture;
if (!resizeRequested)
return internalTakePicture();
// resize was requested
// crop and scale needs to operate on the full resolution image.
if (this.storageSettings.values.snapshotCropScale?.length) {
options.picture = undefined;
// resize after the cop and scale.
needSoftwareResize = resizeRequested;
return internalTakePicture();
}
// determine see if that can be handled by camera hardware
try {
const psos = await this.getPictureOptions();
if (!psos?.[0]?.canResize) {
needSoftwareResize = true;
}
}
catch (e) {
}
if (needSoftwareResize)
options.picture = undefined;
return internalTakePicture()
.catch(async e => {
// the camera snapshot failed, try to fallback to prebuffer snapshot.
if (usePrebufferSnapshots === false)
throw e;
const fallback = await preparePrebufferSnapshot();
if (!fallback)
throw e;
return fallback();
})
};
}
else if (usePrebufferSnapshots) {
takePicture = async () => {
throw new PrebufferUnavailableError();
}
}
else {
takePicture = () => {
throw new Error('Snapshot Unavailable (snapshotUrl empty)');
}
}
}
const pendingPicture = this.snapshotDebouncer(options, async () => {
@@ -286,34 +319,20 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
}, 60000);
}
catch (e) {
this.console.error('Snapshot failed', e);
// do not mask event snapshots, as they're used for detections and not
// user facing display.
if (eventSnapshot)
throw e;
// allow reusing the current picture to mask errors
picture = await this.createErrorImage(e);
}
return picture;
});
let { snapshotMode } = this.storageSettings.values;
if (eventSnapshot) {
// event snapshots must be fulfilled
snapshotMode = 'Default';
}
else if (snapshotMode === 'Never Wait' && !options?.periodicRequest) {
// non periodic snapshots should use a short timeout.
snapshotMode = 'Timeout';
}
let data: Buffer;
try {
switch (snapshotMode) {
case 'Never Wait':
throw new NeverWaitError();
case 'Timeout':
data = await timeoutPromise(1000, pendingPicture);
break;
default:
data = await pendingPicture;
break;
}
data = await pendingPicture;
}
catch (e) {
// allow reusing the current picture to mask errors
@@ -443,7 +462,9 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
}
async getPictureOptions() {
return this.mixinDevice.getPictureOptions();
if (!this.psos)
this.psos = await this.mixinDevice.getPictureOptions();
return this.psos;
}
getMixinSettings(): Promise<Setting[]> {

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/webrtc",
"version": "0.1.33",
"version": "0.1.34",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/webrtc",
"version": "0.1.33",
"version": "0.1.34",
"dependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/webrtc",
"version": "0.1.33",
"version": "0.1.34",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",

View File

@@ -419,12 +419,13 @@ export class WebRTCPlugin extends AutoenableMixinProvider implements DeviceCreat
}
}
const iceServers = this.storageSettings.values.useTurnServer
? [weriftStunServer, weriftTurnServer]
: [weriftStunServer];
return {
iceUseIpv6: false,
iceServers: [
weriftStunServer,
weriftTurnServer,
],
iceServers,
...ret,
};
}

View File

@@ -530,13 +530,13 @@ class RequestMediaStreamOptions(TypedDict):
container: str
destination: MediaStreamDestination
destinationId: str
directMediaStream: bool
id: str
metadata: Any
name: str
prebuffer: float
prebufferBytes: float
refresh: bool
route: Any | Any
tool: MediaStreamTool
video: VideoStreamOptions
pass
@@ -555,7 +555,6 @@ class RequestRecordingStreamOptions(TypedDict):
container: str
destination: MediaStreamDestination
destinationId: str
directMediaStream: bool
duration: float
id: str
loop: bool
@@ -565,6 +564,7 @@ class RequestRecordingStreamOptions(TypedDict):
prebuffer: float
prebufferBytes: float
refresh: bool
route: Any | Any
startTime: float
tool: MediaStreamTool
video: VideoStreamOptions

View File

@@ -568,12 +568,13 @@ export type MediaStreamDestination = "local" | "remote" | "medium-resolution" |
export interface RequestMediaStreamOptions extends MediaStreamOptions {
/**
* When retrieving media, setting disableMediaProxies=true
* will bypass any intermediaries (NVR, rebroadcast) and retrieve
* it directly from the source. This is useful in cases when
* peer to peer connections are possible and preferred, such as WebRTC.
* When retrieving media, setting route directs how the media should be
* retrieved and exposed. A direct route will get the stream
* as is from the source. This will bypass any intermediaries if possible,
* such as an NVR or restreamers.
* An external route will request that that provided route is exposed to the local network.
*/
directMediaStream?: boolean;
route?: 'external' | 'direct';
/**
* Specify the stream refresh behavior when this stream is requested.

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/server",
"version": "0.6.21",
"version": "0.6.23",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/server",
"version": "0.6.21",
"version": "0.6.23",
"license": "ISC",
"dependencies": {
"@ffmpeg-installer/ffmpeg": "^1.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/server",
"version": "0.6.20",
"version": "0.6.24",
"description": "",
"dependencies": {
"@ffmpeg-installer/ffmpeg": "^1.1.0",
@@ -69,7 +69,7 @@
"build": "tsc --outDir dist",
"postbuild": "node test/check-build-output.js",
"prepublishOnly": "npm version patch && git add package.json && npm run build && git commit -m prepublish",
"postpublish": "git push origin v$npm_package_version",
"postpublish": "git tag v$npm_package_version && git push origin v$npm_package_version",
"docker": "scripts/github-workflow-publish-docker.sh"
},
"author": "",

View File

@@ -1,15 +1,15 @@
import { once } from 'events';
import net from 'net';
export async function listenZero(server: net.Server) {
server.listen(0);
export async function listenZero(server: net.Server, hostname?: string) {
server.listen(0, hostname);
await once(server, 'listening');
return (server.address() as net.AddressInfo).port;
}
export async function listenZeroSingleClient() {
export async function listenZeroSingleClient(hostname?: string) {
const server = new net.Server();
const port = await listenZero(server);
const port = await listenZero(server, hostname);
const clientPromise = new Promise<net.Socket>((resolve, reject) => {
const timeout = setTimeout(() => {
@@ -24,7 +24,7 @@ export async function listenZeroSingleClient() {
});
});
clientPromise.catch(() => {});
clientPromise.catch(() => { });
return {
server,

View File

@@ -10,6 +10,7 @@ function newDeviceProxy(id: string, systemManager: SystemManagerImpl) {
}
class DeviceProxyHandler implements PrimitiveProxyHandler<any>, ScryptedDevice {
customProperties: Map<string | number | symbol, any>;
device: Promise<ScryptedDevice>;
constructor(public id: string, public systemManager: SystemManagerImpl) {
}
@@ -43,10 +44,34 @@ class DeviceProxyHandler implements PrimitiveProxyHandler<any>, ScryptedDevice {
}
}
deleteProperty(target: any, p: string | symbol): boolean {
const prop = p.toString();
if (Object.keys(ScryptedInterfaceProperty).includes(prop))
return false;
this.customProperties ||= new Map();
this.customProperties.set(p, undefined);
return true;
}
set(target: any, p: string | symbol, newValue: any, receiver: any): boolean {
const prop = p.toString();
if (Object.keys(ScryptedInterfaceProperty).includes(prop))
return false;
this.customProperties ||= new Map();
this.customProperties.set(p, newValue);
return true;
}
get(target: any, p: PropertyKey, receiver: any): any {
if (p === 'id')
return this.id;
if (this.customProperties?.has(p))
return this.customProperties.get(p);
const handled = RpcPeer.handleFunctionInvocations(this, target, p, receiver);
if (handled)
return handled;

View File

@@ -171,7 +171,7 @@ class RpcProxy implements PrimitiveProxyHandler<any> {
// todo: error constructor adds a "cause" variable in Chrome 93, Node v??
export class RPCResultError extends Error {
constructor(peer: RpcPeer, message: string, public cause?: Error, options?: { name: string, stack: string | undefined }) {
super(`${peer.selfName}:${peer.peerName}: ${message}`);
super(`${message}\n${peer.selfName}:${peer.peerName}`);
if (options?.name) {
this.name = options?.name;

View File

@@ -194,6 +194,12 @@ async function start() {
// lack of login from cookie auth.
const checkToken = (token: string) => {
if (process.env.SCRYPTED_ADMIN_USERNAME && process.env.SCRYPTED_ADMIN_TOKEN === token) {
res.locals.username = process.env.SCRYPTED_ADMIN_USERNAME;
res.locals.aclId = undefined;
return;
}
const [checkHash, ...tokenParts] = token.split('#');
const tokenPart = tokenParts?.join('#');
if (checkHash && tokenPart) {
@@ -544,8 +550,31 @@ async function start() {
await checkResetLogin();
const hostname = os.hostname()?.split('.')?.[0];
const addresses = ((await scrypted.addressSettings.getLocalAddresses()) || getHostAddresses(true, true)).map(address => `https://${address}:${SCRYPTED_SECURE_PORT}`);
// env/header based admin login
if (res.locals.username && res.locals.username === process.env.SCRYPTED_ADMIN_USERNAME) {
res.send({
username: res.locals.username,
token: process.env.SCRYPTED_ADMIN_TOKEN,
addresses,
hostname,
});
return;
}
// env based anon admin login
if (process.env.SCRYPTED_DISABLE_AUTHENTICATION === 'true') {
res.send({
expiration: ONE_DAY_MILLISECONDS,
username: 'anonymous',
addresses,
hostname,
})
return;
}
// basic auth
if (req.protocol === 'https' && req.headers.authorization) {
const username = await new Promise(resolve => {
const basicChecker = basicAuth.check((req) => {
@@ -570,16 +599,7 @@ async function start() {
return;
}
if (process.env.SCRYPTED_DISABLE_AUTHENTICATION === 'true') {
res.send({
expiration: ONE_DAY_MILLISECONDS,
username: 'anonymous',
addresses,
hostname,
})
return;
}
// cookie auth
try {
const login_user_token = getSignedLoginUserTokenRawValue(req);
if (!login_user_token)