Files
phishingclub/backend/remotebrowser/pdf.go
T

158 lines
5.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package remotebrowser
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"time"
"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/launcher"
"github.com/go-rod/rod/lib/proto"
)
// WipeBrowserCache removes the automatically downloaded Chromium directory.
// The next call to RenderHTMLToPDF will trigger a fresh download.
func WipeBrowserCache() error {
dir, err := resolveBrowserRootDir()
if err != nil {
return fmt.Errorf("reportpdf: %w", err)
}
return os.RemoveAll(dir)
}
// applyGuardFlags points the launcher at the egress guard and removes the
// browser's own ways to leave the box around it: it routes all requests through
// the proxy, removes the implicit loopback and link local proxy bypass (without
// which 127.0.0.1 and 169.254.169.254 are reached directly), blocks the WebRTC
// and QUIC UDP paths, and disables the browser's background network calls. It is
// the single definition used by both the renderer and its tests so the guard
// configuration cannot silently drift out of the real render path.
func applyGuardFlags(l *launcher.Launcher, proxyAddr string) *launcher.Launcher {
return l.
Set("proxy-server", proxyAddr).
Set("proxy-bypass-list", "<-loopback>").
Set("disable-background-networking").
Set("disable-component-update").
Set("disable-domain-reliability").
Set("disable-sync").
Set("disable-client-side-phishing-detection").
Set("safebrowsing-disable-auto-update").
Set("no-pings").
Set("no-first-run").
Set("no-default-browser-check").
Set("force-webrtc-ip-handling-policy", "disable_non_proxied_udp").
Set("disable-quic")
}
// RenderHTMLToPDF renders an HTML string to PDF bytes using a headless Chromium instance.
// If execPath is empty the browser binary is resolved automatically using the same path as the runner.
func RenderHTMLToPDF(ctx context.Context, htmlContent string, execPath string) ([]byte, error) {
rootDir, err := resolveBrowserRootDir()
if err != nil {
return nil, fmt.Errorf("reportpdf: %w", err)
}
crashDir := filepath.Join(rootDir, "crashes")
_ = os.MkdirAll(crashDir, 0755)
_ = os.MkdirAll(filepath.Join(rootDir, "config"), 0755)
_ = os.MkdirAll(filepath.Join(rootDir, "cache"), 0755)
// force browser egress through the loopback guard so an embedded report
// resource cannot be used for request forgery (see guardedProxy).
proxy, err := startGuardedProxy()
if err != nil {
return nil, fmt.Errorf("reportpdf: egress guard: %w", err)
}
defer proxy.close()
l := launcher.New().
Headless(true).
Set("disable-crash-reporter").
Set("crash-dumps-dir", crashDir).
Env(chromeEnv(
"XDG_CONFIG_HOME="+filepath.Join(rootDir, "config"),
"XDG_CACHE_HOME="+filepath.Join(rootDir, "cache"),
)...)
applyGuardFlags(l, proxy.addr())
if execPath != "" {
l = l.Bin(execPath)
} else {
b := launcher.NewBrowser()
b.RootDir = rootDir
binPath := b.BinPath()
if _, err := os.Stat(binPath); os.IsNotExist(err) {
if err := b.Download(); err != nil {
return nil, fmt.Errorf("reportpdf: browser download failed: %w", err)
}
}
l = l.Bin(binPath)
}
u, err := l.Launch()
if err != nil {
// Kill does nothing when no process started; Cleanup is skipped here because
// on an early launch failure it blocks forever waiting on the exit channel.
// Kill still removes a process that did start.
l.Kill()
return nil, fmt.Errorf("reportpdf: browser launch failed: %w", err)
}
defer func() { l.Kill(); l.Cleanup() }()
browser := rod.New().ControlURL(u).Context(ctx)
if err := browser.Connect(); err != nil {
return nil, fmt.Errorf("reportpdf: browser connect failed: %w", err)
}
defer browser.Close() //nolint:errcheck
page, err := browser.Page(proto.TargetCreateTarget{URL: "about:blank"})
if err != nil {
return nil, fmt.Errorf("reportpdf: page create failed: %w", err)
}
// Set viewport to A4 at 96 dpi (794×1123 px) so layout fills exactly the paper width.
// Without this Chrome defaults to a wider viewport and the content may not reach the
// right edge of the paper, producing a white strip at certain zoom levels.
if err := page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{
Width: 794,
Height: 1123,
DeviceScaleFactor: 1,
Mobile: false,
}); err != nil {
return nil, fmt.Errorf("reportpdf: set viewport failed: %w", err)
}
if err := page.SetDocumentContent(htmlContent); err != nil {
return nil, fmt.Errorf("reportpdf: set content failed: %w", err)
}
// not fatal: lets inline resources settle before printing
_ = page.WaitIdle(3 * time.Second)
a4Width := 8.27
a4Height := 11.69
zero := 0.0
pdfReader, err := page.PDF(&proto.PagePrintToPDF{
PrintBackground: true,
PaperWidth: &a4Width,
PaperHeight: &a4Height,
MarginTop: &zero,
MarginBottom: &zero,
MarginLeft: &zero,
MarginRight: &zero,
})
if err != nil {
return nil, fmt.Errorf("reportpdf: PDF render failed: %w", err)
}
defer pdfReader.Close() //nolint:errcheck
data, err := io.ReadAll(pdfReader)
if err != nil {
return nil, fmt.Errorf("reportpdf: read PDF failed: %w", err)
}
return data, nil
}