@xenon-device-management/xenon 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/package.json +3 -3
- package/lib/public/assets/{AnnotationOverlay-C-Pw_hvw.js → AnnotationOverlay-AxcBYoKp.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-DmYQ0oSk.js → ApiKeyGate-CIU02EIx.js} +1 -1
- package/lib/public/assets/{BugReportButton-n8T1dFUp.js → BugReportButton-_akEEjXm.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-Bhk1udmn.js +2 -0
- package/lib/public/assets/{EmptyState-DExETevp.js → EmptyState-DRKRm19_.js} +1 -1
- package/lib/public/assets/{FieldGroup-CCdFlZm6.js → FieldGroup-zQQHPh7x.js} +1 -1
- package/lib/public/assets/{FilterMenu-DiUSOSL-.js → FilterMenu-C9_StnDi.js} +1 -1
- package/lib/public/assets/{Menu-ZOfpH6gh.js → Menu-0J7io6pF.js} +1 -1
- package/lib/public/assets/{Modal-CZNtoB-1.js → Modal-C677ljNX.js} +1 -1
- package/lib/public/assets/{RecordingPage-BKp6h8Jl.js → RecordingPage-BDJFck-L.js} +1 -1
- package/lib/public/assets/{RecordingsPage-Bf881FX4.js → RecordingsPage-DQkimbKK.js} +1 -1
- package/lib/public/assets/{SegmentedControl-EUoyizvW.js → SegmentedControl-DvGMZBIE.js} +1 -1
- package/lib/public/assets/{SettingCard-ClT4hf4_.js → SettingCard-ogyouYBq.js} +1 -1
- package/lib/public/assets/{Table-ucR4bnmc.js → Table-BLFCOLlO.js} +1 -1
- package/lib/public/assets/{activity-K-hdylwq.js → activity-BU55TGRG.js} +1 -1
- package/lib/public/assets/{ai-settings-BLhdwI3c.js → ai-settings-GpDAiHav.js} +1 -1
- package/lib/public/assets/{api-keys-BbiN7rBI.js → api-keys-dOzP0k8_.js} +1 -1
- package/lib/public/assets/{apps-Dez1petz.js → apps-mAwAQmkn.js} +1 -1
- package/lib/public/assets/{arrow-left-UjmFHLAJ.js → arrow-left-Dg9mxe9T.js} +1 -1
- package/lib/public/assets/{arrow-right-BVyEigbu.js → arrow-right-BVcmUyq3.js} +1 -1
- package/lib/public/assets/{arrow-up-right-B-ZlBof3.js → arrow-up-right-ByR5pZrH.js} +1 -1
- package/lib/public/assets/{auth-shell-DAQjAgUv.js → auth-shell-Jpgk8CgY.js} +2 -2
- package/lib/public/assets/{builds-page-CmIPa5JK.js → builds-page-VnRTrcdl.js} +1 -1
- package/lib/public/assets/{button-Dd-IhMC1.js → button-D-X4wjO8.js} +1 -1
- package/lib/public/assets/{calendar-CAIeJwa5.js → calendar-ur_p5ETA.js} +1 -1
- package/lib/public/assets/{check-C4Jrjg8f.js → check-K33iMeyq.js} +1 -1
- package/lib/public/assets/{chevron-left-BGzlf-4i.js → chevron-left-DMVWShd-.js} +1 -1
- package/lib/public/assets/{chevron-right-_wYqDjDC.js → chevron-right-DphLJ2_X.js} +1 -1
- package/lib/public/assets/{circle-check-Ckn00yMB.js → circle-check-CNFhu8oE.js} +1 -1
- package/lib/public/assets/{circle-x-pdVkomLa.js → circle-x-BH_cUbqZ.js} +1 -1
- package/lib/public/assets/{clock-B8vmwkHj.js → clock-CVMVdVvp.js} +1 -1
- package/lib/public/assets/{copy-BAqnFI-F.js → copy-CZ2XHafL.js} +1 -1
- package/lib/public/assets/{device-explorer-CYq_4F_N.js → device-explorer-B94BhaqF.js} +52 -52
- package/lib/public/assets/{download-B6UEAp_1.js → download-UUiyw7kO.js} +1 -1
- package/lib/public/assets/{forgot-password-CLqNvmrN.js → forgot-password-CrS14F79.js} +1 -1
- package/lib/public/assets/{funnel-Da90BBbT.js → funnel-BMOE5qJY.js} +1 -1
- package/lib/public/assets/{index-BuIIcmGp.js → index-2XPPbPwy.js} +43 -43
- package/lib/public/assets/{index-MX7IzqyW.js → index-D0R9Yic3.js} +1 -1
- package/lib/public/assets/{input-Bq9aY-We.js → input-C-1b8J7i.js} +1 -1
- package/lib/public/assets/{keycodes-CIhXIJyP.js → keycodes-Cly7dQc9.js} +4 -4
- package/lib/public/assets/{layers-bXjpswQI.js → layers-DFZfqGdX.js} +1 -1
- package/lib/public/assets/{lock-pJ72y5Fy.js → lock-ylHMJpfP.js} +1 -1
- package/lib/public/assets/{login-UOcHALV2.js → login-DWsWKqDx.js} +1 -1
- package/lib/public/assets/{maintenance-settings-XnJgp5yb.js → maintenance-settings-9B6_MTqW.js} +1 -1
- package/lib/public/assets/{monitor-QkAhI1Jx.js → monitor-CzX0hBH1.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-B42Da0m4.js → mouse-pointer-2-CZHD7N3z.js} +1 -1
- package/lib/public/assets/{network-CKVqcyp6.js → network-Bikc4MyT.js} +1 -1
- package/lib/public/assets/{overview-BORGkN3N.js → overview-DzaA0aMe.js} +1 -1
- package/lib/public/assets/{page-header-CKWFOjOI.js → page-header-DqGlDufy.js} +1 -1
- package/lib/public/assets/{plus-BpFKVdC8.js → plus-Ct6aQTGM.js} +1 -1
- package/lib/public/assets/{profile-page-CcbKBicY.js → profile-page-CCnUQ1Qk.js} +1 -1
- package/lib/public/assets/{reset-password-BA0W9bxl.js → reset-password-AmkGGXo5.js} +1 -1
- package/lib/public/assets/{runbook-page-Cp7ZBq3s.js → runbook-page-DQQn9VyE.js} +1 -1
- package/lib/public/assets/{select-DIZWIoVR.js → select-k5ogtEWW.js} +1 -1
- package/lib/public/assets/{selector-detail-page-C_4YuvAp.js → selector-detail-page-BCcwC5ku.js} +1 -1
- package/lib/public/assets/{selector-health-CXjD_Xth.js → selector-health-BVyX6kIh.js} +1 -1
- package/lib/public/assets/{selector-health-page-CTqIHeoG.js → selector-health-page-CElBB6XT.js} +1 -1
- package/lib/public/assets/{session-detail-page-B0KYkk8z.js → session-detail-page-6bqo_-ND.js} +1 -1
- package/lib/public/assets/{settings-BcRBKUXd.js → settings-DKBj9KyN.js} +1 -1
- package/lib/public/assets/{status-pill-outline-Dh2PXSf_.js → status-pill-outline-CZNpvqej.js} +1 -1
- package/lib/public/assets/{tablet-_sXqZztt.js → tablet-B6HI5OxV.js} +1 -1
- package/lib/public/assets/{teams-DGHT8MKm.js → teams-owoDHbOU.js} +1 -1
- package/lib/public/assets/{trash-2-BV88cV3Z.js → trash-2-D61PoUrZ.js} +1 -1
- package/lib/public/assets/{upload-CIDH9wkj.js → upload-1ZYEVvgD.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DYGuSNwl.js → use-password-reset-mode-Dx0ghiiQ.js} +1 -1
- package/lib/public/assets/{users-7yviqflc.js → users-C5CS5Qez.js} +1 -1
- package/lib/public/assets/{users-BUE7MoTq.js → users-PbGMd5sD.js} +1 -1
- package/lib/public/assets/{video-off-BTRxtlGo.js → video-off-_XusI0HK.js} +1 -1
- package/lib/public/assets/{webhook-settings-DMKD9Pgm.js → webhook-settings-Bi_HHQlQ.js} +1 -1
- package/lib/public/assets/{zap-CWE94qcK.js → zap-uzHWFAKI.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/app/routers/control.js +119 -203
- package/lib/src/app/routers/nodePhoneControl.js +520 -0
- package/lib/src/app/ws/h264StreamWs.js +30 -4
- package/lib/src/app/ws/logcatWs.js +36 -7
- package/lib/src/app/ws/nodeSocketRelay.js +167 -0
- package/lib/src/data-service/device-store.js +17 -2
- package/lib/src/data-service/deviceClaims.js +32 -0
- package/lib/src/data-service/deviceFieldOwners.js +3 -1
- package/lib/src/data-service/prisma-store.js +9 -1
- package/lib/src/device-managers/AndroidDeviceManager.js +38 -28
- package/lib/src/device-managers/android/AndroidH264StreamService.js +10 -2
- package/lib/src/device-managers/android/AndroidStreamService.js +3 -1
- package/lib/src/device-managers/ios/IOSStreamService.js +4 -2
- package/lib/src/gateway/forwardToNode.js +10 -3
- package/lib/src/generated/client/edge.js +5 -4
- package/lib/src/generated/client/index-browser.js +2 -1
- package/lib/src/generated/client/index.d.ts +36 -1
- package/lib/src/generated/client/index.js +5 -4
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +4 -0
- package/lib/src/generated/client/wasm.js +2 -1
- package/lib/src/middleware/deviceAccessGuard.js +4 -1
- package/lib/src/services/ServerManager.js +13 -0
- package/lib/src/services/device-access/ticketActorAccess.js +3 -1
- package/lib/src/services/lease/LeaseOrphanSweeper.js +4 -2
- package/lib/test/helpers/own-node-id.js +29 -0
- package/lib/test/setup/scratch-default-database.js +32 -0
- package/lib/test/unit/AndroidDeviceManager.spec.js +60 -0
- package/lib/test/unit/android-h264-service.spec.js +18 -0
- package/lib/test/unit/clipboard-route.spec.js +4 -0
- package/lib/test/unit/control-forward-decision.spec.js +17 -17
- package/lib/test/unit/device-access-guard.spec.js +38 -0
- package/lib/test/unit/hub-node-control-forward.spec.js +374 -1
- package/lib/test/unit/hub-node-race.spec.js +51 -0
- package/lib/test/unit/install-repository-app-team.spec.js +4 -0
- package/lib/test/unit/lease/lease-sweeper-expiry.spec.js +15 -0
- package/lib/test/unit/logcat-ws.spec.js +8 -0
- package/lib/test/unit/mark-node-busy-loki.spec.js +66 -0
- package/lib/test/unit/node-phone-control.spec.js +225 -0
- package/lib/test/unit/node-socket-relay.spec.js +357 -0
- package/lib/test/unit/screenshot-route.spec.js +104 -0
- package/lib/test/unit/stream-leave-route.spec.js +4 -0
- package/lib/test/unit/stream-stop-ownership.spec.js +4 -0
- package/lib/test/unit/stream-stop-recording.spec.js +4 -0
- package/lib/test/unit/stream-ticket-identity.spec.js +11 -1
- package/lib/test/unit/stream-watchdog-timers.spec.js +46 -0
- package/lib/test/unit/upload-install-route.spec.js +205 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/prisma/migrations/20261001120000_device_node_hold/migration.sql +2 -0
- package/prisma/schema.prisma +4 -0
- package/lib/public/assets/DeviceMosaicView-D-rGOBbJ.js +0 -2
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
|
|
12
|
+
var __asyncValues = (this && this.__asyncValues) || function (o) {
|
|
13
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
14
|
+
var m = o[Symbol.asyncIterator], i;
|
|
15
|
+
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
|
|
16
|
+
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
|
|
17
|
+
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
|
18
|
+
};
|
|
19
|
+
var __asyncDelegator = (this && this.__asyncDelegator) || function (o) {
|
|
20
|
+
var i, p;
|
|
21
|
+
return i = {}, verb("next"), verb("throw", function (e) { throw e; }), verb("return"), i[Symbol.iterator] = function () { return this; }, i;
|
|
22
|
+
function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: false } : f ? f(v) : v; } : f; }
|
|
23
|
+
};
|
|
24
|
+
var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
|
|
25
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
26
|
+
var g = generator.apply(thisArg, _arguments || []), i, q = [];
|
|
27
|
+
return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;
|
|
28
|
+
function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }
|
|
29
|
+
function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }
|
|
30
|
+
function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
|
|
31
|
+
function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
|
|
32
|
+
function fulfill(value) { resume("next", value); }
|
|
33
|
+
function reject(value) { resume("throw", value); }
|
|
34
|
+
function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
|
|
35
|
+
};
|
|
36
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
37
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
|
+
};
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.NODE_INSTALL_TIMEOUT_MS = exports.NODE_CONTROL_TIMEOUT_MS = exports.ANSWERED_HERE_FOR_NODES = exports.ANSWERED_HERE = exports.NODE_FORWARDED_CONTROL = void 0;
|
|
41
|
+
exports.safeNodeOrigin = safeNodeOrigin;
|
|
42
|
+
exports.nodeControlUrl = nodeControlUrl;
|
|
43
|
+
exports.isOtherServersPhone = isOtherServersPhone;
|
|
44
|
+
exports.nodePhoneControl = nodePhoneControl;
|
|
45
|
+
exports.installFileOnNode = installFileOnNode;
|
|
46
|
+
exports.screenshotFromNode = screenshotFromNode;
|
|
47
|
+
const crypto_1 = require("crypto");
|
|
48
|
+
const fs_1 = __importDefault(require("fs"));
|
|
49
|
+
const stream_1 = require("stream");
|
|
50
|
+
const typedi_1 = require("typedi");
|
|
51
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
52
|
+
const PluginContext_1 = require("../../PluginContext");
|
|
53
|
+
const localDeviceHosts_1 = require("../../device-managers/localDeviceHosts");
|
|
54
|
+
const actor_1 = require("../../services/device-access/actor");
|
|
55
|
+
const hubSessionToken_1 = require("../../gateway/hubSessionToken");
|
|
56
|
+
const forwardToNode_1 = require("../../gateway/forwardToNode");
|
|
57
|
+
const manualLock_1 = require("../../services/recording/manualLock");
|
|
58
|
+
const SessionOwnerResolver_1 = require("../../services/device-access/SessionOwnerResolver");
|
|
59
|
+
const device_store_1 = require("../../data-service/device-store");
|
|
60
|
+
const deviceAccessPolicy_1 = require("../../services/device-access/deviceAccessPolicy");
|
|
61
|
+
const controlDevice_1 = require("../../middleware/controlDevice");
|
|
62
|
+
/**
|
|
63
|
+
* /control on another server's phone: a node's, or a cloud provider's.
|
|
64
|
+
*
|
|
65
|
+
* The handlers in control.ts act on this server's own phones, with this
|
|
66
|
+
* server's adb and go-ios. On a hub they used to run for a node's phone too,
|
|
67
|
+
* against a phone the hub doesn't have: only five actions were sent on to the
|
|
68
|
+
* node. This sits after the team and ownership guards and decides every
|
|
69
|
+
* request for another server's phone before any handler runs, so an action
|
|
70
|
+
* added later is refused for it until someone puts it on a list below.
|
|
71
|
+
*/
|
|
72
|
+
/**
|
|
73
|
+
* Sent on to the phone's node, and the node's answer relayed unchanged. Each
|
|
74
|
+
* is about the phone alone. The preview's are too: the node holds the phone
|
|
75
|
+
* for the preview, counts its viewers and releases it, by its own rules and
|
|
76
|
+
* with the hub's user, so none of that is kept here.
|
|
77
|
+
*/
|
|
78
|
+
exports.NODE_FORWARDED_CONTROL = [
|
|
79
|
+
'POST tap',
|
|
80
|
+
'POST swipe',
|
|
81
|
+
'POST text',
|
|
82
|
+
'POST keyevent',
|
|
83
|
+
'POST touchandhold',
|
|
84
|
+
'GET screenshot',
|
|
85
|
+
'GET clipboard',
|
|
86
|
+
'POST clipboard',
|
|
87
|
+
'POST lock',
|
|
88
|
+
'POST unlock',
|
|
89
|
+
'GET display',
|
|
90
|
+
'POST uninstall',
|
|
91
|
+
'GET apps',
|
|
92
|
+
'GET logs',
|
|
93
|
+
'POST shell',
|
|
94
|
+
'GET inspector/snapshot',
|
|
95
|
+
'POST stream/start',
|
|
96
|
+
'GET stream/status',
|
|
97
|
+
'POST stream/leave',
|
|
98
|
+
'POST stream/stop',
|
|
99
|
+
// The MJPEG stream itself: relayed for as long as the viewer watches.
|
|
100
|
+
'GET stream',
|
|
101
|
+
// The upload is streamed on as it came: nothing here reads it first.
|
|
102
|
+
'POST upload-install',
|
|
103
|
+
];
|
|
104
|
+
/**
|
|
105
|
+
* Answered by this server's own handler.
|
|
106
|
+
* - appium-session: the session runs through this server, which routes its
|
|
107
|
+
* commands, so its id and base path are this server's.
|
|
108
|
+
* - stream/ticket: the viewer's ticket is this server's, minted after its
|
|
109
|
+
* team check. The stream it opens is relayed from the node: GET stream
|
|
110
|
+
* above, and the H.264 and logcat sockets (src/app/ws/nodeSocketRelay.ts),
|
|
111
|
+
* which get a ticket of the node's own.
|
|
112
|
+
*/
|
|
113
|
+
exports.ANSWERED_HERE = ['GET appium-session', 'POST stream/ticket'];
|
|
114
|
+
/**
|
|
115
|
+
* Answered by this server's own handler for a node's phone (a cloud
|
|
116
|
+
* provider's is refused), which does its part here and asks the node for
|
|
117
|
+
* the rest:
|
|
118
|
+
* - install-repository-app: the app is in this server's library, checked
|
|
119
|
+
* against the caller's teams here, and sent to the node's upload-install
|
|
120
|
+
* (installFileOnNode).
|
|
121
|
+
* - omni-scan, test-locator: Omni-Vision runs here, with this server's AI
|
|
122
|
+
* settings, on the node's screenshot (screenshotFromNode).
|
|
123
|
+
*/
|
|
124
|
+
exports.ANSWERED_HERE_FOR_NODES = [
|
|
125
|
+
'POST install-repository-app',
|
|
126
|
+
'GET omni-scan',
|
|
127
|
+
'POST test-locator',
|
|
128
|
+
];
|
|
129
|
+
// Not on any list, so refused: install takes a path, which names a file on
|
|
130
|
+
// one machine only.
|
|
131
|
+
/**
|
|
132
|
+
* A node that hasn't started answering in this long is treated as gone. Once
|
|
133
|
+
* it has, the answer may take as long as it takes: a stream lasts as long as
|
|
134
|
+
* its viewer.
|
|
135
|
+
*/
|
|
136
|
+
exports.NODE_CONTROL_TIMEOUT_MS = 60000;
|
|
137
|
+
/**
|
|
138
|
+
* An install is answered when it is done, which can take minutes (a large
|
|
139
|
+
* app, a slow phone): its node gets this long to start answering instead.
|
|
140
|
+
*/
|
|
141
|
+
exports.NODE_INSTALL_TIMEOUT_MS = 15 * 60000;
|
|
142
|
+
/** Forwarded actions that are installs, and get NODE_INSTALL_TIMEOUT_MS. */
|
|
143
|
+
const INSTALLS = ['POST upload-install'];
|
|
144
|
+
// Cloud metadata endpoints — never proxy to these regardless of caller.
|
|
145
|
+
const FORBIDDEN_PROXY_HOSTS = new Set([
|
|
146
|
+
'169.254.169.254', // AWS/Azure/GCP IMDS
|
|
147
|
+
'metadata.google.internal',
|
|
148
|
+
'metadata.goog',
|
|
149
|
+
'100.100.100.200', // Alibaba ECS metadata
|
|
150
|
+
'fd00:ec2::254', // AWS IMDSv6
|
|
151
|
+
]);
|
|
152
|
+
/**
|
|
153
|
+
* The node's origin from the phone's reported host: scheme, host and port
|
|
154
|
+
* only, dropping any path, query or fragment the host string carried. Null
|
|
155
|
+
* for a cloud-metadata target or a scheme other than http(s).
|
|
156
|
+
*/
|
|
157
|
+
function safeNodeOrigin(deviceHost) {
|
|
158
|
+
let parsed;
|
|
159
|
+
try {
|
|
160
|
+
parsed = new URL(deviceHost);
|
|
161
|
+
}
|
|
162
|
+
catch (_a) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
166
|
+
return null;
|
|
167
|
+
if (FORBIDDEN_PROXY_HOSTS.has(parsed.hostname))
|
|
168
|
+
return null;
|
|
169
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
170
|
+
}
|
|
171
|
+
/** The node's own URL for this request, or null if the host is unsafe. */
|
|
172
|
+
function nodeControlUrl(deviceHost, req) {
|
|
173
|
+
const origin = safeNodeOrigin(deviceHost);
|
|
174
|
+
if (!origin)
|
|
175
|
+
return null;
|
|
176
|
+
const forwardPath = req.originalUrl.startsWith('/') ? req.originalUrl : `/${req.originalUrl}`;
|
|
177
|
+
return `${origin}${withoutTicket(forwardPath)}`;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* The path without a `ticket` query parameter. A viewer's ticket is this
|
|
181
|
+
* server's, already spent here, and not the node's business: the node gets
|
|
182
|
+
* the hub's token instead.
|
|
183
|
+
*/
|
|
184
|
+
function withoutTicket(path) {
|
|
185
|
+
const q = path.indexOf('?');
|
|
186
|
+
if (q < 0)
|
|
187
|
+
return path;
|
|
188
|
+
const params = new URLSearchParams(path.slice(q + 1));
|
|
189
|
+
if (!params.has('ticket'))
|
|
190
|
+
return path;
|
|
191
|
+
params.delete('ticket');
|
|
192
|
+
const query = params.toString();
|
|
193
|
+
return query ? `${path.slice(0, q)}?${query}` : path.slice(0, q);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* A phone another server drives. Decided by the phone's own row (its nodeId,
|
|
197
|
+
* else its exact host against this server's own hosts), never by the Host
|
|
198
|
+
* header the caller used: a substring match on that header forwarded a lab's
|
|
199
|
+
* own phone back to itself, with no login, whenever the lab was reached as
|
|
200
|
+
* `localhost`.
|
|
201
|
+
*/
|
|
202
|
+
function isOtherServersPhone(device) {
|
|
203
|
+
if (!device.host)
|
|
204
|
+
return false;
|
|
205
|
+
const context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
206
|
+
return !(0, localDeviceHosts_1.isOwnDevice)((0, localDeviceHosts_1.localDeviceHosts)(context.pluginArgs, context.port), context.nodeId, device);
|
|
207
|
+
}
|
|
208
|
+
function originOf(host) {
|
|
209
|
+
try {
|
|
210
|
+
return new URL(host).origin;
|
|
211
|
+
}
|
|
212
|
+
catch (_a) {
|
|
213
|
+
return host;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function refuse(res, error, message) {
|
|
217
|
+
return res.status(501).json({ success: false, error, message });
|
|
218
|
+
}
|
|
219
|
+
function nodePhoneControl(deps = {}) {
|
|
220
|
+
var _a, _b, _c, _d, _e, _f;
|
|
221
|
+
const findDevice = (_a = deps.findDevice) !== null && _a !== void 0 ? _a : controlDevice_1.findControlDeviceInStore;
|
|
222
|
+
const forwardDeps = {
|
|
223
|
+
timeoutMs: (_b = deps.timeoutMs) !== null && _b !== void 0 ? _b : exports.NODE_CONTROL_TIMEOUT_MS,
|
|
224
|
+
installTimeoutMs: (_c = deps.installTimeoutMs) !== null && _c !== void 0 ? _c : exports.NODE_INSTALL_TIMEOUT_MS,
|
|
225
|
+
markNodeBusy: (_d = deps.markNodeBusy) !== null && _d !== void 0 ? _d : ((udid, host, hold) => device_store_1.DeviceStoreFactory.getStore().markNodeBusy(udid, host, hold)),
|
|
226
|
+
describeHolder: (_e = deps.describeHolder) !== null && _e !== void 0 ? _e : ((id) => typedi_1.Container.get(SessionOwnerResolver_1.SessionOwnerResolver).displayName(id)),
|
|
227
|
+
controlToken: (_f = deps.controlToken) !== null && _f !== void 0 ? _f : defaultControlToken,
|
|
228
|
+
};
|
|
229
|
+
return function (req, res, next) {
|
|
230
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
231
|
+
var _a;
|
|
232
|
+
const action = (0, controlDevice_1.controlAction)(req);
|
|
233
|
+
if (!action)
|
|
234
|
+
return next();
|
|
235
|
+
const udid = (0, controlDevice_1.readControlUdid)(req, res, 'nodePhoneControl');
|
|
236
|
+
if (udid === null)
|
|
237
|
+
return;
|
|
238
|
+
if (!udid)
|
|
239
|
+
return next();
|
|
240
|
+
// The guards' memoized lookup: usually no query of its own.
|
|
241
|
+
let device;
|
|
242
|
+
try {
|
|
243
|
+
device = yield (0, controlDevice_1.lookupControlDevice)(res, udid, findDevice);
|
|
244
|
+
}
|
|
245
|
+
catch (e) {
|
|
246
|
+
// Falling through would let the handler, with its own lookup, run a
|
|
247
|
+
// node's phone here: fail closed.
|
|
248
|
+
// Answered as every /control lookup failure is.
|
|
249
|
+
logger_1.default.error(`nodePhoneControl: device lookup failed for ${udid}: ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
|
|
250
|
+
return res.status(503).json((0, deviceAccessPolicy_1.ownershipUnavailableBody)());
|
|
251
|
+
}
|
|
252
|
+
if (!device || !isOtherServersPhone(device))
|
|
253
|
+
return next();
|
|
254
|
+
const key = `${req.method} ${action}`;
|
|
255
|
+
if (exports.ANSWERED_HERE.includes(key))
|
|
256
|
+
return next();
|
|
257
|
+
if (device.cloud) {
|
|
258
|
+
return refuse(res, 'not_available_for_cloud_phone', "This phone is a cloud provider's. Device control isn't available for it here.");
|
|
259
|
+
}
|
|
260
|
+
if (exports.ANSWERED_HERE_FOR_NODES.includes(key))
|
|
261
|
+
return next();
|
|
262
|
+
const node = originOf(device.host);
|
|
263
|
+
if (!exports.NODE_FORWARDED_CONTROL.includes(key)) {
|
|
264
|
+
return refuse(res, 'not_available_through_hub', `This phone is on node ${node}, and the hub doesn't pass ${action} on to nodes yet.`);
|
|
265
|
+
}
|
|
266
|
+
return forward(req, res, { udid, device, node }, forwardDeps);
|
|
267
|
+
});
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
const defaultControlToken = (grant) => typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).controlTokenFor(grant);
|
|
271
|
+
/**
|
|
272
|
+
* The headers of a call to the phone's node, signed for the caller. Null
|
|
273
|
+
* token when this server can't sign (it warns once): the call goes without,
|
|
274
|
+
* which a node with auth disabled accepts and one with auth enabled refuses.
|
|
275
|
+
*/
|
|
276
|
+
function signedHeaders(req, udid, device, controlToken) {
|
|
277
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
278
|
+
var _a;
|
|
279
|
+
const actor = (0, actor_1.resolveActor)(req);
|
|
280
|
+
const headers = {
|
|
281
|
+
accept: (_a = req.headers.accept) !== null && _a !== void 0 ? _a : 'application/json',
|
|
282
|
+
'accept-encoding': 'identity',
|
|
283
|
+
};
|
|
284
|
+
const token = actor.userId
|
|
285
|
+
? yield controlToken({
|
|
286
|
+
userId: actor.userId,
|
|
287
|
+
isAdmin: actor.isAdmin,
|
|
288
|
+
udid,
|
|
289
|
+
host: device.host,
|
|
290
|
+
})
|
|
291
|
+
: null;
|
|
292
|
+
if (token)
|
|
293
|
+
headers[hubSessionToken_1.HUB_TOKEN_HEADER] = token;
|
|
294
|
+
return headers;
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Send one call to the node and relay its answer to `res`. A node that can't
|
|
299
|
+
* be reached is 502, and one that hasn't started answering in `timeoutMs` is
|
|
300
|
+
* 504; once it has, the answer takes as long as it takes. `onAnswer` sees the
|
|
301
|
+
* node's answer first and may reply itself (returning true). Sent once: a
|
|
302
|
+
* retry after a slow node's timeout or 5xx could land the same tap twice.
|
|
303
|
+
*/
|
|
304
|
+
function relayNodeCall(res, call, onAnswer) {
|
|
305
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
306
|
+
var _a, _b, _c;
|
|
307
|
+
const controller = new AbortController();
|
|
308
|
+
let timedOut = false;
|
|
309
|
+
const timer = setTimeout(() => {
|
|
310
|
+
timedOut = true;
|
|
311
|
+
controller.abort();
|
|
312
|
+
}, call.timeoutMs);
|
|
313
|
+
// The caller left: stop waiting for the node on their behalf.
|
|
314
|
+
const onClose = () => {
|
|
315
|
+
if (!res.writableFinished)
|
|
316
|
+
controller.abort();
|
|
317
|
+
};
|
|
318
|
+
res.on('close', onClose);
|
|
319
|
+
try {
|
|
320
|
+
const upstream = yield (0, forwardToNode_1.sendToNode)({
|
|
321
|
+
url: call.url,
|
|
322
|
+
method: call.method,
|
|
323
|
+
headers: call.headers,
|
|
324
|
+
body: call.body,
|
|
325
|
+
signal: controller.signal,
|
|
326
|
+
});
|
|
327
|
+
clearTimeout(timer);
|
|
328
|
+
if (onAnswer && (yield onAnswer(upstream)))
|
|
329
|
+
return;
|
|
330
|
+
yield (0, forwardToNode_1.relayAnswer)(upstream, res, false);
|
|
331
|
+
}
|
|
332
|
+
catch (e) {
|
|
333
|
+
if (res.headersSent || res.destroyed) {
|
|
334
|
+
res.destroy();
|
|
335
|
+
}
|
|
336
|
+
else if (timedOut) {
|
|
337
|
+
res.status(504).json({
|
|
338
|
+
success: false,
|
|
339
|
+
error: 'node_timeout',
|
|
340
|
+
message: `Node ${call.node} didn't answer within ${Math.round(call.timeoutMs / 1000)} s.`,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
logger_1.default.warn(`nodePhoneControl: node ${call.node} unreachable for ${call.udid}: ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
|
|
345
|
+
res.status(502).json({
|
|
346
|
+
success: false,
|
|
347
|
+
error: 'node_unreachable',
|
|
348
|
+
message: `Xenon could not reach node ${call.node} for this phone: ${(_c = (_b = e === null || e === void 0 ? void 0 : e.code) !== null && _b !== void 0 ? _b : e === null || e === void 0 ? void 0 : e.message) !== null && _c !== void 0 ? _c : e}`,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
clearTimeout(timer);
|
|
354
|
+
res.off('close', onClose);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
function forward(req, res, target, deps) {
|
|
359
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
360
|
+
var _a;
|
|
361
|
+
const { udid, device, node } = target;
|
|
362
|
+
const url = nodeControlUrl(device.host, req);
|
|
363
|
+
if (!url)
|
|
364
|
+
return res.status(400).json({ error: 'Unsafe device host' });
|
|
365
|
+
const key = `${req.method} ${(0, controlDevice_1.controlAction)(req)}`;
|
|
366
|
+
const actor = (0, actor_1.resolveActor)(req);
|
|
367
|
+
const headers = yield signedHeaders(req, udid, device, deps.controlToken);
|
|
368
|
+
let body;
|
|
369
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
370
|
+
const type = req.headers['content-type'];
|
|
371
|
+
if (typeof type === 'string' && type.toLowerCase().startsWith('multipart/')) {
|
|
372
|
+
// An upload, streamed on as it came: the route's own parser runs after
|
|
373
|
+
// this, so nothing here has read it.
|
|
374
|
+
body = req;
|
|
375
|
+
headers['content-type'] = type;
|
|
376
|
+
if (req.headers['content-length'])
|
|
377
|
+
headers['content-length'] = req.headers['content-length'];
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
body = Buffer.from(JSON.stringify((_a = req.body) !== null && _a !== void 0 ? _a : {}));
|
|
381
|
+
headers['content-type'] = 'application/json';
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
logger_1.default.debug(`Forwarding ${key} for ${udid} to node ${node}`);
|
|
385
|
+
const timeoutMs = INSTALLS.includes(key) ? deps.installTimeoutMs : deps.timeoutMs;
|
|
386
|
+
yield relayNodeCall(res, { url, method: req.method, headers, body, timeoutMs, node, udid }, (upstream) => __awaiter(this, void 0, void 0, function* () {
|
|
387
|
+
var _a;
|
|
388
|
+
const status = (_a = upstream.statusCode) !== null && _a !== void 0 ? _a : 502;
|
|
389
|
+
if (key === 'POST stream/start' && status < 300) {
|
|
390
|
+
// The node holds the phone for the preview now, and says so in its
|
|
391
|
+
// next report, up to an interval away. Until then this server could
|
|
392
|
+
// hand the phone to a session the node would refuse, and would show
|
|
393
|
+
// no holder. The node writes the same hold for the same user.
|
|
394
|
+
const hold = actor.userId ? (0, manualLock_1.formatManualLock)(actor.userId, udid) : undefined;
|
|
395
|
+
yield deps.markNodeBusy(udid, device.host, hold).catch((e) => {
|
|
396
|
+
var _a;
|
|
397
|
+
logger_1.default.warn(`nodePhoneControl: could not mark ${udid} busy for its node: ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
if (status === 409) {
|
|
401
|
+
yield relayRefusal(yield (0, forwardToNode_1.readAnswer)(upstream), res, deps.describeHolder);
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
return false;
|
|
405
|
+
}));
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* A file as a multipart upload in one field, streamed from disk, with its
|
|
410
|
+
* length known so the node's parser can apply its limit up front.
|
|
411
|
+
*/
|
|
412
|
+
function multipartFile(field, file) {
|
|
413
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
414
|
+
const boundary = `----xenon-${(0, crypto_1.randomBytes)(12).toString('hex')}`;
|
|
415
|
+
const name = file.name.replace(/["\\\r\n]/g, '_');
|
|
416
|
+
const head = Buffer.from(`--${boundary}\r\n` +
|
|
417
|
+
`Content-Disposition: form-data; name="${field}"; filename="${name}"\r\n` +
|
|
418
|
+
'Content-Type: application/octet-stream\r\n\r\n');
|
|
419
|
+
const tail = Buffer.from(`\r\n--${boundary}--\r\n`);
|
|
420
|
+
const { size } = yield fs_1.default.promises.stat(file.path);
|
|
421
|
+
const stream = stream_1.Readable.from((function () {
|
|
422
|
+
return __asyncGenerator(this, arguments, function* () {
|
|
423
|
+
yield yield __await(head);
|
|
424
|
+
yield __await(yield* __asyncDelegator(__asyncValues(fs_1.default.createReadStream(file.path))));
|
|
425
|
+
yield yield __await(tail);
|
|
426
|
+
});
|
|
427
|
+
})());
|
|
428
|
+
return {
|
|
429
|
+
type: `multipart/form-data; boundary=${boundary}`,
|
|
430
|
+
length: head.length + size + tail.length,
|
|
431
|
+
stream,
|
|
432
|
+
};
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Install a file of this server's on another server's phone: sent to the
|
|
437
|
+
* node's upload-install as a multipart upload, signed for the caller, and
|
|
438
|
+
* the node's answer relayed. install-repository-app's, for an app from this
|
|
439
|
+
* server's library, which the node doesn't have. The file's name keeps the
|
|
440
|
+
* extension the node's installer reads.
|
|
441
|
+
*/
|
|
442
|
+
function installFileOnNode(req_1, res_1, udid_1, device_1, file_1) {
|
|
443
|
+
return __awaiter(this, arguments, void 0, function* (req, res, udid, device, file, deps = {}) {
|
|
444
|
+
var _a, _b;
|
|
445
|
+
const origin = safeNodeOrigin(device.host);
|
|
446
|
+
if (!origin) {
|
|
447
|
+
res.status(400).json({ error: 'Unsafe device host' });
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const headers = yield signedHeaders(req, udid, device, (_a = deps.controlToken) !== null && _a !== void 0 ? _a : defaultControlToken);
|
|
451
|
+
const upload = yield multipartFile('app', file);
|
|
452
|
+
headers['content-type'] = upload.type;
|
|
453
|
+
headers['content-length'] = upload.length;
|
|
454
|
+
logger_1.default.info(`Sending ${file.name} to node ${origin} to install on ${udid}`);
|
|
455
|
+
yield relayNodeCall(res, {
|
|
456
|
+
url: `${origin}/xenon/api/control/${encodeURIComponent(udid)}/upload-install`,
|
|
457
|
+
method: 'POST',
|
|
458
|
+
headers,
|
|
459
|
+
body: upload.stream,
|
|
460
|
+
timeoutMs: (_b = deps.timeoutMs) !== null && _b !== void 0 ? _b : exports.NODE_INSTALL_TIMEOUT_MS,
|
|
461
|
+
node: origin,
|
|
462
|
+
udid,
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* The node's screenshot of another server's phone, base64, for work this
|
|
468
|
+
* server does on it: Omni-Vision, with this server's AI settings. Throws when
|
|
469
|
+
* the node doesn't give one.
|
|
470
|
+
*/
|
|
471
|
+
function screenshotFromNode(req_1, udid_1, device_1) {
|
|
472
|
+
return __awaiter(this, arguments, void 0, function* (req, udid, device, deps = {}) {
|
|
473
|
+
var _a, _b, _c;
|
|
474
|
+
const origin = safeNodeOrigin(device.host);
|
|
475
|
+
if (!origin)
|
|
476
|
+
throw new Error('Unsafe device host');
|
|
477
|
+
const headers = yield signedHeaders(req, udid, device, (_a = deps.controlToken) !== null && _a !== void 0 ? _a : defaultControlToken);
|
|
478
|
+
headers.accept = 'application/json';
|
|
479
|
+
const answer = yield (0, forwardToNode_1.readAnswer)(yield (0, forwardToNode_1.sendToNode)({
|
|
480
|
+
url: `${origin}/xenon/api/control/${encodeURIComponent(udid)}/screenshot`,
|
|
481
|
+
method: 'GET',
|
|
482
|
+
headers,
|
|
483
|
+
signal: AbortSignal.timeout((_b = deps.timeoutMs) !== null && _b !== void 0 ? _b : exports.NODE_CONTROL_TIMEOUT_MS),
|
|
484
|
+
}));
|
|
485
|
+
if (answer.status !== 200) {
|
|
486
|
+
throw new Error(`Node ${origin} answered ${answer.status} for a screenshot`);
|
|
487
|
+
}
|
|
488
|
+
const shot = (_c = JSON.parse(answer.body.toString('utf8'))) === null || _c === void 0 ? void 0 : _c.screenshot;
|
|
489
|
+
if (typeof shot !== 'string' || !shot)
|
|
490
|
+
throw new Error(`Node ${origin} sent no screenshot`);
|
|
491
|
+
return shot;
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
const HOLDER_REFUSALS = new Set(['device_held_by_another_user', 'device_in_use_by_session']);
|
|
495
|
+
/**
|
|
496
|
+
* A node's 409, with the holder's name filled in when the node could only
|
|
497
|
+
* give their id: its database has no rows for this server's users, so it
|
|
498
|
+
* says "another user". The wording is denyBody's, as for this server's own
|
|
499
|
+
* refusals. Anything else is passed on as it came.
|
|
500
|
+
*/
|
|
501
|
+
function relayRefusal(answer, res, describeHolder) {
|
|
502
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
503
|
+
var _a;
|
|
504
|
+
let refusal;
|
|
505
|
+
try {
|
|
506
|
+
refusal = JSON.parse(answer.body.toString('utf8'));
|
|
507
|
+
}
|
|
508
|
+
catch (_b) {
|
|
509
|
+
return (0, forwardToNode_1.sendAnswer)(res, answer);
|
|
510
|
+
}
|
|
511
|
+
const holderId = (_a = refusal === null || refusal === void 0 ? void 0 : refusal.holder) === null || _a === void 0 ? void 0 : _a.userId;
|
|
512
|
+
if (!HOLDER_REFUSALS.has(refusal === null || refusal === void 0 ? void 0 : refusal.error) || typeof holderId !== 'string' || refusal.holder.name) {
|
|
513
|
+
return (0, forwardToNode_1.sendAnswer)(res, answer);
|
|
514
|
+
}
|
|
515
|
+
const name = yield describeHolder(holderId).catch(() => null);
|
|
516
|
+
if (!name)
|
|
517
|
+
return (0, forwardToNode_1.sendAnswer)(res, answer);
|
|
518
|
+
res.status(409).json(Object.assign(Object.assign({}, refusal), (0, deviceAccessPolicy_1.denyBody)(refusal.error, holderId, name)));
|
|
519
|
+
});
|
|
520
|
+
}
|
|
@@ -18,6 +18,7 @@ exports.attachH264Ws = attachH264Ws;
|
|
|
18
18
|
const ws_1 = require("ws");
|
|
19
19
|
const logger_1 = __importDefault(require("../../logger"));
|
|
20
20
|
const upgradeRouter_1 = require("./upgradeRouter");
|
|
21
|
+
const nodeSocketRelay_1 = require("./nodeSocketRelay");
|
|
21
22
|
/**
|
|
22
23
|
* Authenticated H.264 video WebSocket.
|
|
23
24
|
*
|
|
@@ -62,6 +63,7 @@ function attachH264Ws(server, deps) {
|
|
|
62
63
|
function upgrade(req, socket, head) {
|
|
63
64
|
const parsed = parseH264WsPath(req.url);
|
|
64
65
|
wss.handleUpgrade(req, socket, head, (ws) => __awaiter(this, void 0, void 0, function* () {
|
|
66
|
+
var _a, _b, _c;
|
|
65
67
|
// Register cleanup BEFORE the awaits below. redeem + startStream can take
|
|
66
68
|
// seconds; if the client disconnects during that window we must still
|
|
67
69
|
// remove it from the multiplexer — otherwise clientCount stays inflated
|
|
@@ -74,20 +76,44 @@ function attachH264Ws(server, deps) {
|
|
|
74
76
|
};
|
|
75
77
|
ws.on('close', onClose);
|
|
76
78
|
ws.on('error', onClose);
|
|
79
|
+
let actor;
|
|
77
80
|
try {
|
|
78
|
-
yield deps.redeem(parsed.ticket, parsed.udid);
|
|
81
|
+
actor = yield deps.redeem(parsed.ticket, parsed.udid);
|
|
79
82
|
}
|
|
80
|
-
catch (
|
|
83
|
+
catch (_d) {
|
|
81
84
|
try {
|
|
82
85
|
ws.close(1008, 'unauthorized');
|
|
83
86
|
}
|
|
84
|
-
catch (
|
|
87
|
+
catch (_e) {
|
|
85
88
|
/* noop */
|
|
86
89
|
}
|
|
87
90
|
return;
|
|
88
91
|
}
|
|
89
92
|
if (closed)
|
|
90
93
|
return; // disconnected during redeem
|
|
94
|
+
// Another server's phone: its node streams it, relayed from here.
|
|
95
|
+
let upstream = null;
|
|
96
|
+
try {
|
|
97
|
+
upstream = (_b = (yield ((_a = deps.nodeSocket) === null || _a === void 0 ? void 0 : _a.call(deps, parsed.udid, actor)))) !== null && _b !== void 0 ? _b : null;
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
const refused = e instanceof nodeSocketRelay_1.NodeSocketRefused;
|
|
101
|
+
logger_1.default.warn(`[${parsed.udid}] H.264 WS from its node failed: ${(_c = e === null || e === void 0 ? void 0 : e.message) !== null && _c !== void 0 ? _c : e}`);
|
|
102
|
+
try {
|
|
103
|
+
ws.close(refused ? 1008 : 1011, refused ? 'refused by the node' : 'node unreachable');
|
|
104
|
+
}
|
|
105
|
+
catch (_f) {
|
|
106
|
+
/* noop */
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (upstream) {
|
|
111
|
+
if (closed)
|
|
112
|
+
return void upstream.terminate();
|
|
113
|
+
(0, nodeSocketRelay_1.relaySocket)(ws, upstream, maxBuffered);
|
|
114
|
+
logger_1.default.info(`[${parsed.udid}] H.264 WS client connected, relayed from its node`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
91
117
|
let mux;
|
|
92
118
|
try {
|
|
93
119
|
mux = yield deps.startStream(parsed.udid);
|
|
@@ -97,7 +123,7 @@ function attachH264Ws(server, deps) {
|
|
|
97
123
|
try {
|
|
98
124
|
ws.close(1011, 'stream failed');
|
|
99
125
|
}
|
|
100
|
-
catch (
|
|
126
|
+
catch (_g) {
|
|
101
127
|
/* noop */
|
|
102
128
|
}
|
|
103
129
|
return;
|