1package cef
2
3import (
4 "os"
5 "strings"
6
7 purecef "github.com/bnema/purego-cef/cef"
8 cef2gtk "github.com/bnema/purego-cef2gtk"
9
10 "github.com/bnema/dumber/internal/logging"
11)
12
13const (
14 dumbSchemeName = "dumb"
15 chromiumEnableFeaturesSwitch = "enable-features"
16 chromiumDisableFeaturesSwitch = "disable-features"
17 chromiumDisableBlinkFeaturesSwitch = "disable-blink-features"
18 chromiumRenderNodeOverrideSwitch = "render-node-override"
19)
20
21// Chromium's core Blink runtime flag for the Web Authentication API is
22// "WebAuth" (PublicKeyCredential IDL uses RuntimeEnabled=WebAuth). The
23// longer "WebAuthentication*" names are separate subfeatures/metrics.
24var cefWebAuthnFeaturesDisabledByPolicy = []string{
25 "WebAuth",
26}
27
28func dumbSchemeOptions() int32 {
29 return purecef.SchemeOptionsSchemeOptionStandard |
30 purecef.SchemeOptionsSchemeOptionSecure |
31 purecef.SchemeOptionsSchemeOptionCorsEnabled |
32 purecef.SchemeOptionsSchemeOptionCspBypassing |
33 purecef.SchemeOptionsSchemeOptionFetchEnabled
34}
35
36func registerDumbScheme(registrar purecef.SchemeRegistrar) {
37 if registrar == nil {
38 return
39 }
40 registrar.AddCustomScheme(dumbSchemeName, dumbSchemeOptions())
41}
42
43func configureCommandLine(commandLine purecef.CommandLine) {
44 configureCommandLineWithRenderStack(commandLine, cef2gtk.RenderStackPlan{})
45}
46
47func configureCommandLineWithRenderStack(commandLine purecef.CommandLine, renderStackPlan cef2gtk.RenderStackPlan) {
48 if commandLine == nil {
49 return
50 }
51
52 // Delegate Wayland accelerated rendering setup to the GTK bridge.
53 // Preserves any pre-existing ozone-platform value (e.g. set via
54 // CEF command-line args) — the bridge is a no-op when already present.
55 cef2gtk.ConfigureCommandLine(commandLine, cef2gtk.CommandLineOptions{RenderStackPlan: renderStackPlan})
56
57 // Enable Chromium's built-in smooth scrolling animation — without this,
58 // mouse wheel scroll jumps in discrete steps with no momentum/easing.
59 commandLine.AppendSwitch("enable-smooth-scrolling")
60
61 // Allow video autoplay without requiring user gesture — sites like
62 // Reddit autoplay muted videos in the feed; without this policy
63 // Chromium blocks them, showing an infinite spinner.
64 commandLine.AppendSwitchWithValue("autoplay-policy", "no-user-gesture-required")
65
66 configureHardwareVideoDecode(commandLine)
67 configureEnvChromiumFlags(commandLine)
68 configureRenderNodeOverride(commandLine)
69 configureWebAuthnFeaturePolicy(commandLine)
70}
71
72func configureHardwareVideoDecode(commandLine purecef.CommandLine) {
73 if commandLine == nil || !envBoolEnabled(cefEnableVAAPIEnvVar) {
74 return
75 }
76
77 appendUniqueCommaSeparatedSwitchValues(commandLine, chromiumEnableFeaturesSwitch,
78 "AcceleratedVideoDecoder",
79 "AcceleratedVideoEncoder",
80 "AcceleratedVideoDecodeLinuxGL",
81 "AcceleratedVideoDecodeLinuxZeroCopyGL",
82 "VaapiIgnoreDriverChecks",
83 )
84 appendSwitchIfMissing(commandLine, "ignore-gpu-blocklist")
85 appendSwitchIfMissing(commandLine, "enable-zero-copy")
86 appendSwitchIfMissing(commandLine, "disable-gpu-driver-bug-workaround")
87}
88
89func configureEnvChromiumFlags(commandLine purecef.CommandLine) {
90 if commandLine == nil {
91 return
92 }
93
94 for _, token := range parseChromiumFlagsEnv(os.Getenv(cefChromiumFlagsEnvVar)) {
95 applyChromiumFlagToken(commandLine, token)
96 }
97}
98
99func configureRenderNodeOverride(commandLine purecef.CommandLine) {
100 if commandLine == nil {
101 return
102 }
103 value := strings.TrimSpace(os.Getenv(cefRenderNodeEnvVar))
104 if value == "" {
105 return
106 }
107 switch strings.ToLower(value) {
108 case "auto", "default", "none", "off", "disable", "disabled":
109 commandLine.RemoveSwitch(chromiumRenderNodeOverrideSwitch)
110 return
111 }
112 commandLine.RemoveSwitch(chromiumRenderNodeOverrideSwitch)
113 commandLine.AppendSwitchWithValue(chromiumRenderNodeOverrideSwitch, value)
114}
115
116func parseChromiumFlagsEnv(raw string) []string {
117 // Reuse Dumber's command-line tokenizer so developer env flags can contain
118 // quoted values without adding a second shell-like parser in the CEF adapter.
119 return parseRelaunchCommandLineArgs(raw)
120}
121
122func applyChromiumFlagToken(commandLine purecef.CommandLine, token string) {
123 if commandLine == nil {
124 return
125 }
126 token = strings.TrimSpace(token)
127 if token == "" || token == "--" {
128 return
129 }
130 name, value, ok := parseChromiumSwitchToken(token)
131 if !ok {
132 return
133 }
134
135 switch name {
136 case chromiumEnableFeaturesSwitch, chromiumDisableFeaturesSwitch, chromiumDisableBlinkFeaturesSwitch:
137 appendUniqueCommaSeparatedSwitchValues(commandLine, name, strings.Split(value, ",")...)
138 default:
139 if value == "" {
140 appendSwitchIfMissing(commandLine, name)
141 return
142 }
143 commandLine.AppendSwitchWithValue(name, value)
144 }
145}
146
147func parseChromiumSwitchToken(token string) (name, value string, ok bool) {
148 token = strings.TrimSpace(token)
149 if token == "" {
150 return "", "", false
151 }
152
153 switch {
154 case strings.HasPrefix(token, "--"):
155 token = strings.TrimPrefix(token, "--")
156 case strings.HasPrefix(token, "-"):
157 token = strings.TrimPrefix(token, "-")
158 default:
159 return "", "", false
160 }
161
162 if token == "" || strings.HasPrefix(token, "-") {
163 return "", "", false
164 }
165 name, value, found := strings.Cut(token, "=")
166 name = strings.TrimSpace(name)
167 if name == "" {
168 return "", "", false
169 }
170 if !found {
171 return name, "", true
172 }
173 return name, value, true
174}
175
176func configureWebAuthnFeaturePolicy(commandLine purecef.CommandLine) {
177 if commandLine == nil {
178 return
179 }
180 if cefWebAuthnUnsafeEnabled() {
181 removeCommaSeparatedSwitchValues(commandLine, chromiumDisableFeaturesSwitch, cefWebAuthnFeaturesDisabledByPolicy...)
182 removeCommaSeparatedSwitchValues(commandLine, chromiumDisableBlinkFeaturesSwitch, cefWebAuthnFeaturesDisabledByPolicy...)
183 return
184 }
185 appendUniqueCommaSeparatedSwitchValues(commandLine, chromiumDisableFeaturesSwitch, cefWebAuthnFeaturesDisabledByPolicy...)
186 appendUniqueCommaSeparatedSwitchValues(commandLine, chromiumDisableBlinkFeaturesSwitch, cefWebAuthnFeaturesDisabledByPolicy...)
187}
188
189func appendUniqueCommaSeparatedSwitchValues(commandLine purecef.CommandLine, name string, values ...string) {
190 if commandLine == nil || len(values) == 0 {
191 return
192 }
193
194 seen := make(map[string]struct{}, len(values))
195 combined := make([]string, 0, len(values))
196 for value := range strings.SplitSeq(commandLine.GetSwitchValue(name), ",") {
197 value = strings.TrimSpace(value)
198 if value == "" {
199 continue
200 }
201 if _, ok := seen[value]; ok {
202 continue
203 }
204 seen[value] = struct{}{}
205 combined = append(combined, value)
206 }
207 for _, value := range values {
208 value = strings.TrimSpace(value)
209 if value == "" {
210 continue
211 }
212 if _, ok := seen[value]; ok {
213 continue
214 }
215 seen[value] = struct{}{}
216 combined = append(combined, value)
217 }
218
219 if len(combined) == 0 {
220 return
221 }
222 commandLine.RemoveSwitch(name)
223 commandLine.AppendSwitchWithValue(name, strings.Join(combined, ","))
224}
225
226func removeCommaSeparatedSwitchValues(commandLine purecef.CommandLine, name string, values ...string) {
227 if commandLine == nil || len(values) == 0 {
228 return
229 }
230
231 existing := commandLine.GetSwitchValue(name)
232 if existing == "" {
233 return
234 }
235
236 removeSet := make(map[string]struct{}, len(values))
237 for _, v := range values {
238 removeSet[strings.TrimSpace(v)] = struct{}{}
239 }
240
241 tokens := strings.Split(existing, ",")
242 cleaned := make([]string, 0, len(tokens))
243 removed := false
244 for _, token := range tokens {
245 token = strings.TrimSpace(token)
246 if token == "" {
247 continue
248 }
249 if _, ok := removeSet[token]; ok {
250 removed = true
251 continue
252 }
253 cleaned = append(cleaned, token)
254 }
255
256 if !removed {
257 return
258 }
259
260 if len(cleaned) == 0 {
261 commandLine.RemoveSwitch(name)
262 return
263 }
264
265 commandLine.RemoveSwitch(name)
266 commandLine.AppendSwitchWithValue(name, strings.Join(cleaned, ","))
267}
268
269// dumberApp implements purecef.App to provide custom scheme registration,
270// command-line configuration, and browser-process lifecycle callbacks.
271type dumberApp struct {
272 engine *Engine
273 bph purecef.BrowserProcessHandler
274 rph purecef.RenderProcessHandler
275}
276
277// newDumberApp creates an App whose BrowserProcessHandler handles context
278// initialization, child process launch tracking, and scheme registration.
279// Returns the raw Go implementation — InitWithApp will wrap it exactly once.
280func newDumberApp(engine *Engine) purecef.App {
281 app := &dumberApp{engine: engine}
282 app.bph = &dumberBPH{engine: engine}
283 app.rph = newComposedRenderProcessHandler()
284 return app
285}
286
287// NewSubprocessApp returns a lightweight raw App implementation for CEF
288// subprocesses re-executed from the main dumber binary. Keep the legacy V8
289// renderer bridge disabled while composing editable-focus reporting with the
290// popup-opener callbacks needed before page scripts run.
291func NewSubprocessApp() purecef.App {
292 return &subprocessApp{rph: newComposedRenderProcessHandler()}
293}
294
295// maxCmdLineLogLen limits logged command line length to avoid leaking sensitive paths.
296const maxCmdLineLogLen = 200
297
298func (a *dumberApp) OnBeforeCommandLineProcessing(processType string, commandLine purecef.CommandLine) {
299 log := logging.FromContext(a.engine.ctx)
300 if commandLine != nil {
301 configureCommandLineWithRenderStack(commandLine, a.engine.renderStackPlan)
302
303 if processType == "" {
304 if cefWebAuthnUnsafeEnabled() {
305 log.Warn().
306 Str("env_var", cefEnableWebAuthnUnsafeEnvVar).
307 Msg("cef: WebAuthn enabled via unsupported developer override")
308 } else {
309 log.Info().
310 Strs("disabled_features", cefWebAuthnFeaturesDisabledByPolicy).
311 Str("override_env_var", cefEnableWebAuthnUnsafeEnvVar).
312 Msg("cef: WebAuthn disabled for CEF windowless/Alloy runtime")
313 }
314 }
315
316 renderNodeOverride := commandLine.GetSwitchValue(chromiumRenderNodeOverrideSwitch)
317 cmdline := commandLine.GetCommandLineString()
318 if len(cmdline) > maxCmdLineLogLen {
319 runes := []rune(cmdline)
320 if len(runes) > maxCmdLineLogLen {
321 cmdline = string(runes[:maxCmdLineLogLen]) + "…"
322 }
323 }
324 log.Debug().
325 Str("process_type", processType).
326 Str("render_node_override", renderNodeOverride).
327 Str("command_line", cmdline).
328 Msg("cef: OnBeforeCommandLineProcessing")
329 }
330}
331func (a *dumberApp) OnRegisterCustomSchemes(registrar purecef.SchemeRegistrar) {
332 registerDumbScheme(registrar)
333 logging.FromContext(a.engine.ctx).Debug().Msg("cef: registered dumb:// custom scheme")
334}
335func (a *dumberApp) GetResourceBundleHandler() purecef.ResourceBundleHandler { return nil }
336func (a *dumberApp) GetBrowserProcessHandler() purecef.BrowserProcessHandler { return a.bph }
337func (a *dumberApp) GetRenderProcessHandler() purecef.RenderProcessHandler { return a.rph }
338
339type subprocessApp struct{ rph purecef.RenderProcessHandler }
340
341func (a *subprocessApp) OnBeforeCommandLineProcessing(_ string, commandLine purecef.CommandLine) {
342 configureCommandLine(commandLine)
343}
344func (a *subprocessApp) OnRegisterCustomSchemes(registrar purecef.SchemeRegistrar) {
345 registerDumbScheme(registrar)
346}
347func (a *subprocessApp) GetResourceBundleHandler() purecef.ResourceBundleHandler { return nil }
348func (a *subprocessApp) GetBrowserProcessHandler() purecef.BrowserProcessHandler { return nil }
349func (a *subprocessApp) GetRenderProcessHandler() purecef.RenderProcessHandler { return a.rph }
350
351// dumberBPH implements purecef.BrowserProcessHandler for context initialization,
352// child process launch tracking, and diagnostic logging.
353type dumberBPH struct {
354 engine *Engine
355}
356
357func (h *dumberBPH) OnRegisterCustomPreferences(_ purecef.PreferencesType, _ purecef.PreferenceRegistrar) {
358}
359func (h *dumberBPH) OnContextInitialized() {
360 h.engine.recordContextInitialized()
361}
362func (h *dumberBPH) OnBeforeChildProcessLaunch(commandLine purecef.CommandLine) {
363 processType := ""
364 commandLineString := ""
365 useAngle := ""
366 ozonePlatform := ""
367 renderNodeOverride := ""
368 if commandLine != nil {
369 configureCommandLineWithRenderStack(commandLine, h.engine.renderStackPlan)
370 appendSwitchIfMissing(commandLine, "no-zygote")
371 processType = commandLine.GetSwitchValue("type")
372 commandLineString = commandLine.GetCommandLineString()
373 useAngle = commandLine.GetSwitchValue("use-angle")
374 ozonePlatform = commandLine.GetSwitchValue("ozone-platform")
375 renderNodeOverride = commandLine.GetSwitchValue(chromiumRenderNodeOverrideSwitch)
376 }
377 h.engine.recordChildProcessLaunch(processType, useAngle, ozonePlatform, renderNodeOverride, commandLineString)
378}
379
380func appendSwitchIfMissing(commandLine purecef.CommandLine, name string) {
381 if commandLine == nil || commandLine.HasSwitch(name) {
382 return
383 }
384 commandLine.AppendSwitch(name)
385}
386
387func parseRelaunchCommandLineArgs(commandLine string) []string {
388 args := make([]string, 0, 4)
389 var current strings.Builder
390 var quote rune
391 escaped := false
392 flush := func() {
393 if current.Len() == 0 {
394 return
395 }
396 args = append(args, current.String())
397 current.Reset()
398 }
399
400 for _, r := range commandLine {
401 switch {
402 case escaped:
403 current.WriteRune(r)
404 escaped = false
405 case r == '\\':
406 escaped = true
407 case quote != 0:
408 if r == quote {
409 quote = 0
410 continue
411 }
412 current.WriteRune(r)
413 case r == '"' || r == '\'':
414 quote = r
415 case r == ' ' || r == '\t' || r == '\n' || r == '\r':
416 flush()
417 default:
418 current.WriteRune(r)
419 }
420 }
421
422 if escaped {
423 current.WriteRune('\\')
424 }
425 flush()
426
427 return args
428}
429
430func parseBrowseRelaunchCommandLine(commandLine purecef.CommandLine) (string, bool) {
431 if commandLine == nil {
432 return "", false
433 }
434
435 args := parseRelaunchCommandLineArgs(commandLine.GetCommandLineString())
436 if len(args) < 2 || args[1] != "browse" {
437 return "", false
438 }
439 var browseURL string
440 for index := 2; index < len(args); index++ {
441 arg := args[index]
442 switch {
443 case arg == "--instance":
444 index++
445 if index >= len(args) || args[index] == "" {
446 return "", false
447 }
448 case strings.HasPrefix(arg, "--instance="):
449 if strings.TrimPrefix(arg, "--instance=") == "" {
450 return "", false
451 }
452 case arg == "--profile" || strings.HasPrefix(arg, "--profile=") || arg == "--ephemeral":
453 // Profile-scoped launches must be handled by their own elected host;
454 // CEF's already-running callback must never route them into this one.
455 return "", false
456 case strings.HasPrefix(arg, "-"):
457 return "", false
458 case browseURL == "":
459 browseURL = arg
460 default:
461 return "", false
462 }
463 }
464 if browseURL == "" {
465 return "", false
466 }
467 return browseURL, true
468}
469
470func isBrowseRelaunchCommandLine(commandLine purecef.CommandLine) bool {
471 if commandLine == nil {
472 return false
473 }
474
475 args := parseRelaunchCommandLineArgs(commandLine.GetCommandLineString())
476 return len(args) >= 2 && args[1] == "browse"
477}
478
479func isGPUProcessRelaunchCommandLine(commandLine purecef.CommandLine) bool {
480 if commandLine == nil {
481 return false
482 }
483 for _, arg := range parseRelaunchCommandLineArgs(commandLine.GetCommandLineString()) {
484 if arg == "--type=gpu-process" {
485 return true
486 }
487 }
488 return false
489}
490
491func parseBrowseURLFromRelaunchCommandLine(commandLine purecef.CommandLine) string {
492 browseURL, _ := parseBrowseRelaunchCommandLine(commandLine)
493 return browseURL
494}
495
496func (h *dumberBPH) OnAlreadyRunningAppRelaunch(commandLine purecef.CommandLine, _ string) int32 {
497 if isGPUProcessRelaunchCommandLine(commandLine) {
498 if h != nil && h.engine != nil {
499 h.engine.recordGPURelaunch()
500 }
501 return 0
502 }
503 if browseURL, ok := parseBrowseRelaunchCommandLine(commandLine); ok {
504 if h != nil && h.engine != nil {
505 if handler := h.engine.alreadyRunningAppRelaunchCallback(); handler != nil {
506 handler(browseURL)
507 }
508 }
509 return 1
510 }
511 if isBrowseRelaunchCommandLine(commandLine) {
512 return 1
513 }
514
515 return 0
516}
517func (h *dumberBPH) GetDefaultClient() purecef.RawClient { return nil }
518func (h *dumberBPH) GetDefaultRequestContextHandler() purecef.RequestContextHandler { return nil }
519
520// OnScheduleMessagePumpWork is a no-op — multi-threaded message loop drives
521// its own pump. Required by the BrowserProcessHandler interface.
522func (h *dumberBPH) OnScheduleMessagePumpWork(_ int64) {}