Skip to content
Open
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
12 changes: 12 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Native sources are hashed by tool/cef_host_hash.sh to key the prebuilt
# cef_host artifact in GCS. The hash is over RAW BYTES, so a Windows checkout
# with core.autocrlf=true would compute a different digest than the macOS
# publisher for the same commit — forever missing the prebuilt. Pin LF for
# everything the hash covers (and shell scripts, which break under CRLF).
packages/**/native/** text eol=lf
packages/**/tool/*.sh text eol=lf
*.sh text eol=lf
# Windows batch files MUST be CRLF (cmd.exe misparses LF-only .bat, dropping
# characters). This overrides the native/** LF pin above for *.bat only — the
# hash key stays deterministic because CRLF is pinned on BOTH platforms.
*.bat text eol=crlf
31 changes: 31 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jobs:
flutter analyze
(cd packages/flutter_cef_platform_interface && flutter pub get && flutter analyze)
(cd packages/flutter_cef_macos && flutter pub get && flutter analyze)
(cd packages/flutter_cef_windows && flutter pub get && flutter analyze)
(cd example && flutter pub get && flutter analyze)
- name: Test
run: flutter test
Expand All @@ -35,3 +36,33 @@ jobs:
# swiftc suite runs without Xcode/CocoaPods. (A regression here previously went
# unnoticed precisely because CI did not run it.)
run: ./packages/flutter_cef_macos/test/run_filter_tests.sh

# Windows: analyze the endorsed windows implementation + build the example so
# the ~5.5k lines of native plugin/cef_host/CDP-relay code (profiles, cookies,
# JS bridge, agent control) are actually compiled by CI, not merged blind.
windows-build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: 3.38.8
channel: stable
- run: flutter --version
- name: Analyze (windows implementation package)
shell: bash
run: |
flutter pub get
flutter analyze
(cd packages/flutter_cef_windows && flutter pub get && flutter analyze)
- name: Dart tests (includes the Windows key/accelerator + verb twins)
run: flutter test
- name: Build example for Windows (compiles the plugin + cef_host)
shell: bash
# CEF (~200 MB) is fetched by the plugin CMake from the pinned dist; if
# the fetch/build is unavailable on the runner this step surfaces it
# rather than letting native regressions merge unbuilt.
run: |
cd example
flutter pub get
flutter build windows --debug
13 changes: 9 additions & 4 deletions PORTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,12 @@ lives in the Flutter app's process and:
- Spawns **one `cef_host` subprocess per profile** (`--ipc --cdp-port
--profile-dir --allowed-schemes --ephemeral` as argv), then creates **N
browsers in that process** via `opCreateBrowser` frames carrying `url, width,
height, dpr, iosurface-id` + a `browserId`. `--profile-dir` is always passed; a
height, dpr` + a `browserId`. **The create carries no surface id** — the
surface model is *producer-allocates*: `cef_host` mints (and on resize
re-mints) the shared surface itself and announces it to the host via
`opPresent` frames carrying `{surface-id, srcW, srcH}` (the host promotes a
frame only when its dims match the expected size — see
`docs/OSR_SCALE_MISMATCH.md`). `--profile-dir` is always passed; a
persistent (named) profile points it at a stable cache dir, while an ephemeral
profile points it at a unique throwaway temp dir and adds `--ephemeral` (so the
host's CDP / mock-keychain guards fire only for a real persistent profile —
Expand Down Expand Up @@ -85,11 +90,11 @@ reused verbatim. Only these seams are macOS-specific (file:line are into

| Seam | macOS reference | What your platform provides |
| --- | --- | --- |
| **Shared surface** — receive painted frames and present them to the host texture. CEF delivers either software `OnPaint` (CPU buffer) or `OnAcceleratedPaint` (a shared GPU texture handle). | `OnPaint` / `OnAcceleratedPaint` + the `IOSurface*` ops (~lines 346–450); `g_surface` (~133). macOS uses an IOSurface-backed `CVPixelBuffer`. | **Windows**: a shared D3D11 texture / DXGI keyed-mutex handle. **Linux**: shared memory or a DMA-buf, presented via the platform texture. Look the surface up by the `--iosurface-id`-equivalent handle the host passes. |
| **Shared surface** — receive painted frames and present them to the host texture. CEF delivers either software `OnPaint` (CPU buffer) or `OnAcceleratedPaint` (a shared GPU texture handle). | `OnPaint` / `OnAcceleratedPaint` + the `IOSurface*` ops (~lines 346–450); `g_surface` (~133). macOS uses an IOSurface-backed `CVPixelBuffer`. | **Windows**: a shared D3D11 texture. Note (verified against CEF 144 `cef_types_win.h` + Flutter engine `external_texture_d3d.cc`): CEF's `OnAcceleratedPaint` delivers an **NT** handle (no keyed mutex, non-owning, pool-released when the callback returns — open it synchronously via `ID3D11Device1::OpenSharedResource1`), while Flutter's ANGLE compositor requires a **legacy** `D3D11_RESOURCE_MISC_SHARED` handle (`EGL_D3D_TEXTURE_2D_SHARE_HANDLE_ANGLE`); the two are mutually exclusive on one texture, so exactly one `CopyResource` NT→legacy is structurally mandatory (macOS also blits — `CompositeMetalLocked`). **Linux**: shared memory or a DMA-buf, presented via the platform texture. Either way the model is **producer-allocates**: `cef_host` mints the shared surface and announces it via `opPresent` — the host does NOT pass a surface id in the create (the macOS field name differs too: `shared_texture_io_surface` vs Windows `shared_texture_handle`, so the accelerated-paint glue takes a per-OS `#if`, not a typedef). |
| **IPC transport** — a framed bidirectional byte stream to the host. Wire format: 4-byte big-endian length prefix, then `[u32 browserId][opcode][payload]` (`browserId 0` = process-level: `opReady`, process logs, `opShutdown`). | `WriteAll`/`ReadAll` (207–235), `SendFrame` (~237–249), `ConnectUnixSocket` (1341+), the read loop (~1140). Unix domain socket. | **Windows**: a named pipe. **Linux**: a Unix domain socket (reuse as-is). Keep the same length-prefixed framing and the `browserId` dimension. |
| **App / run loop** — give CEF a host application + message loop, and a per-profile cache dir. | `CefHostApplication : NSApplication<CefAppProtocol>` (304–330); `@autoreleasepool` + `sharedApplication` (1420+); `--profile-dir` → `settings.root_cache_path` cache; `_NSGetExecutablePath` (1335). | **Windows/Linux**: the platform's CEF message-loop integration (`CefRunMessageLoop` or OS loop) and the caller-supplied `--profile-dir` (per-profile, persistent) as the cache path. |
| **Agent-control CDP pipe (optional)** — for `agentControl`, the host launches `cef_host` so Chromium's `--remote-debugging-pipe` (NUL-framed CDP JSON) rides **inherited file descriptors 3=read / 4=write** instead of a TCP port. | `CefProfileHost.launchViaPosixSpawn` (Swift): two `pipe()` pairs `dup2`'d onto fds 3/4 via `posix_spawn_file_actions`, parent ends marked `FD_CLOEXEC`; `cef_host` appends `remote-debugging-pipe` in `OnBeforeCommandLineProcessing`. The relay (`CdpRelay.swift`) + per-tile Target filter are platform-agnostic; only the fd placement is OS-specific. | **Linux**: reuse the `posix_spawn_file_actions` fd-3/4 recipe verbatim. **Windows**: a different model — `CreatePipe` + `STARTUPINFOEX` `PROC_THREAD_ATTRIBUTE_HANDLE_LIST` with `bInheritHandles`, and Chromium takes the two pipe **HANDLEs** as `--remote-debugging-pipe` args (not fds 3/4). The default (non-agent) launch also needs a native `exec`/`CreateProcess` (Foundation.Process is macOS-only). |
| **Sandbox** — bring the child processes into the Chromium sandbox in a signed/release build. | `process_helper.mm`: `CefScopedSandboxContext` (release only); `settings.no_sandbox` toggled by `CEF_HOST_ADHOC` (1426/1433). | **Windows**: link the `cef_sandbox` static lib + `CefScopedSandboxContext`. **Linux**: the SUID / user-namespace sandbox helper. |
| **Agent-control CDP pipe (optional)** — for `agentControl`, the host launches `cef_host` so Chromium's `--remote-debugging-pipe` (NUL-framed CDP JSON) rides **inherited file descriptors 3=read / 4=write** instead of a TCP port. | `CefProfileHost.launchViaPosixSpawn` (Swift): two `pipe()` pairs `dup2`'d onto fds 3/4 via `posix_spawn_file_actions`, parent ends marked `FD_CLOEXEC`; `cef_host` appends `remote-debugging-pipe` in `OnBeforeCommandLineProcessing`. The relay (`CdpRelay.swift`) + per-tile Target filter are platform-agnostic; only the fd placement is OS-specific. | **Linux**: reuse the `posix_spawn_file_actions` fd-3/4 recipe verbatim. **Windows**: a different model — `CreatePipe` + `SetHandleInformation(HANDLE_FLAG_INHERIT)` + `STARTUPINFOEX` `PROC_THREAD_ATTRIBUTE_HANDLE_LIST`, and Chromium takes the two inherited pipe **HANDLE values** via the separate switch `--remote-debugging-io-pipes=<read>,<write>` (uint32-serialized HANDLEs, passed **in addition to** the bare `--remote-debugging-pipe` — verified against `content_switches.cc` at chromium 144.0.7559.254; `--remote-debugging-pipe` itself takes no arguments on any platform). The default (non-agent) launch also needs a native `exec`/`CreateProcess` (Foundation.Process is macOS-only). |
| **Sandbox** — bring the child processes into the Chromium sandbox in a signed/release build. | `process_helper.mm`: `CefScopedSandboxContext` (release only); `settings.no_sandbox` toggled by `CEF_HOST_ADHOC` (1426/1433). | **Windows**: CEF 144 ships **no `cef_sandbox.lib`** — the current model (`cef_sandbox_win.h`, CEF issue #3824) is: build your app as a **DLL** exporting `RunWinMain`/`RunConsoleMain` and launch it through the prebuilt `bootstrap.exe`/`bootstrapc.exe` from the CEF distribution, which establishes the sandbox before entering your code. **Linux**: the SUID / user-namespace sandbox helper. |
| **Framework / resource path** — point CEF at the CEF binary distribution. | `CefScopedLibraryLoader::LoadInMain`/`LoadInHelper`; `framework_dir_path` / `main_bundle_path` (1453/1466). | The equivalent paths for your bundle layout. |
| **Build + bundle + sign** | `native/build_cef_host.sh` (CMake, `CEF_MULTI_PROCESS` / `CEF_HOST_ADHOC` flags), `tool/bundle_cef_host.sh`. | A platform build that produces `cef_host` + the CEF runtime, and a bundling step into the host app. |

Expand Down
50 changes: 37 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ final cookies = await c.getCookies(); // read/enumerate; pass url: to scope
c.deleteCookie(url: 'https://example.com/', name: 'sid'); c.clearCookies();
c.scrollTo(0, 200); c.scrollBy(0, -50); await c.getScrollPosition();
c.clearLocalStorage(); await c.getTitle(); await c.getUserAgent();
c.onDownload = (suggestedName) {}; // downloads land in ~/Downloads
c.onDownload = (suggestedName) {}; // a download started (a native Save panel is shown)

// open the Chrome DevTools inspector for this view in its own window
c.openDevTools();
Expand Down Expand Up @@ -223,11 +223,15 @@ final c = CefWebController(profile: 'work');
CefWebView(url: startUrl, controller: c);
```

- **Persistent.** A named profile is stored on disk at
`<Application Support>/<bundleId>/flutter_cef/profiles/<name>` (the directory
is created `0700`, owner-only, and the profile name is sanitized to
`[A-Za-z0-9._-]`). CEF is started with `persist_session_cookies` on, so a
login survives `cef_host` and host-app relaunch.
- **Persistent.** A named profile is stored on disk — on macOS at
`<Application Support>/<bundleId>/flutter_cef/profiles/<name>`, on Windows at
`%LOCALAPPDATA%\flutter_cef\profiles\<name>`. The directory is created
owner-only (`0700` on macOS; a current-user-SID-protected DACL on Windows) and
the profile name is sanitized to `[A-Za-z0-9._-]`. CEF is started with
`persist_session_cookies` on, so a login survives `cef_host` and host-app
relaunch. On Windows the `profiles\` root is **not** namespaced by app, so every
flutter_cef app for a user shares it — a `profile: 'work'` in two apps resolves
to the same directory (co-locate only mutually-trusting apps on a shared name).
- **Shared.** Every view constructed with the same non-null `profile` is served
by **one `cef_host` process with one cookie jar** — they share one login.
Cookie writes are therefore process-wide: `clearCookies()` /
Expand Down Expand Up @@ -273,6 +277,17 @@ persist under the mock keychain anyway (dev convenience only — do not ship it)
The downgrade leaks nothing: the refusal happens before any browser is created,
so nothing is ever written to the persistent directory.

**Windows is different — no downgrade, and no signing requirement.** On Windows,
OSCrypt encrypts the cookie store with **DPAPI**, which is **always available and
independent of code signing**. There is no mock-keychain state, so the
ad-hoc-downgrade rule above has no Windows analogue: a named `profile:` on Windows
simply persists, encrypted at rest, in every build. The trade-off is that DPAPI's
user-tier protection is **same-user-readable** — any process running as the same
Windows user can decrypt the store (`CryptUnprotectData`). This is **weaker than
the macOS Keychain**: at-rest protection covers other users and offline disk theft
(with BitLocker as the full-disk backstop), but **not** other same-user processes.
As on macOS, `localStorage` / IndexedDB are stored unencrypted regardless.

Two further caveats even under a signed build: `localStorage` and IndexedDB are
**not** encrypted by OSCrypt (they sit in the profile directory as plaintext —
FileVault is again the backstop), and **CDP (`enableCdp`) is incompatible with a
Expand Down Expand Up @@ -366,12 +381,15 @@ SIGABRT'd the instant the hardware is touched. Declare

### A named `profile:` silently behaves as ephemeral (login doesn't persist)

The default ad-hoc dev build (`CEF_HOST_ADHOC=ON`) has only a mock keychain and
can't encrypt cookies at rest, so it **downgrades a named profile to ephemeral**
rather than persisting a login to a plaintext store (it logs a warning). For real
persistence ship a signed release build (`CEF_HOST_ADHOC=OFF`, real
Keychain/OSCrypt). For dev only, set `FLUTTER_CEF_ALLOW_INSECURE_PROFILE=1` —
**never ship that override.**
**macOS only.** The default ad-hoc dev build (`CEF_HOST_ADHOC=ON`) has only a mock
keychain and can't encrypt cookies at rest, so it **downgrades a named profile to
ephemeral** rather than persisting a login to a plaintext store (it logs a
warning). For real persistence ship a signed release build (`CEF_HOST_ADHOC=OFF`,
real Keychain/OSCrypt). For dev only, set `FLUTTER_CEF_ALLOW_INSECURE_PROFILE=1` —
**never ship that override.** On **Windows** this never happens: OSCrypt uses
DPAPI, which is always available and signing-independent, so a named profile
persists in every build (see [Secrets at rest](#secrets-at-rest) for the
same-user-readable caveat).

### `onProcessGone` fires with reason `'locked'`

Expand Down Expand Up @@ -438,7 +456,13 @@ Next:
`flutter_cef_platform_interface` + `flutter_cef_macos`); a new platform is a
sibling `flutter_cef_<os>` package. The CEF logic + IPC protocol are portable;
each OS supplies its own host plugin + shared-texture / transport / sandbox
glue. See [`PORTING.md`](PORTING.md) for the full contract and seam map.
glue. See [`PORTING.md`](PORTING.md) for the full contract and seam map. A
Windows port (`flutter_cef_windows`) is in progress: the Dart controller /
widget surface is already cross-platform (the JS bridge, JS dialogs,
find-in-page, content zoom, and downloads all speak the same
method-channel/wire protocol on both OSes — see
`packages/flutter_cef_windows/native/cef_host/PROTOCOL.md`), with the Windows
host's sandbox + code-signing story still pending.

## Credits

Expand Down
30 changes: 30 additions & 0 deletions example/.metadata
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.

version:
revision: "bd7a4a6b5576630823ca344e3e684c53aa1a0f46"
channel: "stable"

project_type: app

# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46
base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46
- platform: windows
create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46
base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46

# User provided section

# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
Loading
Loading