import axios from 'axios'; import { BufferConverter, OauthClient, ScryptedDeviceBase, ScryptedMimeTypes, Setting, Settings } from '@scrypted/sdk'; import qs from 'query-string'; import { GcmRtcManager, GcmRtcConnection } from './legacy'; import { Duplex } from 'stream'; import net from 'net'; import tls from 'tls'; import HttpProxy from 'http-proxy'; import { Server, createServer } from 'http'; import Url from 'url'; import {once} from 'events'; import sdk from '@scrypted/sdk'; const {deviceManager} = sdk; export const DEFAULT_SENDER_ID = '827888101440'; export async function createDefaultRtcManager(): Promise { const manager = await GcmRtcManager.start({ // Scrypted '827888101440': '', }, { iceServers: [ { urls: ["turn:turn0.clockworkmod.com", "turn:n0.clockworkmod.com", "turn:n1.clockworkmod.com"], username: "foo", credential: "bar", }, ], }); return manager; } async function whitelist(localUrl: string, ttl: number): Promise { const local = Url.parse(localUrl); const token_info = localStorage.getItem('token_info'); const q = qs.stringify({ scope: local.path, ttl, }) const scope = await axios(`https://home.scrypted.app/_punch/scope?${q}`, { headers: { Authorization: `Bearer ${token_info}` }, }) const {userToken, userTokenSignature} = scope.data; const tokens = qs.stringify({ user_token: userToken, user_token_signature: userTokenSignature }) const url = `https://home.scrypted.app${local.path}?${tokens}`; return url; } class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings, BufferConverter { manager: GcmRtcManager; server: Server; proxy: HttpProxy; constructor() { super(); this.initialize(); this.fromMimeType = `${ScryptedMimeTypes.LocalUrl};${ScryptedMimeTypes.AcceptUrlParameter}=true`; this.toMimeType = ScryptedMimeTypes.Url; } async convert(data: Buffer|string, fromMimeType: string): Promise { return whitelist(data.toString(), 10 * 365 * 24 * 60 * 60 * 1000); } async getSettings(): Promise { return [ { title: 'Refresh Token', value: this.storage.getItem('token_info'), description: 'Authorization token used by Scrypted Cloud.', readonly: true, } ] } async putSetting(key: string, value: string | number | boolean) { } async getOauthUrl(): Promise { const args = qs.stringify({ registration_id: this.manager.registrationId, sender_id: DEFAULT_SENDER_ID, }) return `https://home.scrypted.app/_punch/login?${args}` } async onOauthCallback(callbackUrl: string) { } async initialize() { this.server = createServer((req, res) => { const url = Url.parse(req.url); if (url.path.startsWith('/web/oauth/callback') && url.query) { const query = qs.parse(url.query); if (!query.callback_url && query.token_info && query.user_info) { localStorage.setItem('token_info', query.token_info as string) res.setHeader('Location', 'https://home.scrypted.app/endpoint/@scrypted/core/public/'); res.writeHead(302); res.end(); return; } } else if (url.path === '/web/') { res.setHeader('Location', 'https://home.scrypted.app/endpoint/@scrypted/core/public/'); res.writeHead(302); res.end(); return; } else if (url.path === '/web/component/home/endpoint') { this.proxy.web(req, res, { target: 'https://localhost:9443/endpoint/@scrypted/google-home/public/', ignorePath: true, secure: false, }); return; } this.proxy.web(req, res, undefined, (err) => console.error(err)); }); this.server.on('upgrade', (req, socket, head) => { this.proxy.ws(req, socket, head, { target: 'wss://localhost:9443', ws: true, secure: false }); }) // this.server = net.createServer(conn => console.log('connectionz')) as any; // listen(0) does not work in a cluster!!! // https://nodejs.org/api/cluster.html#cluster_how_it_works // server.listen(0) Normally, this will cause servers to listen on a random port. // However, in a cluster, each worker will receive the same "random" port each time they // do listen(0). In essence, the port is random the first time, but predictable thereafter. // To listen on a unique port, generate a port number based on the cluster worker ID. this.server.listen(10081 + Math.round(Math.random() * 10000), '127.0.0.1'); await once(this.server, 'listening'); const port = (this.server.address() as any).port; this.proxy = HttpProxy.createProxy({ target: `https://localhost:9443`, secure: false, }); this.proxy.on('error', () => {}) this.manager = await createDefaultRtcManager(); this.manager.listen("http://localhost", (conn: GcmRtcConnection) => { conn.on('socket', async (command: string, socket: Duplex) => { let local: any; await new Promise(resolve => process.nextTick(resolve)); if (true) { local = net.connect({ port, host: '127.0.0.1', }); await new Promise(resolve => process.nextTick(resolve)); } else { local = tls.connect({ port: 9443, host: '127.0.0.1', rejectUnauthorized: false, }) } socket.pipe(local).pipe(socket); }); }) const token_info = localStorage.getItem('token_info'); if (token_info) { const q = qs.stringify({ fcm_registration_id: this.manager.registrationId, sender_id: DEFAULT_SENDER_ID, }) } } } export default new ScryptedCloud();