Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions apps/mobile/src/features/devices/DeviceStreamWebView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { AppActivityIndicator } from "../../components/ui/AppActivityIndicator";
import { AppText } from "../../components/ui/AppText";

import {
deviceStreamBaseUrl,
deviceStreamDocument,
deviceStreamMessage,
type DeviceStreamConfiguration,
Expand Down Expand Up @@ -51,6 +52,7 @@ export function DeviceStreamWebView({
key={`${attempt}:${configuration}`}
ref={ref}
configuration={configuration}
baseUrl={deviceStreamBaseUrl(Platform.OS, props.platform, props.access.httpBase)}
background={props.colors.background}
onUnauthorized={props.onUnauthorized}
onInputConnected={props.onInputConnected}
Expand All @@ -76,6 +78,7 @@ export function DeviceStreamWebView({
function DeviceStreamDocumentView({
ref,
configuration,
baseUrl,
background,
onUnauthorized,
onInputConnected,
Expand All @@ -84,6 +87,7 @@ function DeviceStreamDocumentView({
onRecoverProcess,
}: NativeStreamBridge & {
readonly configuration: string;
readonly baseUrl: string;
readonly background: string;
readonly onRetry: () => void;
readonly onStreaming: () => void;
Expand Down Expand Up @@ -113,8 +117,7 @@ function DeviceStreamDocumentView({
}, [started]);
const source = {
html: deviceStreamDocument(configuration, deviceStreamScript),
// Android WebCodecs needs a secure document; streams still use the environment's URLs.
baseUrl: Platform.OS === "android" ? "https://localhost/" : "file:///",
baseUrl,
};
const command = (button: keyof DeviceStreamRef) => {
webView.current?.injectJavaScript(
Expand Down
18 changes: 17 additions & 1 deletion apps/mobile/src/features/devices/device-stream-document.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
import * as NodeVM from "node:vm";
import { describe, expect, it, vi } from "vite-plus/test";

import { deviceStreamDocument, deviceStreamMessage } from "./device-stream-document";
import {
deviceStreamBaseUrl,
deviceStreamDocument,
deviceStreamMessage,
} from "./device-stream-document";

describe("native device stream document", () => {
it.each([
["android", "ios", "http://192.168.1.10:3773/api/device-hub", "http://192.168.1.10:3773/"],
["android", "ios", "https://env.example/t3/api/device-hub", "https://env.example/"],
["android", "android", "http://192.168.1.10:3773/api/device-hub", "https://localhost/"],
["ios", "ios", "http://192.168.1.10:3773/api/device-hub", "file:///"],
] as const)(
"uses a %s viewer base URL for %s targets on %s",
(viewer, target, httpBase, expected) => {
expect(deviceStreamBaseUrl(viewer, target, httpBase)).toBe(expected);
},
);

it("keeps ticket and device values from terminating the embedded script", () => {
const deviceId = '</script><script>alert("device")</script>';
const configuration = JSON.stringify({ deviceId, ticket: "<ticket>" });
Expand Down
14 changes: 14 additions & 0 deletions apps/mobile/src/features/devices/device-stream-document.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { DeviceHubAccess } from "@t3tools/client-runtime/device/hub-access";
import type { DevicePlatform } from "@t3tools/contracts";
import type { PlatformOSType } from "react-native";

export interface DeviceStreamConfiguration {
readonly access: DeviceHubAccess;
Expand All @@ -15,6 +16,19 @@ export interface DeviceStreamConfiguration {
};
}

export function deviceStreamBaseUrl(
viewerPlatform: PlatformOSType,
devicePlatform: DevicePlatform,
httpBase: string,
) {
if (viewerPlatform !== "android") return "file:///";
// Android targets stream through WebCodecs, which needs a secure context. iOS targets
// stream an MJPEG image from the environment, which a synthetic https document blocks as
// mixed content on plain-http environments even with mixedContentMode="always".
if (devicePlatform === "android") return "https://localhost/";
return `${new URL(httpBase).origin}/`;
}

export function deviceStreamDocument(configuration: string, script: string) {
const safeConfiguration = configuration.replace(/</g, "\\u003c");
const safeScript = script.replace(/<\/script/gi, "<\\/script");
Expand Down
Loading