@xenon-device-management/xenon 2.0.0 → 2.2.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/README.md +2 -1
- package/lib/package.json +3 -4
- package/lib/public/assets/{AnnotationOverlay-BmTPCAds.js → AnnotationOverlay-XpewX5nn.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-B5eB7f8B.js → ApiKeyGate-BQDv3ZCC.js} +1 -1
- package/lib/public/assets/BugReportButton-C7OmS61B.js +10 -0
- package/lib/public/assets/{DeviceMosaicView-9DUoZnQi.js → DeviceMosaicView-wl3l2Zlt.js} +1 -1
- package/lib/public/assets/{EmptyState-lvyNiXev.js → EmptyState-fPxujqqE.js} +1 -1
- package/lib/public/assets/{FieldGroup-BduMPM9K.js → FieldGroup-5bCutWrs.js} +1 -1
- package/lib/public/assets/{FilterMenu-BSqX6I94.js → FilterMenu-owsBiDT1.js} +1 -1
- package/lib/public/assets/{Menu-Ba5cT7jZ.js → Menu-DFhqzPSt.js} +1 -1
- package/lib/public/assets/{Modal-CceoMY7D.js → Modal-Bw-a-E-e.js} +1 -1
- package/lib/public/assets/{RecordingPage-6T9dOidc.js → RecordingPage-WtByf5bC.js} +1 -1
- package/lib/public/assets/{RecordingsPage-qj6FdZXR.js → RecordingsPage-FpWWFNgT.js} +1 -1
- package/lib/public/assets/{SegmentedControl-CSRuZ8TR.js → SegmentedControl-Jzn9c7o8.js} +1 -1
- package/lib/public/assets/{SettingCard-Bp1kz4By.js → SettingCard-BR7nOhpT.js} +1 -1
- package/lib/public/assets/{Table-LV7zFyZg.js → Table-B_Va3V05.js} +1 -1
- package/lib/public/assets/{activity-D17TUloi.js → activity-DxRFWvkN.js} +1 -1
- package/lib/public/assets/{ai-settings-qvbhY89P.js → ai-settings-CQf-9_V6.js} +1 -1
- package/lib/public/assets/{api-keys-BfpvdG7G.js → api-keys-SoE1Kd7N.js} +1 -1
- package/lib/public/assets/{apps-Dbnzd0fW.js → apps-CgGXTlgn.js} +1 -1
- package/lib/public/assets/{arrow-left-DD1VjtC7.js → arrow-left-uQ6WIwiC.js} +1 -1
- package/lib/public/assets/{arrow-right-D0pnM3ma.js → arrow-right-C9RyfzXS.js} +1 -1
- package/lib/public/assets/{arrow-up-right-BIC8JOV6.js → arrow-up-right-DFHi_8yG.js} +1 -1
- package/lib/public/assets/{auth-shell-DWHPXSnI.js → auth-shell-Bii1t4qc.js} +2 -2
- package/lib/public/assets/{builds-page-ClCNBUv_.js → builds-page-Bnv8Z9hi.js} +1 -1
- package/lib/public/assets/{button-xtAhYR-f.js → button-_Y78wXiP.js} +1 -1
- package/lib/public/assets/{calendar-CupwJ7Bm.js → calendar-D4MqSOc2.js} +1 -1
- package/lib/public/assets/{check-CyuEoKkd.js → check-uGHCXe-L.js} +1 -1
- package/lib/public/assets/{chevron-left-DgX-F9xK.js → chevron-left-Dy1OqpzB.js} +1 -1
- package/lib/public/assets/{chevron-right-BKJgJnU9.js → chevron-right-BUsldEli.js} +1 -1
- package/lib/public/assets/{circle-check-CZdg8mMa.js → circle-check-D3Qq0tKp.js} +1 -1
- package/lib/public/assets/{circle-x-CZH6t9z-.js → circle-x-B4IOD1DE.js} +1 -1
- package/lib/public/assets/{clock-Cd-t7xpe.js → clock-BfYfzvSg.js} +1 -1
- package/lib/public/assets/{copy-C6zerX3I.js → copy-DZwYE5Va.js} +1 -1
- package/lib/public/assets/{device-explorer-B1pI6b_M.js → device-explorer-CRG4Utt5.js} +3 -3
- package/lib/public/assets/device-explorer-D2AqrrLN.css +1 -0
- package/lib/public/assets/{download-x9WP7mcL.js → download-CCDYo_TN.js} +1 -1
- package/lib/public/assets/{forgot-password-B0TEH4N-.js → forgot-password-DuLCmVFb.js} +1 -1
- package/lib/public/assets/{funnel-Cb40MfFN.js → funnel-CtKNPqzb.js} +1 -1
- package/lib/public/assets/{index-BR1wTIPC.js → index-3uxl3o7K.js} +43 -43
- package/lib/public/assets/{index-BvH_ekRy.js → index-Zy1Op5C1.js} +1 -1
- package/lib/public/assets/{input-DejcCiTI.js → input-CzTWvUXZ.js} +1 -1
- package/lib/public/assets/{keycodes-Mavx5iGg.js → keycodes-CI-nYDFM.js} +1 -1
- package/lib/public/assets/{layers-BKqYLiHE.js → layers-YZZGFyTX.js} +1 -1
- package/lib/public/assets/{lock-LFsFhc17.js → lock-L8tzWgDo.js} +1 -1
- package/lib/public/assets/{login-x2pkzt-t.js → login-DBbX-fu8.js} +1 -1
- package/lib/public/assets/{maintenance-settings-CYafcoV-.js → maintenance-settings-AJwkOIsE.js} +1 -1
- package/lib/public/assets/{monitor-_7_uJ8d9.js → monitor-Di_66F2m.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-BfLuURKN.js → mouse-pointer-2-Cnab2zdg.js} +1 -1
- package/lib/public/assets/{network-Z_zBhpsD.js → network-DiSumY-1.js} +1 -1
- package/lib/public/assets/{overview-BY5uv08S.js → overview-CwE4gXl5.js} +1 -1
- package/lib/public/assets/{page-header-CP2UkykG.js → page-header-CfR4Sm9V.js} +1 -1
- package/lib/public/assets/{plus-BTD0dnQ0.js → plus-DOS8P9CQ.js} +1 -1
- package/lib/public/assets/{profile-page-BhJZuCS7.js → profile-page-BYpHwQIH.js} +1 -1
- package/lib/public/assets/{reset-password-CMN0SO81.js → reset-password-BRVJU_qV.js} +1 -1
- package/lib/public/assets/{runbook-page-CBpPX9_5.js → runbook-page-Bi3JvAFZ.js} +1 -1
- package/lib/public/assets/{select-Uc9E5pye.js → select-Cqx7pEou.js} +1 -1
- package/lib/public/assets/{selector-detail-page-BSyBnRuU.js → selector-detail-page-C0DsncKj.js} +1 -1
- package/lib/public/assets/{selector-health-DzqxNkzm.js → selector-health-CAhpdilF.js} +1 -1
- package/lib/public/assets/{selector-health-page-CyPLjoD1.js → selector-health-page-CrCVeW2m.js} +1 -1
- package/lib/public/assets/{session-detail-page-BZQZN1P-.js → session-detail-page-F_hwYqEt.js} +1 -1
- package/lib/public/assets/{settings-CkSWiZq8.js → settings-C-s5pypL.js} +1 -1
- package/lib/public/assets/{status-pill-outline-BFYni2qo.js → status-pill-outline-DY5uzv6A.js} +1 -1
- package/lib/public/assets/{tablet-BSd-xSZK.js → tablet-Dli7gwyg.js} +1 -1
- package/lib/public/assets/{teams-PpoEQz31.js → teams-ui4IhHNC.js} +1 -1
- package/lib/public/assets/{trash-2-BorJv5_u.js → trash-2-BV4hSmXJ.js} +1 -1
- package/lib/public/assets/{upload-C3i7sAZl.js → upload-DkGy_J-L.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-CeZQ3EsH.js → use-password-reset-mode-BFtMqQOh.js} +1 -1
- package/lib/public/assets/{users-Drd7ifJx.js → users-BU0nXIVe.js} +1 -1
- package/lib/public/assets/{users-CEJ3fC78.js → users-Nb_rzRmF.js} +1 -1
- package/lib/public/assets/{video-off-CNzn6bPE.js → video-off-CUNzD2kH.js} +1 -1
- package/lib/public/assets/{webhook-settings-BFqX6QXV.js → webhook-settings-Ca3UFXZe.js} +1 -1
- package/lib/public/assets/{zap-4yN8Jdfw.js → zap-B2bH2vKX.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/InternalHttpClient.js +5 -1
- package/lib/src/XenonCapabilityManager.js +15 -1
- package/lib/src/app/appiumBasePath.js +32 -0
- package/lib/src/app/index.js +12 -0
- package/lib/src/app/insertBeforeRoutes.js +24 -4
- package/lib/src/app/registerCommandAuth.js +152 -33
- package/lib/src/app/routers/control.js +11 -104
- package/lib/src/app/routers/grid.js +28 -8
- package/lib/src/app/routers/nodePhoneControl.js +301 -0
- package/lib/src/app/ws/h264StreamWs.js +43 -12
- package/lib/src/app/ws/logcatWs.js +48 -14
- package/lib/src/app/ws/nodeSocketRelay.js +167 -0
- package/lib/src/app/ws/upgradeRouter.js +215 -0
- package/lib/src/dashboard/event-manager.js +7 -6
- package/lib/src/data-service/device-service.js +152 -49
- package/lib/src/data-service/device-store.js +92 -10
- package/lib/src/data-service/deviceClaims.js +116 -0
- package/lib/src/data-service/deviceFieldOwners.js +32 -10
- package/lib/src/data-service/prisma-store.js +102 -31
- package/lib/src/device-managers/AndroidDeviceManager.js +40 -42
- package/lib/src/device-managers/HealthMonitorService.js +17 -2
- package/lib/src/device-managers/IOSDeviceManager.js +5 -1
- package/lib/src/device-managers/NodeDevices.js +7 -2
- package/lib/src/device-managers/android/AndroidH264StreamService.js +13 -5
- package/lib/src/device-managers/android/AndroidStreamService.js +9 -6
- package/lib/src/device-managers/android/previewHold.js +2 -2
- package/lib/src/device-managers/ios/IOSDiscoveryService.js +19 -8
- package/lib/src/device-managers/ios/IOSStreamService.js +162 -40
- package/lib/src/device-managers/localDeviceHosts.js +86 -0
- package/lib/src/device-managers/ownDeviceRow.js +38 -0
- package/lib/src/device-utils.js +78 -24
- package/lib/src/gateway/createHandoff.js +73 -0
- package/lib/src/gateway/defaultGateway.js +80 -0
- package/lib/src/gateway/forwardToNode.js +147 -0
- package/lib/src/gateway/hubSessionToken.js +356 -0
- package/lib/src/gateway/internalCall.js +90 -0
- package/lib/src/gateway/nodeSessionStatus.js +140 -0
- package/lib/src/gateway/nodeWebDriverUrl.js +139 -0
- package/lib/src/gateway/sessionCreate.js +136 -0
- package/lib/src/gateway/sessionGateway.js +201 -0
- package/lib/src/gateway/sessionLocator.js +100 -0
- package/lib/src/generated/client/edge.js +7 -4
- package/lib/src/generated/client/index-browser.js +4 -1
- package/lib/src/generated/client/index.d.ts +112 -1
- package/lib/src/generated/client/index.js +7 -4
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +9 -0
- package/lib/src/generated/client/wasm.js +4 -1
- package/lib/src/helpers/index.js +5 -1
- package/lib/src/logger.js +5 -0
- package/lib/src/middleware/authMiddleware.js +80 -0
- package/lib/src/middleware/commandAuth.js +31 -17
- package/lib/src/middleware/csrfMiddleware.js +3 -2
- package/lib/src/middleware/deviceAccessGuard.js +4 -1
- package/lib/src/middleware/sessionListingFilter.js +153 -0
- package/lib/src/middleware/sessionUpgradeGuard.js +203 -0
- package/lib/src/plugin.js +27 -1
- package/lib/src/prisma.js +2 -1
- package/lib/src/services/DeviceReconciler.js +5 -2
- package/lib/src/services/OrphanSweeper.js +12 -19
- package/lib/src/services/PortAllocator.js +75 -16
- package/lib/src/services/ServerManager.js +63 -30
- package/lib/src/services/SessionLifecycleService.js +335 -76
- package/lib/src/services/ShutdownCoordinator.js +7 -1
- package/lib/src/services/SocketServer.js +11 -2
- package/lib/src/services/device-access/LiveSessionOwners.js +53 -0
- package/lib/src/services/device-access/SessionOwnerResolver.js +74 -0
- package/lib/src/services/device-access/ticketActorAccess.js +3 -1
- package/lib/src/services/lease/LeaseOrphanSweeper.js +34 -6
- package/lib/src/services/lease/LeaseService.js +19 -5
- package/lib/src/services/session/nodeCreateCaps.js +55 -0
- package/lib/src/services/session/sessionCredentials.js +7 -59
- package/lib/src/sessions/CloudSession.js +12 -0
- package/lib/src/sessions/LocalSession.js +52 -26
- package/lib/src/sessions/RemoteSession.js +116 -9
- package/lib/src/sessions/SessionManager.js +49 -6
- package/lib/src/sessions/appiumUmbrella.js +32 -0
- package/lib/test/helpers/appium-umbrella.js +84 -0
- package/lib/test/helpers/fake-device-table.js +97 -0
- package/lib/test/helpers/loki-stores.js +41 -0
- package/lib/test/helpers/loopbackRequest.js +77 -0
- package/lib/test/helpers/own-node-id.js +29 -0
- package/lib/test/helpers/scratch-database.js +100 -0
- package/lib/test/helpers/scratch-port-leases.js +119 -0
- package/lib/test/helpers/test-container.js +35 -27
- package/lib/test/integration/androidDevices.spec.js +5 -4
- package/lib/test/integration/ios/01iOSSimulator.spec.js +6 -5
- package/lib/test/integration/ios/02iOSDevices.spec.js +3 -2
- package/lib/test/integration/prisma-device-store-add-devices.spec.js +14 -4
- package/lib/test/setup/scratch-default-database.js +32 -0
- package/lib/test/unit/AndroidDeviceManager.spec.js +56 -1
- package/lib/test/unit/DeviceModel.spec.js +9 -9
- package/lib/test/unit/HealingCascade.test.js +5 -1
- package/lib/test/unit/OrphanSweeper.test.js +17 -13
- package/lib/test/unit/SessionHeartbeatWrite.test.js +6 -7
- package/lib/test/unit/StreamTicketService.spec.js +4 -1
- package/lib/test/unit/TokenEndpoint.spec.js +17 -3
- package/lib/test/unit/aggregate-hotspots-session-scope.spec.js +5 -5
- package/lib/test/unit/android-h264-service.spec.js +24 -0
- package/lib/test/unit/android-stream-stall-eof.spec.js +10 -0
- package/lib/test/unit/apps-team-visibility.spec.js +16 -16
- package/lib/test/unit/auth-me-auth-disabled.test.js +7 -4
- package/lib/test/unit/authMiddleware.appTicket.spec.js +5 -5
- package/lib/test/unit/authMiddleware.bearer.spec.js +4 -1
- package/lib/test/unit/authMiddleware.mcpAudience.spec.js +4 -1
- package/lib/test/unit/authMiddleware.streamTicket.spec.js +19 -7
- package/lib/test/unit/authMiddleware.test.js +3 -0
- package/lib/test/unit/bug-report/route.spec.js +9 -9
- package/lib/test/unit/cliargs-admin-only.spec.js +3 -3
- package/lib/test/unit/clipboard-route.spec.js +22 -8
- package/lib/test/unit/command-auth-appium-server.spec.js +187 -32
- package/lib/test/unit/command-auth.spec.js +31 -31
- package/lib/test/unit/control-device-lookup.spec.js +9 -9
- package/lib/test/unit/control-forward-decision.spec.js +115 -0
- package/lib/test/unit/control-lookup-failure.spec.js +3 -3
- package/lib/test/unit/create-error-message.spec.js +43 -0
- package/lib/test/unit/create-handoff.spec.js +86 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
- package/lib/test/unit/device-access-guard.spec.js +66 -28
- package/lib/test/unit/device-service.spec.js +4 -0
- package/lib/test/unit/device-store-clear-at-boot.spec.js +48 -0
- package/lib/test/unit/device-sync-lost-update.spec.js +17 -89
- package/lib/test/unit/device-team-guard.spec.js +20 -20
- package/lib/test/unit/device-utils.spec.js +8 -0
- package/lib/test/unit/extract-team-cap.spec.js +65 -0
- package/lib/test/unit/grid-admin-only.spec.js +3 -3
- package/lib/test/unit/healing-tier-gate.spec.js +5 -1
- package/lib/test/unit/health-monitor-own-phones.spec.js +102 -0
- package/lib/test/unit/health-monitor-stream-lookup.spec.js +11 -1
- package/lib/test/unit/helpers.spec.js +27 -0
- package/lib/test/unit/hub-boot-sweep-recovery.spec.js +192 -0
- package/lib/test/unit/hub-create-once.spec.js +190 -0
- package/lib/test/unit/hub-create-session-umbrella.spec.js +160 -0
- package/lib/test/unit/hub-gateway-create-appium-server.spec.js +591 -0
- package/lib/test/unit/hub-node-control-forward.spec.js +608 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +594 -0
- package/lib/test/unit/hub-node-race.spec.js +426 -0
- package/lib/test/unit/hub-node-same-host.spec.js +356 -0
- package/lib/test/unit/hub-register-removal.spec.js +165 -0
- package/lib/test/unit/hub-remote-session-heartbeat.spec.js +277 -0
- package/lib/test/unit/hub-routed-session-recovery.spec.js +151 -0
- package/lib/test/unit/hub-session-token.spec.js +265 -0
- package/lib/test/unit/insert-before-routes.spec.js +5 -5
- package/lib/test/unit/install-repository-app-team.spec.js +6 -2
- package/lib/test/unit/interceptor/MitmProxyHost.spec.js +23 -10
- package/lib/test/unit/internal-call.spec.js +181 -0
- package/lib/test/unit/ios-discovery-same-host.spec.js +193 -0
- package/lib/test/unit/ios-stream-appium-tunnel.spec.js +317 -0
- package/lib/test/unit/ios-stream-port-leases.spec.js +299 -0
- package/lib/test/unit/ios-wda-status-identity.spec.js +84 -0
- package/lib/test/unit/is-xenon-running.spec.js +69 -0
- package/lib/test/unit/lease/LeaseOrphanSweeper.spec.js +2 -2
- package/lib/test/unit/lease/LeaseService.spec.js +28 -1
- package/lib/test/unit/lease/lease-reap-held-phone.spec.js +154 -0
- package/lib/test/unit/lease/lease-sweeper-expiry.spec.js +107 -0
- package/lib/test/unit/lease/lease-team-visibility.spec.js +2 -2
- package/lib/test/unit/lease/lease-token-log-filter.spec.js +242 -64
- package/lib/test/unit/lease/lease-token-oracle.spec.js +20 -7
- package/lib/test/unit/lease/session-lease-proof.spec.js +8 -3
- package/lib/test/unit/logcat-ws.spec.js +12 -1
- package/lib/test/unit/loopback-request.spec.js +81 -0
- package/lib/test/unit/mark-node-busy-loki.spec.js +53 -0
- package/lib/test/unit/node-create-caps.spec.js +54 -0
- package/lib/test/unit/node-gateway-create-appium-server.spec.js +354 -0
- package/lib/test/unit/node-hub-session-owner.spec.js +223 -0
- package/lib/test/unit/node-phone-control.spec.js +131 -0
- package/lib/test/unit/node-socket-relay.spec.js +357 -0
- package/lib/test/unit/omni-vision.spec.js +7 -3
- package/lib/test/unit/plugin.spec.js +45 -4
- package/lib/test/unit/port-allocator-os-probe.spec.js +151 -0
- package/lib/test/unit/profile-router.test.js +11 -11
- package/lib/test/unit/queue-requester.spec.js +5 -0
- package/lib/test/unit/queue-team-scope-routes.spec.js +17 -16
- package/lib/test/unit/queue-visibility.spec.js +1 -1
- package/lib/test/unit/recording-downloads-visibility.spec.js +2 -2
- package/lib/test/unit/recording-instrumentation.spec.js +5 -0
- package/lib/test/unit/recording-source-ended.spec.js +5 -0
- package/lib/test/unit/recording-store.spec.js +9 -3
- package/lib/test/unit/recordings-library-routes.spec.js +51 -51
- package/lib/test/unit/recordings-write-visibility.spec.js +2 -2
- package/lib/test/unit/remote-session-create.spec.js +130 -0
- package/lib/test/unit/reservation-team-visibility.spec.js +5 -5
- package/lib/test/unit/reset-link.test.js +14 -14
- package/lib/test/unit/session-app-resolution.spec.js +14 -10
- package/lib/test/unit/session-assets.spec.js +9 -9
- package/lib/test/unit/session-attribution-wiring.spec.js +29 -2
- package/lib/test/unit/session-create-layer.spec.js +255 -0
- package/lib/test/unit/session-credential-strip.spec.js +25 -9
- package/lib/test/unit/session-credentials.spec.js +0 -47
- package/lib/test/unit/session-listing-auth.spec.js +345 -0
- package/lib/test/unit/session-locator.spec.js +250 -0
- package/lib/test/unit/session-node-calls.spec.js +174 -0
- package/lib/test/unit/session-owner-resolver.spec.js +107 -0
- package/lib/test/unit/session-recovery-routing.spec.js +118 -0
- package/lib/test/unit/session-upgrade-guard.spec.js +571 -0
- package/lib/test/unit/shutdown-drain-remote.spec.js +58 -0
- package/lib/test/unit/socketServer.bearer.spec.js +3 -1
- package/lib/test/unit/stream-leave-route.spec.js +19 -7
- package/lib/test/unit/stream-own-device-row.spec.js +164 -0
- package/lib/test/unit/stream-stop-ownership.spec.js +17 -5
- package/lib/test/unit/stream-stop-recording.spec.js +17 -5
- package/lib/test/unit/stream-ticket-identity.spec.js +26 -6
- package/lib/test/unit/stream-watchdog-timers.spec.js +46 -0
- package/lib/test/unit/upgrade-router.spec.js +446 -0
- package/lib/test/unit/users-router.test.js +11 -8
- package/lib/test/unit/verify-credential.spec.js +4 -1
- package/lib/test/unit/webdriver-info-route.spec.js +64 -0
- package/lib/test/unit/ws-upgrade-routing-appium-server.spec.js +500 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -4
- package/prisma/migrations/20260929120000_device_claims/migration.sql +8 -0
- package/prisma/schema.prisma +9 -0
- package/lib/public/assets/BugReportButton-DKYsOdGV.js +0 -10
- package/lib/public/assets/device-explorer-DV8rWRxL.css +0 -1
- package/lib/src/proxy/wd-command-proxy.js +0 -221
|
@@ -80,6 +80,7 @@ const deviceTeamGuard_1 = require("../../middleware/deviceTeamGuard");
|
|
|
80
80
|
const controlDevice_1 = require("../../middleware/controlDevice");
|
|
81
81
|
const manualLock_1 = require("../../services/recording/manualLock");
|
|
82
82
|
const streamStartConflict_1 = require("./streamStartConflict");
|
|
83
|
+
const nodePhoneControl_1 = require("./nodePhoneControl");
|
|
83
84
|
const streamLeave_1 = require("./streamLeave");
|
|
84
85
|
const SessionOwnerResolver_1 = require("../../services/device-access/SessionOwnerResolver");
|
|
85
86
|
const actor_1 = require("../../services/device-access/actor");
|
|
@@ -110,37 +111,10 @@ router.use((0, deviceTeamGuard_1.deviceTeamGuard)());
|
|
|
110
111
|
// another user's Appium session. Mounted here so every current and future
|
|
111
112
|
// mutation is covered without per-handler opt-in.
|
|
112
113
|
router.use((0, deviceAccessGuard_1.deviceAccessGuard)());
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
'metadata.goog',
|
|
118
|
-
'100.100.100.200', // Alibaba ECS metadata
|
|
119
|
-
'fd00:ec2::254', // AWS IMDSv6
|
|
120
|
-
]);
|
|
121
|
-
/**
|
|
122
|
-
* Build a safe proxy URL from a device's reported host. Strips any path,
|
|
123
|
-
* query, or fragment the host string carried, blocks cloud-metadata targets,
|
|
124
|
-
* and refuses non-http(s) schemes. Returns null if the host is unsafe.
|
|
125
|
-
*/
|
|
126
|
-
function buildProxyUrl(deviceHost, req) {
|
|
127
|
-
let parsed;
|
|
128
|
-
try {
|
|
129
|
-
parsed = new URL(deviceHost);
|
|
130
|
-
}
|
|
131
|
-
catch (_a) {
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
135
|
-
return null;
|
|
136
|
-
if (FORBIDDEN_PROXY_HOSTS.has(parsed.hostname))
|
|
137
|
-
return null;
|
|
138
|
-
// Only keep scheme + host + port; discard any attacker-baked path/query/fragment.
|
|
139
|
-
const origin = `${parsed.protocol}//${parsed.host}`;
|
|
140
|
-
// Treat req.originalUrl as a path — reparse to strip any control characters.
|
|
141
|
-
const forwardPath = req.originalUrl.startsWith('/') ? req.originalUrl : `/${req.originalUrl}`;
|
|
142
|
-
return `${origin}${forwardPath}`;
|
|
143
|
-
}
|
|
114
|
+
// Another server's phone (a node's, or a cloud provider's): its actions are
|
|
115
|
+
// sent on to its node, answered here, or refused, before any handler below
|
|
116
|
+
// runs it with this server's own tools. See nodePhoneControl.ts.
|
|
117
|
+
router.use((0, nodePhoneControl_1.nodePhoneControl)());
|
|
144
118
|
function getDeviceInfo(udid) {
|
|
145
119
|
return __awaiter(this, void 0, void 0, function* () {
|
|
146
120
|
const device = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
|
|
@@ -172,26 +146,12 @@ function getDeviceManagerForPlatform(platform) {
|
|
|
172
146
|
});
|
|
173
147
|
}
|
|
174
148
|
router.post('/:udid/tap', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
175
|
-
var _a, _b;
|
|
176
149
|
const { udid } = req.params;
|
|
177
150
|
const { x, y } = req.body;
|
|
178
151
|
const device = yield getDeviceInfo(udid);
|
|
179
152
|
if (!device)
|
|
180
153
|
return res.status(404).send('Device not found');
|
|
181
154
|
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
182
|
-
if (device.host && !device.host.includes(req.get('host') || '')) {
|
|
183
|
-
const target = buildProxyUrl(device.host, req);
|
|
184
|
-
if (!target)
|
|
185
|
-
return res.status(400).send({ error: 'Unsafe device host' });
|
|
186
|
-
logger_1.default.info(`Proxying tap for ${udid} to ${target}`);
|
|
187
|
-
try {
|
|
188
|
-
yield InternalHttpClient_1.InternalHttpClient.post(target, req.body);
|
|
189
|
-
return res.status(200).send({ success: true });
|
|
190
|
-
}
|
|
191
|
-
catch (err) {
|
|
192
|
-
return res.status(((_a = err.response) === null || _a === void 0 ? void 0 : _a.status) || 500).send(((_b = err.response) === null || _b === void 0 ? void 0 : _b.data) || err.message);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
155
|
if (manager && manager.tap) {
|
|
196
156
|
try {
|
|
197
157
|
yield manager.tap(udid, x, y);
|
|
@@ -216,26 +176,12 @@ router.post('/:udid/tap', (req, res) => __awaiter(void 0, void 0, void 0, functi
|
|
|
216
176
|
res.status(400).send('Manager not found or tap not supported');
|
|
217
177
|
}));
|
|
218
178
|
router.post('/:udid/swipe', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
219
|
-
var _a, _b;
|
|
220
179
|
const { udid } = req.params;
|
|
221
180
|
const { x, y, endX, endY, duration = 1000 } = req.body;
|
|
222
181
|
const device = yield getDeviceInfo(udid);
|
|
223
182
|
if (!device)
|
|
224
183
|
return res.status(404).send('Device not found');
|
|
225
184
|
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
226
|
-
if (device.host && !device.host.includes(req.get('host') || '')) {
|
|
227
|
-
const target = buildProxyUrl(device.host, req);
|
|
228
|
-
if (!target)
|
|
229
|
-
return res.status(400).send({ error: 'Unsafe device host' });
|
|
230
|
-
logger_1.default.info(`Proxying swipe for ${udid} to ${target}`);
|
|
231
|
-
try {
|
|
232
|
-
yield InternalHttpClient_1.InternalHttpClient.post(target, req.body);
|
|
233
|
-
return res.status(200).send({ success: true });
|
|
234
|
-
}
|
|
235
|
-
catch (err) {
|
|
236
|
-
return res.status(((_a = err.response) === null || _a === void 0 ? void 0 : _a.status) || 500).send(((_b = err.response) === null || _b === void 0 ? void 0 : _b.data) || err.message);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
185
|
if (manager && manager.swipe) {
|
|
240
186
|
try {
|
|
241
187
|
yield manager.swipe(udid, x, y, endX, endY, duration);
|
|
@@ -249,26 +195,12 @@ router.post('/:udid/swipe', (req, res) => __awaiter(void 0, void 0, void 0, func
|
|
|
249
195
|
res.status(400).send('Manager not found or swipe not supported');
|
|
250
196
|
}));
|
|
251
197
|
router.post('/:udid/text', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
252
|
-
var _a, _b;
|
|
253
198
|
const { udid } = req.params;
|
|
254
199
|
const { text } = req.body;
|
|
255
200
|
const device = yield getDeviceInfo(udid);
|
|
256
201
|
if (!device)
|
|
257
202
|
return res.status(404).send('Device not found');
|
|
258
203
|
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
259
|
-
if (device.host && !device.host.includes(req.get('host') || '')) {
|
|
260
|
-
const target = buildProxyUrl(device.host, req);
|
|
261
|
-
if (!target)
|
|
262
|
-
return res.status(400).send({ error: 'Unsafe device host' });
|
|
263
|
-
logger_1.default.info(`Proxying typeText for ${udid} to ${target}`);
|
|
264
|
-
try {
|
|
265
|
-
yield InternalHttpClient_1.InternalHttpClient.post(target, req.body);
|
|
266
|
-
return res.status(200).send({ success: true });
|
|
267
|
-
}
|
|
268
|
-
catch (err) {
|
|
269
|
-
return res.status(((_a = err.response) === null || _a === void 0 ? void 0 : _a.status) || 500).send(((_b = err.response) === null || _b === void 0 ? void 0 : _b.data) || err.message);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
204
|
if (manager && manager.typeText) {
|
|
273
205
|
try {
|
|
274
206
|
yield manager.typeText(udid, text);
|
|
@@ -282,26 +214,12 @@ router.post('/:udid/text', (req, res) => __awaiter(void 0, void 0, void 0, funct
|
|
|
282
214
|
res.status(400).send('Manager not found or typeText not supported');
|
|
283
215
|
}));
|
|
284
216
|
router.post('/:udid/keyevent', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
285
|
-
var _a, _b;
|
|
286
217
|
const { udid } = req.params;
|
|
287
218
|
const { keyCode } = req.body;
|
|
288
219
|
const device = yield getDeviceInfo(udid);
|
|
289
220
|
if (!device)
|
|
290
221
|
return res.status(404).send('Device not found');
|
|
291
222
|
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
292
|
-
if (device.host && !device.host.includes(req.get('host') || '')) {
|
|
293
|
-
const target = buildProxyUrl(device.host, req);
|
|
294
|
-
if (!target)
|
|
295
|
-
return res.status(400).send({ error: 'Unsafe device host' });
|
|
296
|
-
logger_1.default.info(`Proxying keyevent for ${udid} to ${target}`);
|
|
297
|
-
try {
|
|
298
|
-
yield InternalHttpClient_1.InternalHttpClient.post(target, req.body);
|
|
299
|
-
return res.status(200).send({ success: true });
|
|
300
|
-
}
|
|
301
|
-
catch (err) {
|
|
302
|
-
return res.status(((_a = err.response) === null || _a === void 0 ? void 0 : _a.status) || 500).send(((_b = err.response) === null || _b === void 0 ? void 0 : _b.data) || err.message);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
223
|
if (manager && manager.pressKey) {
|
|
306
224
|
try {
|
|
307
225
|
yield manager.pressKey(udid, keyCode);
|
|
@@ -385,26 +303,12 @@ router.post('/:udid/clipboard', (req, res) => __awaiter(void 0, void 0, void 0,
|
|
|
385
303
|
res.status(400).send('Manager not found or setClipboard not supported');
|
|
386
304
|
}));
|
|
387
305
|
router.post('/:udid/touchAndHold', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
388
|
-
var _a, _b;
|
|
389
306
|
const { udid } = req.params;
|
|
390
307
|
const { x, y, duration = 1000 } = req.body;
|
|
391
308
|
const device = yield getDeviceInfo(udid);
|
|
392
309
|
if (!device)
|
|
393
310
|
return res.status(404).send('Device not found');
|
|
394
311
|
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
395
|
-
if (device.host && !device.host.includes(req.get('host') || '')) {
|
|
396
|
-
const target = buildProxyUrl(device.host, req);
|
|
397
|
-
if (!target)
|
|
398
|
-
return res.status(400).send({ error: 'Unsafe device host' });
|
|
399
|
-
logger_1.default.info(`Proxying touchAndHold for ${udid} to ${target}`);
|
|
400
|
-
try {
|
|
401
|
-
yield InternalHttpClient_1.InternalHttpClient.post(target, req.body);
|
|
402
|
-
return res.status(200).send({ success: true });
|
|
403
|
-
}
|
|
404
|
-
catch (err) {
|
|
405
|
-
return res.status(((_a = err.response) === null || _a === void 0 ? void 0 : _a.status) || 500).send(((_b = err.response) === null || _b === void 0 ? void 0 : _b.data) || err.message);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
312
|
if (manager && manager.touchAndHold) {
|
|
409
313
|
try {
|
|
410
314
|
yield manager.touchAndHold(udid, x, y, duration);
|
|
@@ -800,7 +704,9 @@ router.post('/:udid/stream/ticket', (req, res) => __awaiter(void 0, void 0, void
|
|
|
800
704
|
function stopPreview(udid, device, release) {
|
|
801
705
|
return __awaiter(this, void 0, void 0, function* () {
|
|
802
706
|
if (device.platform === 'ios' || device.platform === 'tvos') {
|
|
803
|
-
|
|
707
|
+
// A viewer's stop: an Appium session on the phone may be driving this
|
|
708
|
+
// stream's WDA, and then the stream stays (see IOSStreamService.stopStream).
|
|
709
|
+
yield typedi_1.Container.get(IOSStreamService_1.default).stopStream(udid, { forViewer: true });
|
|
804
710
|
}
|
|
805
711
|
else {
|
|
806
712
|
yield typedi_1.Container.get(AndroidStreamService_1.default).stopStream(udid);
|
|
@@ -1026,8 +932,9 @@ router.get('/:udid/stream', (req, res) => __awaiter(void 0, void 0, void 0, func
|
|
|
1026
932
|
getSession: (id) => iosStreamService.getStreamStatus(id),
|
|
1027
933
|
// Single attempt (retries = 0): a healthy WDA answers in milliseconds,
|
|
1028
934
|
// so this costs nothing on the happy path and stays bounded by the
|
|
1029
|
-
// 2.5s /status timeout when WDA is dead.
|
|
1030
|
-
|
|
935
|
+
// 2.5s /status timeout when WDA is dead. No udid: WDA names no phone
|
|
936
|
+
// (see isWDARunning). The port is this phone's stream's own lease.
|
|
937
|
+
isWdaHealthy: (wdaPort) => iosStreamService.isWDARunning(wdaPort, 0),
|
|
1031
938
|
startStream: (id) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1032
939
|
var _a;
|
|
1033
940
|
logger_1.default.info(`Stream for iOS device ${id} requested (Status: ${((_a = iosStreamService.getStreamStatus(id)) === null || _a === void 0 ? void 0 : _a.status) || 'idle'})...`);
|
|
@@ -58,6 +58,8 @@ const device_managers_1 = require("../../device-managers");
|
|
|
58
58
|
const typedi_1 = require("typedi");
|
|
59
59
|
const prisma_1 = require("../../prisma");
|
|
60
60
|
const DeviceTeamResolver_1 = require("../../services/device-access/DeviceTeamResolver");
|
|
61
|
+
const PluginContext_1 = require("../../PluginContext");
|
|
62
|
+
const localDeviceHosts_1 = require("../../device-managers/localDeviceHosts");
|
|
61
63
|
const store = device_store_1.DeviceStoreFactory.getStore();
|
|
62
64
|
const pendingStore = device_store_1.DeviceStoreFactory.getPendingSessionStore();
|
|
63
65
|
const SERVER_UP_TIME = new Date().toISOString();
|
|
@@ -141,26 +143,44 @@ function getDeviceByPlatform(request, response) {
|
|
|
141
143
|
return response.status(200).send(yield (0, device_service_1.enrichDevicesWithTeamNames)(devices));
|
|
142
144
|
});
|
|
143
145
|
}
|
|
146
|
+
/** A removal a node asked for, from what it sent: strings only. */
|
|
147
|
+
function nodeRemoval(host, nodeId, udid) {
|
|
148
|
+
return {
|
|
149
|
+
udid,
|
|
150
|
+
host: typeof host === 'string' && host !== '' ? host : undefined,
|
|
151
|
+
nodeId: typeof nodeId === 'string' && nodeId !== '' ? nodeId : undefined,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/** Whether a row is one of this server's own phones, which no node may remove. */
|
|
155
|
+
function isThisServersOwn() {
|
|
156
|
+
const context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
157
|
+
const local = (0, localDeviceHosts_1.localDeviceHosts)(context.pluginArgs, context.port);
|
|
158
|
+
return (device) => (0, localDeviceHosts_1.isOwnDevice)(local, context.nodeId, device);
|
|
159
|
+
}
|
|
144
160
|
function registerNode(request, response) {
|
|
145
161
|
return __awaiter(this, void 0, void 0, function* () {
|
|
146
162
|
const requestBody = request.body;
|
|
147
163
|
const { type } = request.query;
|
|
148
164
|
if (type === 'add') {
|
|
149
|
-
// A node's
|
|
150
|
-
//
|
|
151
|
-
|
|
165
|
+
// A node's report of its phones (AddDevicesOptions.nodeReport): what the
|
|
166
|
+
// node observes of them, and whether they are busy there (`nodeBusy`).
|
|
167
|
+
// It never frees a phone this hub has claimed for a session, nor changes
|
|
168
|
+
// the settings this hub owns for it: team, tags, reservation, block.
|
|
169
|
+
const addedDevices = yield (0, device_service_1.addNewDevice)(requestBody, undefined, { nodeReport: true });
|
|
152
170
|
if (addedDevices.length > 0) {
|
|
153
171
|
logger_1.default.info(`Added new devices: ${JSON.stringify(addedDevices)}`);
|
|
154
172
|
}
|
|
155
173
|
}
|
|
156
174
|
else if (type === 'remove') {
|
|
157
|
-
|
|
175
|
+
// One phone each, by udid: an entry without one names no phone.
|
|
176
|
+
const removals = (Array.isArray(requestBody) ? requestBody : [])
|
|
177
|
+
.filter((entry) => typeof (entry === null || entry === void 0 ? void 0 : entry.udid) === 'string' && entry.udid !== '')
|
|
178
|
+
.map((entry) => nodeRemoval(entry.host, entry.nodeId, entry.udid));
|
|
179
|
+
yield (0, device_service_1.removeNodeDevices)(removals, isThisServersOwn());
|
|
158
180
|
}
|
|
159
181
|
else if (type === 'unregister') {
|
|
160
|
-
const { host } = request.query;
|
|
161
|
-
|
|
162
|
-
yield (0, device_service_1.removeDevicesByHost)(host);
|
|
163
|
-
}
|
|
182
|
+
const { host, nodeId } = request.query;
|
|
183
|
+
yield (0, device_service_1.removeNodeDevices)([nodeRemoval(host, nodeId)], isThisServersOwn());
|
|
164
184
|
}
|
|
165
185
|
response.status(200).send({
|
|
166
186
|
success: true,
|
|
@@ -0,0 +1,301 @@
|
|
|
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 __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.NODE_CONTROL_TIMEOUT_MS = exports.ANSWERED_HERE = exports.NODE_FORWARDED_CONTROL = void 0;
|
|
16
|
+
exports.safeNodeOrigin = safeNodeOrigin;
|
|
17
|
+
exports.nodeControlUrl = nodeControlUrl;
|
|
18
|
+
exports.isOtherServersPhone = isOtherServersPhone;
|
|
19
|
+
exports.nodePhoneControl = nodePhoneControl;
|
|
20
|
+
const typedi_1 = require("typedi");
|
|
21
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
22
|
+
const PluginContext_1 = require("../../PluginContext");
|
|
23
|
+
const localDeviceHosts_1 = require("../../device-managers/localDeviceHosts");
|
|
24
|
+
const actor_1 = require("../../services/device-access/actor");
|
|
25
|
+
const hubSessionToken_1 = require("../../gateway/hubSessionToken");
|
|
26
|
+
const forwardToNode_1 = require("../../gateway/forwardToNode");
|
|
27
|
+
const device_store_1 = require("../../data-service/device-store");
|
|
28
|
+
const deviceAccessPolicy_1 = require("../../services/device-access/deviceAccessPolicy");
|
|
29
|
+
const controlDevice_1 = require("../../middleware/controlDevice");
|
|
30
|
+
/**
|
|
31
|
+
* /control on another server's phone: a node's, or a cloud provider's.
|
|
32
|
+
*
|
|
33
|
+
* The handlers in control.ts act on this server's own phones, with this
|
|
34
|
+
* server's adb and go-ios. On a hub they used to run for a node's phone too,
|
|
35
|
+
* against a phone the hub doesn't have: only five actions were sent on to the
|
|
36
|
+
* node. This sits after the team and ownership guards and decides every
|
|
37
|
+
* request for another server's phone before any handler runs, so an action
|
|
38
|
+
* added later is refused for it until someone puts it on a list below.
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* Sent on to the phone's node, and the node's answer relayed unchanged. Each
|
|
42
|
+
* is about the phone alone. The preview's are too: the node holds the phone
|
|
43
|
+
* for the preview, counts its viewers and releases it, by its own rules and
|
|
44
|
+
* with the hub's user, so none of that is kept here.
|
|
45
|
+
*/
|
|
46
|
+
exports.NODE_FORWARDED_CONTROL = [
|
|
47
|
+
'POST tap',
|
|
48
|
+
'POST swipe',
|
|
49
|
+
'POST text',
|
|
50
|
+
'POST keyevent',
|
|
51
|
+
'POST touchandhold',
|
|
52
|
+
'GET screenshot',
|
|
53
|
+
'GET clipboard',
|
|
54
|
+
'POST clipboard',
|
|
55
|
+
'POST lock',
|
|
56
|
+
'POST unlock',
|
|
57
|
+
'GET display',
|
|
58
|
+
'POST uninstall',
|
|
59
|
+
'GET apps',
|
|
60
|
+
'GET logs',
|
|
61
|
+
'POST shell',
|
|
62
|
+
'GET inspector/snapshot',
|
|
63
|
+
'POST stream/start',
|
|
64
|
+
'GET stream/status',
|
|
65
|
+
'POST stream/leave',
|
|
66
|
+
'POST stream/stop',
|
|
67
|
+
// The MJPEG stream itself: relayed for as long as the viewer watches.
|
|
68
|
+
'GET stream',
|
|
69
|
+
];
|
|
70
|
+
/**
|
|
71
|
+
* Answered by this server's own handler.
|
|
72
|
+
* - appium-session: the session runs through this server, which routes its
|
|
73
|
+
* commands, so its id and base path are this server's.
|
|
74
|
+
* - stream/ticket: the viewer's ticket is this server's, minted after its
|
|
75
|
+
* team check. The stream it opens is relayed from the node: GET stream
|
|
76
|
+
* above, and the H.264 and logcat sockets (src/app/ws/nodeSocketRelay.ts),
|
|
77
|
+
* which get a ticket of the node's own.
|
|
78
|
+
*/
|
|
79
|
+
exports.ANSWERED_HERE = ['GET appium-session', 'POST stream/ticket'];
|
|
80
|
+
// Not on either list, and why, so nobody adds them as a plain forward:
|
|
81
|
+
// - upload-install is a multipart upload;
|
|
82
|
+
// - install-repository-app installs a file from this server's app library,
|
|
83
|
+
// which the node doesn't have;
|
|
84
|
+
// - install takes a path, which names a file on one machine only;
|
|
85
|
+
// - omni-scan and test-locator run this server's AI settings on a
|
|
86
|
+
// screenshot.
|
|
87
|
+
/**
|
|
88
|
+
* A node that hasn't started answering in this long is treated as gone. Once
|
|
89
|
+
* it has, the answer may take as long as it takes: a stream lasts as long as
|
|
90
|
+
* its viewer.
|
|
91
|
+
*/
|
|
92
|
+
exports.NODE_CONTROL_TIMEOUT_MS = 60000;
|
|
93
|
+
// Cloud metadata endpoints — never proxy to these regardless of caller.
|
|
94
|
+
const FORBIDDEN_PROXY_HOSTS = new Set([
|
|
95
|
+
'169.254.169.254', // AWS/Azure/GCP IMDS
|
|
96
|
+
'metadata.google.internal',
|
|
97
|
+
'metadata.goog',
|
|
98
|
+
'100.100.100.200', // Alibaba ECS metadata
|
|
99
|
+
'fd00:ec2::254', // AWS IMDSv6
|
|
100
|
+
]);
|
|
101
|
+
/**
|
|
102
|
+
* The node's origin from the phone's reported host: scheme, host and port
|
|
103
|
+
* only, dropping any path, query or fragment the host string carried. Null
|
|
104
|
+
* for a cloud-metadata target or a scheme other than http(s).
|
|
105
|
+
*/
|
|
106
|
+
function safeNodeOrigin(deviceHost) {
|
|
107
|
+
let parsed;
|
|
108
|
+
try {
|
|
109
|
+
parsed = new URL(deviceHost);
|
|
110
|
+
}
|
|
111
|
+
catch (_a) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
115
|
+
return null;
|
|
116
|
+
if (FORBIDDEN_PROXY_HOSTS.has(parsed.hostname))
|
|
117
|
+
return null;
|
|
118
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
119
|
+
}
|
|
120
|
+
/** The node's own URL for this request, or null if the host is unsafe. */
|
|
121
|
+
function nodeControlUrl(deviceHost, req) {
|
|
122
|
+
const origin = safeNodeOrigin(deviceHost);
|
|
123
|
+
if (!origin)
|
|
124
|
+
return null;
|
|
125
|
+
const forwardPath = req.originalUrl.startsWith('/') ? req.originalUrl : `/${req.originalUrl}`;
|
|
126
|
+
return `${origin}${withoutTicket(forwardPath)}`;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The path without a `ticket` query parameter. A viewer's ticket is this
|
|
130
|
+
* server's, already spent here, and not the node's business: the node gets
|
|
131
|
+
* the hub's token instead.
|
|
132
|
+
*/
|
|
133
|
+
function withoutTicket(path) {
|
|
134
|
+
const q = path.indexOf('?');
|
|
135
|
+
if (q < 0)
|
|
136
|
+
return path;
|
|
137
|
+
const params = new URLSearchParams(path.slice(q + 1));
|
|
138
|
+
if (!params.has('ticket'))
|
|
139
|
+
return path;
|
|
140
|
+
params.delete('ticket');
|
|
141
|
+
const query = params.toString();
|
|
142
|
+
return query ? `${path.slice(0, q)}?${query}` : path.slice(0, q);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* A phone another server drives. Decided by the phone's own row (its nodeId,
|
|
146
|
+
* else its exact host against this server's own hosts), never by the Host
|
|
147
|
+
* header the caller used: a substring match on that header forwarded a lab's
|
|
148
|
+
* own phone back to itself, with no login, whenever the lab was reached as
|
|
149
|
+
* `localhost`.
|
|
150
|
+
*/
|
|
151
|
+
function isOtherServersPhone(device) {
|
|
152
|
+
if (!device.host)
|
|
153
|
+
return false;
|
|
154
|
+
const context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
155
|
+
return !(0, localDeviceHosts_1.isOwnDevice)((0, localDeviceHosts_1.localDeviceHosts)(context.pluginArgs, context.port), context.nodeId, device);
|
|
156
|
+
}
|
|
157
|
+
function originOf(host) {
|
|
158
|
+
try {
|
|
159
|
+
return new URL(host).origin;
|
|
160
|
+
}
|
|
161
|
+
catch (_a) {
|
|
162
|
+
return host;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function refuse(res, error, message) {
|
|
166
|
+
return res.status(501).json({ success: false, error, message });
|
|
167
|
+
}
|
|
168
|
+
function nodePhoneControl(deps = {}) {
|
|
169
|
+
var _a, _b, _c;
|
|
170
|
+
const findDevice = (_a = deps.findDevice) !== null && _a !== void 0 ? _a : controlDevice_1.findControlDeviceInStore;
|
|
171
|
+
const timeoutMs = (_b = deps.timeoutMs) !== null && _b !== void 0 ? _b : exports.NODE_CONTROL_TIMEOUT_MS;
|
|
172
|
+
const markNodeBusy = (_c = deps.markNodeBusy) !== null && _c !== void 0 ? _c : ((udid, host) => device_store_1.DeviceStoreFactory.getStore().markNodeBusy(udid, host));
|
|
173
|
+
return function (req, res, next) {
|
|
174
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
175
|
+
var _a;
|
|
176
|
+
const action = (0, controlDevice_1.controlAction)(req);
|
|
177
|
+
if (!action)
|
|
178
|
+
return next();
|
|
179
|
+
const udid = (0, controlDevice_1.readControlUdid)(req, res, 'nodePhoneControl');
|
|
180
|
+
if (udid === null)
|
|
181
|
+
return;
|
|
182
|
+
if (!udid)
|
|
183
|
+
return next();
|
|
184
|
+
// The guards' memoized lookup: usually no query of its own.
|
|
185
|
+
let device;
|
|
186
|
+
try {
|
|
187
|
+
device = yield (0, controlDevice_1.lookupControlDevice)(res, udid, findDevice);
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
// Falling through would let the handler, with its own lookup, run a
|
|
191
|
+
// node's phone here: fail closed.
|
|
192
|
+
// Answered as every /control lookup failure is.
|
|
193
|
+
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}`);
|
|
194
|
+
return res.status(503).json((0, deviceAccessPolicy_1.ownershipUnavailableBody)());
|
|
195
|
+
}
|
|
196
|
+
if (!device || !isOtherServersPhone(device))
|
|
197
|
+
return next();
|
|
198
|
+
const key = `${req.method} ${action}`;
|
|
199
|
+
if (exports.ANSWERED_HERE.includes(key))
|
|
200
|
+
return next();
|
|
201
|
+
if (device.cloud) {
|
|
202
|
+
return refuse(res, 'not_available_for_cloud_phone', "This phone is a cloud provider's. Device control isn't available for it here.");
|
|
203
|
+
}
|
|
204
|
+
const node = originOf(device.host);
|
|
205
|
+
if (!exports.NODE_FORWARDED_CONTROL.includes(key)) {
|
|
206
|
+
return refuse(res, 'not_available_through_hub', `This phone is on node ${node}, and the hub doesn't pass ${action} on to nodes yet.`);
|
|
207
|
+
}
|
|
208
|
+
return forward(req, res, { udid, device, node, timeoutMs, markNodeBusy });
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function forward(req, res, target) {
|
|
213
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
214
|
+
var _a, _b, _c, _d, _e, _f;
|
|
215
|
+
const { udid, device, node, timeoutMs } = target;
|
|
216
|
+
const url = nodeControlUrl(device.host, req);
|
|
217
|
+
if (!url)
|
|
218
|
+
return res.status(400).json({ error: 'Unsafe device host' });
|
|
219
|
+
const actor = (0, actor_1.resolveActor)(req);
|
|
220
|
+
const headers = {
|
|
221
|
+
accept: (_a = req.headers.accept) !== null && _a !== void 0 ? _a : 'application/json',
|
|
222
|
+
'accept-encoding': 'identity',
|
|
223
|
+
};
|
|
224
|
+
// Null when this server can't sign (it warns once): the call goes without,
|
|
225
|
+
// which a node with auth disabled accepts and one with auth enabled refuses.
|
|
226
|
+
const token = actor.userId
|
|
227
|
+
? yield typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).controlTokenFor({
|
|
228
|
+
userId: actor.userId,
|
|
229
|
+
isAdmin: actor.isAdmin,
|
|
230
|
+
udid,
|
|
231
|
+
host: device.host,
|
|
232
|
+
})
|
|
233
|
+
: null;
|
|
234
|
+
if (token)
|
|
235
|
+
headers[hubSessionToken_1.HUB_TOKEN_HEADER] = token;
|
|
236
|
+
let body;
|
|
237
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
238
|
+
body = Buffer.from(JSON.stringify((_b = req.body) !== null && _b !== void 0 ? _b : {}));
|
|
239
|
+
headers['content-type'] = 'application/json';
|
|
240
|
+
}
|
|
241
|
+
const controller = new AbortController();
|
|
242
|
+
let timedOut = false;
|
|
243
|
+
const timer = setTimeout(() => {
|
|
244
|
+
timedOut = true;
|
|
245
|
+
controller.abort();
|
|
246
|
+
}, timeoutMs);
|
|
247
|
+
// The caller left: stop waiting for the node on their behalf.
|
|
248
|
+
const onClose = () => {
|
|
249
|
+
if (!res.writableFinished)
|
|
250
|
+
controller.abort();
|
|
251
|
+
};
|
|
252
|
+
res.on('close', onClose);
|
|
253
|
+
logger_1.default.debug(`Forwarding ${req.method} ${(0, controlDevice_1.controlAction)(req)} for ${udid} to node ${node}`);
|
|
254
|
+
try {
|
|
255
|
+
const upstream = yield (0, forwardToNode_1.sendToNode)({
|
|
256
|
+
url,
|
|
257
|
+
method: req.method,
|
|
258
|
+
headers,
|
|
259
|
+
body,
|
|
260
|
+
signal: controller.signal,
|
|
261
|
+
});
|
|
262
|
+
// The node has answered: from here the answer takes as long as it takes.
|
|
263
|
+
clearTimeout(timer);
|
|
264
|
+
const status = (_c = upstream.statusCode) !== null && _c !== void 0 ? _c : 502;
|
|
265
|
+
if (req.method === 'POST' && (0, controlDevice_1.controlAction)(req) === 'stream/start' && status < 300) {
|
|
266
|
+
// The node holds the phone for the preview now, and says so in its
|
|
267
|
+
// next report, up to an interval away. Until then this server could
|
|
268
|
+
// hand the phone to a session the node would refuse.
|
|
269
|
+
yield target.markNodeBusy(udid, device.host).catch((e) => {
|
|
270
|
+
var _a;
|
|
271
|
+
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}`);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
yield (0, forwardToNode_1.relayAnswer)(upstream, res, false);
|
|
275
|
+
}
|
|
276
|
+
catch (e) {
|
|
277
|
+
if (res.headersSent || res.destroyed) {
|
|
278
|
+
res.destroy();
|
|
279
|
+
}
|
|
280
|
+
else if (timedOut) {
|
|
281
|
+
res.status(504).json({
|
|
282
|
+
success: false,
|
|
283
|
+
error: 'node_timeout',
|
|
284
|
+
message: `Node ${node} didn't answer within ${Math.round(timeoutMs / 1000)} s.`,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
logger_1.default.warn(`nodePhoneControl: node ${node} unreachable for ${udid}: ${(_d = e === null || e === void 0 ? void 0 : e.message) !== null && _d !== void 0 ? _d : e}`);
|
|
289
|
+
res.status(502).json({
|
|
290
|
+
success: false,
|
|
291
|
+
error: 'node_unreachable',
|
|
292
|
+
message: `Xenon could not reach node ${node} for this phone: ${(_f = (_e = e === null || e === void 0 ? void 0 : e.code) !== null && _e !== void 0 ? _e : e === null || e === void 0 ? void 0 : e.message) !== null && _f !== void 0 ? _f : e}`,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
finally {
|
|
297
|
+
clearTimeout(timer);
|
|
298
|
+
res.off('close', onClose);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|