mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-06-08 07:33:52 +02:00
23539c10c9
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
121 lines
3.4 KiB
Go
121 lines
3.4 KiB
Go
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 auto-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)
|
||
}
|
||
|
||
// RenderHTMLToPDF renders an HTML string to PDF bytes using a headless Chromium instance.
|
||
// If execPath is empty the browser binary is auto-resolved 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)
|
||
|
||
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"),
|
||
)...)
|
||
|
||
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 {
|
||
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)
|
||
}
|
||
|
||
// non-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
|
||
}
|