@xenon-device-management/xenon 2.0.0 → 2.1.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 +1 -2
- package/lib/public/assets/{AnnotationOverlay-BmTPCAds.js → AnnotationOverlay-C-Pw_hvw.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-B5eB7f8B.js → ApiKeyGate-DmYQ0oSk.js} +1 -1
- package/lib/public/assets/BugReportButton-n8T1dFUp.js +10 -0
- package/lib/public/assets/{DeviceMosaicView-9DUoZnQi.js → DeviceMosaicView-D-rGOBbJ.js} +1 -1
- package/lib/public/assets/{EmptyState-lvyNiXev.js → EmptyState-DExETevp.js} +1 -1
- package/lib/public/assets/{FieldGroup-BduMPM9K.js → FieldGroup-CCdFlZm6.js} +1 -1
- package/lib/public/assets/{FilterMenu-BSqX6I94.js → FilterMenu-DiUSOSL-.js} +1 -1
- package/lib/public/assets/{Menu-Ba5cT7jZ.js → Menu-ZOfpH6gh.js} +1 -1
- package/lib/public/assets/{Modal-CceoMY7D.js → Modal-CZNtoB-1.js} +1 -1
- package/lib/public/assets/{RecordingPage-6T9dOidc.js → RecordingPage-BKp6h8Jl.js} +1 -1
- package/lib/public/assets/{RecordingsPage-qj6FdZXR.js → RecordingsPage-Bf881FX4.js} +1 -1
- package/lib/public/assets/{SegmentedControl-CSRuZ8TR.js → SegmentedControl-EUoyizvW.js} +1 -1
- package/lib/public/assets/{SettingCard-Bp1kz4By.js → SettingCard-ClT4hf4_.js} +1 -1
- package/lib/public/assets/{Table-LV7zFyZg.js → Table-ucR4bnmc.js} +1 -1
- package/lib/public/assets/{activity-D17TUloi.js → activity-K-hdylwq.js} +1 -1
- package/lib/public/assets/{ai-settings-qvbhY89P.js → ai-settings-BLhdwI3c.js} +1 -1
- package/lib/public/assets/{api-keys-BfpvdG7G.js → api-keys-BbiN7rBI.js} +1 -1
- package/lib/public/assets/{apps-Dbnzd0fW.js → apps-Dez1petz.js} +1 -1
- package/lib/public/assets/{arrow-left-DD1VjtC7.js → arrow-left-UjmFHLAJ.js} +1 -1
- package/lib/public/assets/{arrow-right-D0pnM3ma.js → arrow-right-BVyEigbu.js} +1 -1
- package/lib/public/assets/{arrow-up-right-BIC8JOV6.js → arrow-up-right-B-ZlBof3.js} +1 -1
- package/lib/public/assets/{auth-shell-DWHPXSnI.js → auth-shell-DAQjAgUv.js} +2 -2
- package/lib/public/assets/{builds-page-ClCNBUv_.js → builds-page-CmIPa5JK.js} +1 -1
- package/lib/public/assets/{button-xtAhYR-f.js → button-Dd-IhMC1.js} +1 -1
- package/lib/public/assets/{calendar-CupwJ7Bm.js → calendar-CAIeJwa5.js} +1 -1
- package/lib/public/assets/{check-CyuEoKkd.js → check-C4Jrjg8f.js} +1 -1
- package/lib/public/assets/{chevron-left-DgX-F9xK.js → chevron-left-BGzlf-4i.js} +1 -1
- package/lib/public/assets/{chevron-right-BKJgJnU9.js → chevron-right-_wYqDjDC.js} +1 -1
- package/lib/public/assets/{circle-check-CZdg8mMa.js → circle-check-Ckn00yMB.js} +1 -1
- package/lib/public/assets/{circle-x-CZH6t9z-.js → circle-x-pdVkomLa.js} +1 -1
- package/lib/public/assets/{clock-Cd-t7xpe.js → clock-B8vmwkHj.js} +1 -1
- package/lib/public/assets/{copy-C6zerX3I.js → copy-BAqnFI-F.js} +1 -1
- package/lib/public/assets/{device-explorer-B1pI6b_M.js → device-explorer-CYq_4F_N.js} +3 -3
- package/lib/public/assets/device-explorer-D2AqrrLN.css +1 -0
- package/lib/public/assets/{download-x9WP7mcL.js → download-B6UEAp_1.js} +1 -1
- package/lib/public/assets/{forgot-password-B0TEH4N-.js → forgot-password-CLqNvmrN.js} +1 -1
- package/lib/public/assets/{funnel-Cb40MfFN.js → funnel-Da90BBbT.js} +1 -1
- package/lib/public/assets/{index-BR1wTIPC.js → index-BuIIcmGp.js} +2 -2
- package/lib/public/assets/{index-BvH_ekRy.js → index-MX7IzqyW.js} +1 -1
- package/lib/public/assets/{input-DejcCiTI.js → input-Bq9aY-We.js} +1 -1
- package/lib/public/assets/{keycodes-Mavx5iGg.js → keycodes-CIhXIJyP.js} +1 -1
- package/lib/public/assets/{layers-BKqYLiHE.js → layers-bXjpswQI.js} +1 -1
- package/lib/public/assets/{lock-LFsFhc17.js → lock-pJ72y5Fy.js} +1 -1
- package/lib/public/assets/{login-x2pkzt-t.js → login-UOcHALV2.js} +1 -1
- package/lib/public/assets/{maintenance-settings-CYafcoV-.js → maintenance-settings-XnJgp5yb.js} +1 -1
- package/lib/public/assets/{monitor-_7_uJ8d9.js → monitor-QkAhI1Jx.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-BfLuURKN.js → mouse-pointer-2-B42Da0m4.js} +1 -1
- package/lib/public/assets/{network-Z_zBhpsD.js → network-CKVqcyp6.js} +1 -1
- package/lib/public/assets/{overview-BY5uv08S.js → overview-BORGkN3N.js} +1 -1
- package/lib/public/assets/{page-header-CP2UkykG.js → page-header-CKWFOjOI.js} +1 -1
- package/lib/public/assets/{plus-BTD0dnQ0.js → plus-BpFKVdC8.js} +1 -1
- package/lib/public/assets/{profile-page-BhJZuCS7.js → profile-page-CcbKBicY.js} +1 -1
- package/lib/public/assets/{reset-password-CMN0SO81.js → reset-password-BA0W9bxl.js} +1 -1
- package/lib/public/assets/{runbook-page-CBpPX9_5.js → runbook-page-Cp7ZBq3s.js} +1 -1
- package/lib/public/assets/{select-Uc9E5pye.js → select-DIZWIoVR.js} +1 -1
- package/lib/public/assets/{selector-detail-page-BSyBnRuU.js → selector-detail-page-C_4YuvAp.js} +1 -1
- package/lib/public/assets/{selector-health-DzqxNkzm.js → selector-health-CXjD_Xth.js} +1 -1
- package/lib/public/assets/{selector-health-page-CyPLjoD1.js → selector-health-page-CTqIHeoG.js} +1 -1
- package/lib/public/assets/{session-detail-page-BZQZN1P-.js → session-detail-page-B0KYkk8z.js} +1 -1
- package/lib/public/assets/{settings-CkSWiZq8.js → settings-BcRBKUXd.js} +1 -1
- package/lib/public/assets/{status-pill-outline-BFYni2qo.js → status-pill-outline-Dh2PXSf_.js} +1 -1
- package/lib/public/assets/{tablet-BSd-xSZK.js → tablet-_sXqZztt.js} +1 -1
- package/lib/public/assets/{teams-PpoEQz31.js → teams-DGHT8MKm.js} +1 -1
- package/lib/public/assets/{trash-2-BorJv5_u.js → trash-2-BV88cV3Z.js} +1 -1
- package/lib/public/assets/{upload-C3i7sAZl.js → upload-CIDH9wkj.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-CeZQ3EsH.js → use-password-reset-mode-DYGuSNwl.js} +1 -1
- package/lib/public/assets/{users-CEJ3fC78.js → users-7yviqflc.js} +1 -1
- package/lib/public/assets/{users-Drd7ifJx.js → users-BUE7MoTq.js} +1 -1
- package/lib/public/assets/{video-off-CNzn6bPE.js → video-off-BTRxtlGo.js} +1 -1
- package/lib/public/assets/{webhook-settings-BFqX6QXV.js → webhook-settings-DMKD9Pgm.js} +1 -1
- package/lib/public/assets/{zap-4yN8Jdfw.js → zap-CWE94qcK.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 +57 -13
- package/lib/src/app/routers/grid.js +28 -8
- package/lib/src/app/ws/h264StreamWs.js +13 -8
- package/lib/src/app/ws/logcatWs.js +12 -7
- 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 +81 -10
- package/lib/src/data-service/deviceClaims.js +98 -0
- package/lib/src/data-service/deviceFieldOwners.js +32 -10
- package/lib/src/data-service/prisma-store.js +94 -31
- package/lib/src/device-managers/AndroidDeviceManager.js +13 -15
- 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 +3 -3
- package/lib/src/device-managers/android/AndroidStreamService.js +6 -5
- 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 +158 -38
- 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/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 +50 -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/lease/LeaseOrphanSweeper.js +32 -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/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/unit/AndroidDeviceManager.spec.js +8 -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 +6 -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 +18 -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 +28 -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 +291 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +594 -0
- package/lib/test/unit/hub-node-race.spec.js +401 -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 +2 -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 +92 -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 +4 -1
- package/lib/test/unit/loopback-request.spec.js +81 -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/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 +15 -7
- package/lib/test/unit/stream-own-device-row.spec.js +164 -0
- package/lib/test/unit/stream-stop-ownership.spec.js +13 -5
- package/lib/test/unit/stream-stop-recording.spec.js +13 -5
- package/lib/test/unit/stream-ticket-identity.spec.js +15 -5
- 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 +1 -2
- 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
package/lib/public/index.html
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
Learn how to configure a non-root public URL by running `npm run build`.
|
|
37
37
|
-->
|
|
38
38
|
<title>Xenon</title>
|
|
39
|
-
<script type="module" crossorigin src="/xenon/assets/index-
|
|
39
|
+
<script type="module" crossorigin src="/xenon/assets/index-BuIIcmGp.js"></script>
|
|
40
40
|
<link rel="stylesheet" crossorigin href="/xenon/assets/index-hymrhlEZ.css">
|
|
41
41
|
</head>
|
|
42
42
|
|
|
@@ -55,7 +55,8 @@ const logger_1 = __importDefault(require("./logger"));
|
|
|
55
55
|
*
|
|
56
56
|
* Features:
|
|
57
57
|
* - Keep-alive connections for performance
|
|
58
|
-
* - Automatic retry with exponential backoff
|
|
58
|
+
* - Automatic retry with exponential backoff, except for a request that sets
|
|
59
|
+
* `retry: false` (InternalRequestConfig)
|
|
59
60
|
* - Request/response logging for debugging
|
|
60
61
|
* - Correlation ID tracking
|
|
61
62
|
*/
|
|
@@ -178,6 +179,9 @@ class InternalHttpClient {
|
|
|
178
179
|
logger_1.default.warn(`[HTTP ←] ${(_c = config === null || config === void 0 ? void 0 : config.method) === null || _c === void 0 ? void 0 : _c.toUpperCase()} ${config === null || config === void 0 ? void 0 : config.url} ` +
|
|
179
180
|
`[${status || 'ERR'}] ${duration}ms - ${error.message}`);
|
|
180
181
|
}
|
|
182
|
+
if (config.retry === false) {
|
|
183
|
+
return Promise.reject(error);
|
|
184
|
+
}
|
|
181
185
|
const decision = InternalHttpClient.classifyRetry(error);
|
|
182
186
|
if (!decision.retryable || config.retryCount >= InternalHttpClient.MAX_RETRIES) {
|
|
183
187
|
return Promise.reject(error);
|
|
@@ -206,9 +206,23 @@ function iOSCapabilities(caps, freeDevice) {
|
|
|
206
206
|
deleteMatch.forEach((value) => deleteAlwaysMatch(caps, value));
|
|
207
207
|
});
|
|
208
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* The team a session asks to be placed in, or undefined. Documented as
|
|
211
|
+
* `xe:options.team` (`teamId` is read too), with `xenon:options` as the
|
|
212
|
+
* alias, through xenonOptionsOf like every other Xenon option. The flat caps
|
|
213
|
+
* older clients send (`xenon:team`, `xe:teamId`, ...) still work; an options
|
|
214
|
+
* field wins over them. `df:` is never read.
|
|
215
|
+
*
|
|
216
|
+
* The merge is field by field, so if a request spells it `team` in one
|
|
217
|
+
* namespace and `teamId` in the other, `team` wins.
|
|
218
|
+
*/
|
|
209
219
|
function extractTeamCap(caps) {
|
|
210
220
|
var _a;
|
|
211
|
-
const
|
|
221
|
+
const options = (0, xenonOptions_1.xenonOptionsOf)(caps);
|
|
222
|
+
const fromOptions = (_a = (0, xenonOptions_1.stringOption)(options, 'team')) !== null && _a !== void 0 ? _a : (0, xenonOptions_1.stringOption)(options, 'teamId');
|
|
223
|
+
if (fromOptions)
|
|
224
|
+
return fromOptions;
|
|
225
|
+
const merged = (0, xenonOptions_1.mergedFirstMatch)(caps);
|
|
212
226
|
const prefixes = ['xenon:', 'xe:', 'appium:', ''];
|
|
213
227
|
const names = ['teamId', 'team_id', 'team'];
|
|
214
228
|
for (const prefix of prefixes) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeBasePath = normalizeBasePath;
|
|
4
|
+
exports.appiumPathname = appiumPathname;
|
|
5
|
+
/**
|
|
6
|
+
* Appium's base-path rule (base-driver `normalizeBasePath`): drop one trailing
|
|
7
|
+
* '/', and add a leading '/' unless the path is empty. `cliArgs.basePath`
|
|
8
|
+
* reaches a plugin as the operator wrote it, while Appium normalises its own
|
|
9
|
+
* copy before building routes and WebSocket paths, so anything placed in front
|
|
10
|
+
* of those must normalise the same way or it would miss what it guards.
|
|
11
|
+
*/
|
|
12
|
+
function normalizeBasePath(basePath) {
|
|
13
|
+
let normalized = typeof basePath === 'string' ? basePath : '';
|
|
14
|
+
normalized = normalized.replace(/\/$/, '');
|
|
15
|
+
if (normalized !== '' && !normalized.startsWith('/'))
|
|
16
|
+
normalized = `/${normalized}`;
|
|
17
|
+
return normalized;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The pathname Appium matches an upgrade against, computed as
|
|
21
|
+
* base-driver's tryHandleWebSocketUpgrade does: WHATWG URL parsing, which
|
|
22
|
+
* resolves dot segments (also `%2e%2e`) and drops the query and an absolute
|
|
23
|
+
* form's host.
|
|
24
|
+
*/
|
|
25
|
+
function appiumPathname(url) {
|
|
26
|
+
try {
|
|
27
|
+
return new URL(url, 'http://localhost').pathname;
|
|
28
|
+
}
|
|
29
|
+
catch (_a) {
|
|
30
|
+
return url;
|
|
31
|
+
}
|
|
32
|
+
}
|
package/lib/src/app/index.js
CHANGED
|
@@ -86,6 +86,9 @@ const rateLimitMiddleware_1 = require("../middleware/rateLimitMiddleware");
|
|
|
86
86
|
const csrfMiddleware_1 = require("../middleware/csrfMiddleware");
|
|
87
87
|
const swagger_1 = require("./swagger");
|
|
88
88
|
const typedi_1 = require("typedi");
|
|
89
|
+
const PluginContext_1 = require("../PluginContext");
|
|
90
|
+
const nodeWebDriverUrl_1 = require("../gateway/nodeWebDriverUrl");
|
|
91
|
+
const nodeSessionStatus_1 = require("../gateway/nodeSessionStatus");
|
|
89
92
|
const dashboardPluginUrl = null;
|
|
90
93
|
const ASYNC_LOCK = new async_lock_1.default();
|
|
91
94
|
const router = express_1.default.Router(), apiRouter = express_1.default.Router(), staticFilesRouter = express_1.default.Router();
|
|
@@ -228,6 +231,15 @@ function createRouter(pluginArgs) {
|
|
|
228
231
|
apiRouter.use(csrfMiddleware_1.csrfMiddleware);
|
|
229
232
|
// Health endpoint: no auth, no rate limit
|
|
230
233
|
apiRouter.get('/health', (_req, res) => res.json({ ok: true }));
|
|
234
|
+
// Where this server's WebDriver API lives: a hub asks a node before sending
|
|
235
|
+
// it a session or a command (gateway/nodeWebDriverUrl.ts). No login: the
|
|
236
|
+
// hub holds no credentials for the node, and the base path is in every
|
|
237
|
+
// client's URL anyway.
|
|
238
|
+
apiRouter.get('/webdriver', (0, nodeWebDriverUrl_1.webdriverInfoHandler)(() => typedi_1.Container.get(PluginContext_1.PluginContext).nodeBasePath));
|
|
239
|
+
// On a node: whether a session exists, for the hub's heartbeat, answered
|
|
240
|
+
// without sending the session a command. No login (the hub holds no
|
|
241
|
+
// credentials for the node); it takes the hub's session token instead.
|
|
242
|
+
(0, nodeSessionStatus_1.registerNodeSessionStatus)(apiRouter, pluginArgs);
|
|
231
243
|
// Dashboard login: unauthenticated (rate-limited internally via separate IP logic)
|
|
232
244
|
apiRouter.use('/auth', (0, auth_1.authPublicRouter)()); // login, logout — unauthenticated
|
|
233
245
|
// All remaining /api/* requires authentication (cookie session for
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.routerStackOf = routerStackOf;
|
|
4
4
|
exports.insertBeforeRoutes = insertBeforeRoutes;
|
|
5
|
+
exports.insertAtStart = insertAtStart;
|
|
5
6
|
function isRouter(candidate) {
|
|
6
7
|
return !!candidate && Array.isArray(candidate.stack);
|
|
7
8
|
}
|
|
@@ -28,7 +29,11 @@ function routerStackOf(app) {
|
|
|
28
29
|
var _a;
|
|
29
30
|
return (_a = findRouter(app)) === null || _a === void 0 ? void 0 : _a.router.stack;
|
|
30
31
|
}
|
|
31
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Let `app.use()` build one layer, then move it to `positionOf(stack)` (the
|
|
34
|
+
* stack without the new layer).
|
|
35
|
+
*/
|
|
36
|
+
function spliceNewLayer(app, use, positionOf) {
|
|
32
37
|
const found = findRouter(app);
|
|
33
38
|
if (!found) {
|
|
34
39
|
return {
|
|
@@ -38,7 +43,7 @@ function insertBeforeRoutes(app, path, handler) {
|
|
|
38
43
|
}
|
|
39
44
|
const { stack } = found.router;
|
|
40
45
|
const lengthBefore = stack.length;
|
|
41
|
-
|
|
46
|
+
use();
|
|
42
47
|
const added = stack.splice(lengthBefore);
|
|
43
48
|
if (added.length !== 1) {
|
|
44
49
|
// app.use() added its layer somewhere other than the stack we found, or
|
|
@@ -50,8 +55,23 @@ function insertBeforeRoutes(app, path, handler) {
|
|
|
50
55
|
reason: `app.use() added ${added.length} layers to the router stack, expected 1`,
|
|
51
56
|
};
|
|
52
57
|
}
|
|
53
|
-
const
|
|
54
|
-
const index = firstRoute === -1 ? stack.length : firstRoute;
|
|
58
|
+
const index = positionOf(stack);
|
|
55
59
|
stack.splice(index, 0, added[0]);
|
|
56
60
|
return { placed: true, shape: found.shape, index };
|
|
57
61
|
}
|
|
62
|
+
function insertBeforeRoutes(app, path, handler) {
|
|
63
|
+
return spliceNewLayer(app, () => app.use(path, handler), (stack) => {
|
|
64
|
+
const firstRoute = stack.findIndex((layer) => !!layer.route);
|
|
65
|
+
return firstRoute === -1 ? stack.length : firstRoute;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Put a path-less middleware at the very front of the router stack, ahead of
|
|
70
|
+
* middleware as well as routes. For a check that must run before one of
|
|
71
|
+
* Appium's own middlewares (its WebSocket `handleUpgrade`), which sit ahead of
|
|
72
|
+
* the routes. On Express 4 that is also ahead of `query` and `expressInit`, so
|
|
73
|
+
* the handler must use only Node's own request and response API.
|
|
74
|
+
*/
|
|
75
|
+
function insertAtStart(app, handler) {
|
|
76
|
+
return spliceNewLayer(app, () => app.use(handler), () => 0);
|
|
77
|
+
}
|
|
@@ -1,67 +1,186 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
3
|
+
var t = {};
|
|
4
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
5
|
+
t[p] = s[p];
|
|
6
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
7
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
8
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
9
|
+
t[p[i]] = s[p[i]];
|
|
10
|
+
}
|
|
11
|
+
return t;
|
|
12
|
+
};
|
|
2
13
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
14
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
15
|
};
|
|
5
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.normalizeBasePath =
|
|
17
|
+
exports.normalizeBasePath = void 0;
|
|
7
18
|
exports.sessionCommandPath = sessionCommandPath;
|
|
19
|
+
exports.sessionListingPath = sessionListingPath;
|
|
20
|
+
exports.commandAuthDeps = commandAuthDeps;
|
|
21
|
+
exports.sessionCreatePath = sessionCreatePath;
|
|
22
|
+
exports.registerSessionGateway = registerSessionGateway;
|
|
8
23
|
exports.registerCommandAuth = registerCommandAuth;
|
|
24
|
+
exports.registerSessionUpgradeGuard = registerSessionUpgradeGuard;
|
|
9
25
|
const typedi_1 = require("typedi");
|
|
10
26
|
const logger_1 = __importDefault(require("../logger"));
|
|
11
27
|
const config_1 = require("../config");
|
|
12
28
|
const SessionOwnerResolver_1 = require("../services/device-access/SessionOwnerResolver");
|
|
13
29
|
const commandAuth_1 = require("../middleware/commandAuth");
|
|
14
30
|
const commandCaller_1 = require("../middleware/commandCaller");
|
|
31
|
+
const sessionListingFilter_1 = require("../middleware/sessionListingFilter");
|
|
32
|
+
const sessionUpgradeGuard_1 = require("../middleware/sessionUpgradeGuard");
|
|
33
|
+
const internalCall_1 = require("../gateway/internalCall");
|
|
34
|
+
const sessionGateway_1 = require("../gateway/sessionGateway");
|
|
35
|
+
const sessionCreate_1 = require("../gateway/sessionCreate");
|
|
36
|
+
const appiumBasePath_1 = require("./appiumBasePath");
|
|
37
|
+
Object.defineProperty(exports, "normalizeBasePath", { enumerable: true, get: function () { return appiumBasePath_1.normalizeBasePath; } });
|
|
15
38
|
const insertBeforeRoutes_1 = require("./insertBeforeRoutes");
|
|
16
|
-
/**
|
|
17
|
-
* Appium's base-path rule (base-driver `normalizeBasePath`): drop one trailing
|
|
18
|
-
* '/', and add a leading '/' unless the path is empty. `cliArgs.basePath`
|
|
19
|
-
* reaches a plugin as the operator wrote it, while Appium normalises its own
|
|
20
|
-
* copy before building routes, so the guard must normalise the same way or it
|
|
21
|
-
* would miss the routes it is meant to sit in front of.
|
|
22
|
-
*/
|
|
23
|
-
function normalizeBasePath(basePath) {
|
|
24
|
-
let normalized = typeof basePath === 'string' ? basePath : '';
|
|
25
|
-
normalized = normalized.replace(/\/$/, '');
|
|
26
|
-
if (normalized !== '' && !normalized.startsWith('/'))
|
|
27
|
-
normalized = `/${normalized}`;
|
|
28
|
-
return normalized;
|
|
29
|
-
}
|
|
30
39
|
/**
|
|
31
40
|
* Everything under `<basePath>/session/:sessionId`, every method. The create
|
|
32
41
|
* route (`POST <basePath>/session`) has no id segment and does not match.
|
|
33
42
|
*/
|
|
34
43
|
function sessionCommandPath(basePath) {
|
|
35
|
-
return `${normalizeBasePath(basePath)}/session/:sessionId`;
|
|
44
|
+
return `${(0, appiumBasePath_1.normalizeBasePath)(basePath)}/session/:sessionId`;
|
|
45
|
+
}
|
|
46
|
+
/** Appium 3's session listing route (base-driver `getAppiumSessions`). */
|
|
47
|
+
function sessionListingPath(basePath) {
|
|
48
|
+
return `${(0, appiumBasePath_1.normalizeBasePath)(basePath)}/appium/sessions`;
|
|
36
49
|
}
|
|
37
50
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
51
|
+
* The dependencies per-command auth runs with. ServerManager builds one set
|
|
52
|
+
* and hands it to both registrations below, so commands, the session listing
|
|
53
|
+
* and session WebSockets share one credential cache.
|
|
54
|
+
*/
|
|
55
|
+
function commandAuthDeps(overrides = {}) {
|
|
56
|
+
var _a, _b, _c, _d, _e, _f;
|
|
57
|
+
return {
|
|
58
|
+
enabled: (_a = overrides.enabled) !== null && _a !== void 0 ? _a : (() => (0, commandAuth_1.commandAuthEnabled)()),
|
|
59
|
+
authDisabled: (_b = overrides.authDisabled) !== null && _b !== void 0 ? _b : (() => config_1.config.authDisabled === true),
|
|
60
|
+
verifier: (_c = overrides.verifier) !== null && _c !== void 0 ? _c : new commandCaller_1.CommandCallerVerifier(),
|
|
61
|
+
ownerOf: (_d = overrides.ownerOf) !== null && _d !== void 0 ? _d : ((sessionId) => typedi_1.Container.get(SessionOwnerResolver_1.SessionOwnerResolver).ownerOf(sessionId)),
|
|
62
|
+
ownersOf: (_e = overrides.ownersOf) !== null && _e !== void 0 ? _e : ((sessionIds) => typedi_1.Container.get(SessionOwnerResolver_1.SessionOwnerResolver).ownersOf(sessionIds)),
|
|
63
|
+
logger: (_f = overrides.logger) !== null && _f !== void 0 ? _f : logger_1.default.scope('CommandAuth'),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const refuseToStart = (what) => new Error(`XENON_REQUIRE_COMMAND_AUTH is on, but ${what}. Refusing to start rather than ` +
|
|
67
|
+
'serve sessions unchecked.');
|
|
68
|
+
/** The create route (`POST <basePath>/session`), where the create layer is mounted. */
|
|
69
|
+
function sessionCreatePath(basePath) {
|
|
70
|
+
return `${(0, appiumBasePath_1.normalizeBasePath)(basePath)}/session`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Install the session gateway (gateway/sessionGateway.ts) and the
|
|
74
|
+
* session-listing filter (sessionListingFilter.ts) ahead of Appium's routes.
|
|
75
|
+
* Called from ServerManager.registerRoutes, which Appium runs after adding
|
|
76
|
+
* them. The gateway is three adjacent layers:
|
|
77
|
+
*
|
|
78
|
+
* 1. the internal-call layer (gateway/internalCall.ts), path-less, which turns
|
|
79
|
+
* Xenon's own `<basePath>/wd-internal/...` calls, when they carry this
|
|
80
|
+
* process's secret, into the plain session command;
|
|
81
|
+
* 2. with `options.create`, the create layer at `<basePath>/session`
|
|
82
|
+
* (gateway/sessionCreate.ts), which allocates the phone for
|
|
83
|
+
* `POST <basePath>/session` and creates a remote phone's session itself;
|
|
84
|
+
* 3. the session layer at `<basePath>/session/:sessionId`: per-command auth,
|
|
85
|
+
* then, on a hub, forwarding of the sessions other servers run.
|
|
86
|
+
*
|
|
87
|
+
* The first comes first, so the others see an internal call by its real path
|
|
88
|
+
* and know it for one. It is path-less because Express puts a mount path back
|
|
89
|
+
* onto the URL when a mounted layer calls next(), which would undo the strip.
|
|
90
|
+
* The others are mounted, so their path matching is the router's own.
|
|
91
|
+
*
|
|
92
|
+
* All of it is always installed and reads the setting per request, so a
|
|
93
|
+
* server with per-command auth off pays one env check per session command and
|
|
94
|
+
* does no lookups for it. If a layer cannot be placed ahead of the routes the
|
|
95
|
+
* error is logged, and with the setting on (and auth enabled) the server
|
|
96
|
+
* refuses to start: an operator who asked for the check must not get a server
|
|
97
|
+
* that silently serves session commands without it.
|
|
40
98
|
*
|
|
41
|
-
*
|
|
42
|
-
* server with the setting off pays one env check per session command and does
|
|
43
|
-
* no lookups. If it cannot be placed ahead of the routes the error is logged,
|
|
44
|
-
* and with the setting on (and auth enabled) the server refuses to start:
|
|
45
|
-
* an operator who asked for the check must not get a server that silently
|
|
46
|
-
* serves session commands without it.
|
|
99
|
+
* Returns where the session layer was placed.
|
|
47
100
|
*/
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
const path = sessionCommandPath(cliArgs === null || cliArgs === void 0 ? void 0 : cliArgs.basePath);
|
|
51
|
-
const result = (0, insertBeforeRoutes_1.insertBeforeRoutes)(app, path, (0, commandAuth_1.createCommandAuthMiddleware)(deps));
|
|
101
|
+
function registerSessionGateway(app, cliArgs, deps, options = {}) {
|
|
102
|
+
var _a;
|
|
52
103
|
const active = deps.enabled() && !deps.authDisabled();
|
|
104
|
+
const basePath = (0, appiumBasePath_1.normalizeBasePath)(cliArgs === null || cliArgs === void 0 ? void 0 : cliArgs.basePath);
|
|
105
|
+
const { create } = options, gateway = __rest(options, ["create"]);
|
|
106
|
+
const path = sessionCommandPath(basePath);
|
|
107
|
+
const internal = (0, insertBeforeRoutes_1.insertBeforeRoutes)(app, '/', (0, internalCall_1.createInternalCallLayer)(basePath));
|
|
108
|
+
const created = internal.placed && create
|
|
109
|
+
? (0, insertBeforeRoutes_1.insertBeforeRoutes)(app, sessionCreatePath(basePath), (0, sessionCreate_1.createSessionCreateLayer)(Object.assign(Object.assign({}, create), { authDisabled: deps.authDisabled, logger: (_a = create.logger) !== null && _a !== void 0 ? _a : deps.logger })))
|
|
110
|
+
: internal;
|
|
111
|
+
const result = created.placed
|
|
112
|
+
? (0, insertBeforeRoutes_1.insertBeforeRoutes)(app, path, (0, sessionGateway_1.createSessionGatewayLayer)(Object.assign(Object.assign({}, gateway), { auth: deps })))
|
|
113
|
+
: created;
|
|
53
114
|
if (!result.placed) {
|
|
54
115
|
deps.logger.error(`Per-command auth could not be placed ahead of Appium's routes for ${path}: ${result.reason}.`);
|
|
55
116
|
if (active) {
|
|
56
|
-
throw
|
|
57
|
-
|
|
58
|
-
|
|
117
|
+
throw refuseToStart("the per-command check could not be placed ahead of Appium's WebDriver routes, " +
|
|
118
|
+
'so it would never run');
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
const listingPath = sessionListingPath(cliArgs === null || cliArgs === void 0 ? void 0 : cliArgs.basePath);
|
|
123
|
+
const listing = (0, insertBeforeRoutes_1.insertBeforeRoutes)(app, listingPath, (0, sessionListingFilter_1.createSessionListingFilter)(deps));
|
|
124
|
+
if (!listing.placed) {
|
|
125
|
+
deps.logger.error("The session-listing filter could not be placed ahead of Appium's routes for " +
|
|
126
|
+
`${listingPath}: ${listing.reason}.`);
|
|
127
|
+
if (active) {
|
|
128
|
+
throw refuseToStart(`the session-listing filter could not be placed ahead of ${listingPath}`);
|
|
59
129
|
}
|
|
60
130
|
return result;
|
|
61
131
|
}
|
|
62
132
|
deps.logger.info(active
|
|
63
133
|
? `Per-command auth is ON: every request under ${path} needs the session owner's ` +
|
|
64
|
-
|
|
65
|
-
|
|
134
|
+
`credentials or an override admin, and ${listingPath} lists only the caller's ` +
|
|
135
|
+
'sessions (XENON_REQUIRE_COMMAND_AUTH).'
|
|
136
|
+
: `Per-command auth is off for ${path} and ${listingPath} ` +
|
|
137
|
+
'(XENON_REQUIRE_COMMAND_AUTH not set, or auth disabled).');
|
|
66
138
|
return result;
|
|
67
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Per-command auth (commandAuth.ts) and the session-listing filter, through the
|
|
142
|
+
* session gateway with no routing: a server that runs every session itself.
|
|
143
|
+
*/
|
|
144
|
+
function registerCommandAuth(app, cliArgs, overrides = {}) {
|
|
145
|
+
return registerSessionGateway(app, cliArgs, commandAuthDeps(overrides));
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Install the session WebSocket guard (sessionUpgradeGuard.ts) on both of the
|
|
149
|
+
* paths Appium can take an upgrade by: the http.Server's `upgrade` event and
|
|
150
|
+
* the front of the Express stack. Called from ServerManager.updateServer with
|
|
151
|
+
* the http.Server Appium hands plugins.
|
|
152
|
+
*
|
|
153
|
+
* Like registerCommandAuth, it is always installed and reads the setting per
|
|
154
|
+
* upgrade; with the setting off every upgrade passes straight through. If
|
|
155
|
+
* either hook cannot be installed the error is logged, and with the setting on
|
|
156
|
+
* the server refuses to start.
|
|
157
|
+
*/
|
|
158
|
+
function registerSessionUpgradeGuard(httpServer, app, cliArgs, overrides = {}) {
|
|
159
|
+
const deps = commandAuthDeps(overrides);
|
|
160
|
+
const active = deps.enabled() && !deps.authDisabled();
|
|
161
|
+
const basePath = (0, appiumBasePath_1.normalizeBasePath)(cliArgs === null || cliArgs === void 0 ? void 0 : cliArgs.basePath);
|
|
162
|
+
const guard = (0, sessionUpgradeGuard_1.createSessionUpgradeGuard)(basePath, deps);
|
|
163
|
+
const problems = [];
|
|
164
|
+
if (httpServer && typeof httpServer.emit === 'function') {
|
|
165
|
+
(0, sessionUpgradeGuard_1.guardUpgradeEvents)(httpServer, guard);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
problems.push('no HTTP server was given to guard');
|
|
169
|
+
}
|
|
170
|
+
const front = (0, insertBeforeRoutes_1.insertAtStart)(app, guard.middleware);
|
|
171
|
+
if (!front.placed)
|
|
172
|
+
problems.push(`the Express guard was not placed: ${front.reason}`);
|
|
173
|
+
if (problems.length > 0) {
|
|
174
|
+
const reason = problems.join('; ');
|
|
175
|
+
deps.logger.error(`The session WebSocket guard could not be installed: ${reason}.`);
|
|
176
|
+
if (active) {
|
|
177
|
+
throw refuseToStart(`session WebSockets (${basePath}/bidi/<id>, /ws/session/<id>/...) could not be guarded`);
|
|
178
|
+
}
|
|
179
|
+
return { placed: false, shape: front.shape, reason };
|
|
180
|
+
}
|
|
181
|
+
deps.logger.info(active
|
|
182
|
+
? `Session WebSocket guard is ON: upgrades to ${basePath}/bidi/:sessionId and ` +
|
|
183
|
+
"/ws/session/:sessionId/... need the session owner's credentials or an override admin."
|
|
184
|
+
: 'Session WebSocket guard is off (XENON_REQUIRE_COMMAND_AUTH not set, or auth disabled).');
|
|
185
|
+
return front;
|
|
186
|
+
}
|
|
@@ -68,6 +68,7 @@ const InspectorService_1 = require("../../services/InspectorService");
|
|
|
68
68
|
const StreamTicketService_1 = require("../../services/token/StreamTicketService");
|
|
69
69
|
const appVisibility_1 = require("../../services/device-access/appVisibility");
|
|
70
70
|
const PluginContext_1 = require("../../PluginContext");
|
|
71
|
+
const localDeviceHosts_1 = require("../../device-managers/localDeviceHosts");
|
|
71
72
|
const streamType_1 = require("./streamType");
|
|
72
73
|
const iosStreamPort_1 = require("./iosStreamPort");
|
|
73
74
|
const androidH264Config_1 = require("./androidH264Config");
|
|
@@ -83,6 +84,7 @@ const streamStartConflict_1 = require("./streamStartConflict");
|
|
|
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");
|
|
87
|
+
const hubSessionToken_1 = require("../../gateway/hubSessionToken");
|
|
86
88
|
const deviceAccessPolicy_1 = require("../../services/device-access/deviceAccessPolicy");
|
|
87
89
|
const router = (0, express_1.Router)();
|
|
88
90
|
/** True when an in-memory iOS/Android (MJPEG or H.264) stream session exists. */
|
|
@@ -141,6 +143,45 @@ function buildProxyUrl(deviceHost, req) {
|
|
|
141
143
|
const forwardPath = req.originalUrl.startsWith('/') ? req.originalUrl : `/${req.originalUrl}`;
|
|
142
144
|
return `${origin}${forwardPath}`;
|
|
143
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Forward a control action on another server's phone to that server, signed
|
|
148
|
+
* for the caller: the hub's control token (hubSessionToken.ts) names the
|
|
149
|
+
* user, whether they are an admin, the phone and its node. This hub already
|
|
150
|
+
* checked them (the guards above); a node with auth enabled accepts the token
|
|
151
|
+
* for /control on that phone and runs its own ownership guard with that user.
|
|
152
|
+
* Sent with no credential at all, it was refused by every such node. A cloud
|
|
153
|
+
* provider never gets a hub token.
|
|
154
|
+
*/
|
|
155
|
+
function forwardControl(device, req, target) {
|
|
156
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
157
|
+
const actor = (0, actor_1.resolveActor)(req);
|
|
158
|
+
const token = !device.cloud && actor.userId
|
|
159
|
+
? yield typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).controlTokenFor({
|
|
160
|
+
userId: actor.userId,
|
|
161
|
+
isAdmin: actor.isAdmin,
|
|
162
|
+
udid: device.udid,
|
|
163
|
+
host: device.host,
|
|
164
|
+
})
|
|
165
|
+
: null;
|
|
166
|
+
// Sent once: a retry after a slow node's timeout or 5xx could land the
|
|
167
|
+
// same tap twice.
|
|
168
|
+
const config = Object.assign(Object.assign({}, (token ? { headers: { [hubSessionToken_1.HUB_TOKEN_HEADER]: token } } : {})), { retry: false });
|
|
169
|
+
yield InternalHttpClient_1.InternalHttpClient.post(target, req.body, config);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* A phone another server drives, whose /control actions are forwarded there.
|
|
174
|
+
* Decided by the phone's own row (its nodeId, else its exact host against this
|
|
175
|
+
* server's own hosts), never by the Host header the caller used: a substring
|
|
176
|
+
* match on that header forwarded a lab's own phone back to itself, with no
|
|
177
|
+
* login, whenever the lab was reached as `localhost`.
|
|
178
|
+
*/
|
|
179
|
+
function isOtherServersPhone(device) {
|
|
180
|
+
if (!device.host)
|
|
181
|
+
return false;
|
|
182
|
+
const context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
183
|
+
return !(0, localDeviceHosts_1.isOwnDevice)((0, localDeviceHosts_1.localDeviceHosts)(context.pluginArgs, context.port), context.nodeId, device);
|
|
184
|
+
}
|
|
144
185
|
function getDeviceInfo(udid) {
|
|
145
186
|
return __awaiter(this, void 0, void 0, function* () {
|
|
146
187
|
const device = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
|
|
@@ -179,13 +220,13 @@ router.post('/:udid/tap', (req, res) => __awaiter(void 0, void 0, void 0, functi
|
|
|
179
220
|
if (!device)
|
|
180
221
|
return res.status(404).send('Device not found');
|
|
181
222
|
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
182
|
-
if (device
|
|
223
|
+
if (isOtherServersPhone(device)) {
|
|
183
224
|
const target = buildProxyUrl(device.host, req);
|
|
184
225
|
if (!target)
|
|
185
226
|
return res.status(400).send({ error: 'Unsafe device host' });
|
|
186
227
|
logger_1.default.info(`Proxying tap for ${udid} to ${target}`);
|
|
187
228
|
try {
|
|
188
|
-
yield
|
|
229
|
+
yield forwardControl(device, req, target);
|
|
189
230
|
return res.status(200).send({ success: true });
|
|
190
231
|
}
|
|
191
232
|
catch (err) {
|
|
@@ -223,13 +264,13 @@ router.post('/:udid/swipe', (req, res) => __awaiter(void 0, void 0, void 0, func
|
|
|
223
264
|
if (!device)
|
|
224
265
|
return res.status(404).send('Device not found');
|
|
225
266
|
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
226
|
-
if (device
|
|
267
|
+
if (isOtherServersPhone(device)) {
|
|
227
268
|
const target = buildProxyUrl(device.host, req);
|
|
228
269
|
if (!target)
|
|
229
270
|
return res.status(400).send({ error: 'Unsafe device host' });
|
|
230
271
|
logger_1.default.info(`Proxying swipe for ${udid} to ${target}`);
|
|
231
272
|
try {
|
|
232
|
-
yield
|
|
273
|
+
yield forwardControl(device, req, target);
|
|
233
274
|
return res.status(200).send({ success: true });
|
|
234
275
|
}
|
|
235
276
|
catch (err) {
|
|
@@ -256,13 +297,13 @@ router.post('/:udid/text', (req, res) => __awaiter(void 0, void 0, void 0, funct
|
|
|
256
297
|
if (!device)
|
|
257
298
|
return res.status(404).send('Device not found');
|
|
258
299
|
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
259
|
-
if (device
|
|
300
|
+
if (isOtherServersPhone(device)) {
|
|
260
301
|
const target = buildProxyUrl(device.host, req);
|
|
261
302
|
if (!target)
|
|
262
303
|
return res.status(400).send({ error: 'Unsafe device host' });
|
|
263
304
|
logger_1.default.info(`Proxying typeText for ${udid} to ${target}`);
|
|
264
305
|
try {
|
|
265
|
-
yield
|
|
306
|
+
yield forwardControl(device, req, target);
|
|
266
307
|
return res.status(200).send({ success: true });
|
|
267
308
|
}
|
|
268
309
|
catch (err) {
|
|
@@ -289,13 +330,13 @@ router.post('/:udid/keyevent', (req, res) => __awaiter(void 0, void 0, void 0, f
|
|
|
289
330
|
if (!device)
|
|
290
331
|
return res.status(404).send('Device not found');
|
|
291
332
|
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
292
|
-
if (device
|
|
333
|
+
if (isOtherServersPhone(device)) {
|
|
293
334
|
const target = buildProxyUrl(device.host, req);
|
|
294
335
|
if (!target)
|
|
295
336
|
return res.status(400).send({ error: 'Unsafe device host' });
|
|
296
337
|
logger_1.default.info(`Proxying keyevent for ${udid} to ${target}`);
|
|
297
338
|
try {
|
|
298
|
-
yield
|
|
339
|
+
yield forwardControl(device, req, target);
|
|
299
340
|
return res.status(200).send({ success: true });
|
|
300
341
|
}
|
|
301
342
|
catch (err) {
|
|
@@ -392,13 +433,13 @@ router.post('/:udid/touchAndHold', (req, res) => __awaiter(void 0, void 0, void
|
|
|
392
433
|
if (!device)
|
|
393
434
|
return res.status(404).send('Device not found');
|
|
394
435
|
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
395
|
-
if (device
|
|
436
|
+
if (isOtherServersPhone(device)) {
|
|
396
437
|
const target = buildProxyUrl(device.host, req);
|
|
397
438
|
if (!target)
|
|
398
439
|
return res.status(400).send({ error: 'Unsafe device host' });
|
|
399
440
|
logger_1.default.info(`Proxying touchAndHold for ${udid} to ${target}`);
|
|
400
441
|
try {
|
|
401
|
-
yield
|
|
442
|
+
yield forwardControl(device, req, target);
|
|
402
443
|
return res.status(200).send({ success: true });
|
|
403
444
|
}
|
|
404
445
|
catch (err) {
|
|
@@ -800,7 +841,9 @@ router.post('/:udid/stream/ticket', (req, res) => __awaiter(void 0, void 0, void
|
|
|
800
841
|
function stopPreview(udid, device, release) {
|
|
801
842
|
return __awaiter(this, void 0, void 0, function* () {
|
|
802
843
|
if (device.platform === 'ios' || device.platform === 'tvos') {
|
|
803
|
-
|
|
844
|
+
// A viewer's stop: an Appium session on the phone may be driving this
|
|
845
|
+
// stream's WDA, and then the stream stays (see IOSStreamService.stopStream).
|
|
846
|
+
yield typedi_1.Container.get(IOSStreamService_1.default).stopStream(udid, { forViewer: true });
|
|
804
847
|
}
|
|
805
848
|
else {
|
|
806
849
|
yield typedi_1.Container.get(AndroidStreamService_1.default).stopStream(udid);
|
|
@@ -1026,8 +1069,9 @@ router.get('/:udid/stream', (req, res) => __awaiter(void 0, void 0, void 0, func
|
|
|
1026
1069
|
getSession: (id) => iosStreamService.getStreamStatus(id),
|
|
1027
1070
|
// Single attempt (retries = 0): a healthy WDA answers in milliseconds,
|
|
1028
1071
|
// so this costs nothing on the happy path and stays bounded by the
|
|
1029
|
-
// 2.5s /status timeout when WDA is dead.
|
|
1030
|
-
|
|
1072
|
+
// 2.5s /status timeout when WDA is dead. No udid: WDA names no phone
|
|
1073
|
+
// (see isWDARunning). The port is this phone's stream's own lease.
|
|
1074
|
+
isWdaHealthy: (wdaPort) => iosStreamService.isWDARunning(wdaPort, 0),
|
|
1031
1075
|
startStream: (id) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1032
1076
|
var _a;
|
|
1033
1077
|
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,
|