mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-16 20:32:13 +01:00
feat(chromium): add --chromium-clear-storage to clear local storage between conversions (#919)
This commit is contained in:
@@ -49,6 +49,7 @@ type browserArguments struct {
|
||||
denyPublicIPs bool
|
||||
clearCache bool
|
||||
clearCookies bool
|
||||
clearStorage bool
|
||||
disableJavaScript bool
|
||||
}
|
||||
|
||||
@@ -367,6 +368,7 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *slog.Logger, url, out
|
||||
runtime.Enable(),
|
||||
clearCacheActionFunc(logger, b.arguments.clearCache),
|
||||
clearCookiesActionFunc(logger, b.arguments.clearCookies),
|
||||
clearStorageActionFunc(logger, b.arguments.clearStorage, url),
|
||||
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
|
||||
setCookiesActionFunc(logger, options.Cookies),
|
||||
userAgentOverride(logger, options.UserAgent),
|
||||
@@ -393,6 +395,7 @@ func (b *chromiumBrowser) screenshot(ctx context.Context, logger *slog.Logger, u
|
||||
runtime.Enable(),
|
||||
clearCacheActionFunc(logger, b.arguments.clearCache),
|
||||
clearCookiesActionFunc(logger, b.arguments.clearCookies),
|
||||
clearStorageActionFunc(logger, b.arguments.clearStorage, url),
|
||||
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
|
||||
setCookiesActionFunc(logger, options.Cookies),
|
||||
userAgentOverride(logger, options.UserAgent),
|
||||
|
||||
@@ -477,6 +477,7 @@ func (mod *Chromium) Descriptor() gotenberg.ModuleDescriptor {
|
||||
fs.Bool("chromium-deny-public-ips", false, "Reject URLs whose host resolves to a public IP address. Enable on air-gapped or data-governed deployments to prevent outbound traffic from leaving a private network")
|
||||
fs.Bool("chromium-clear-cache", false, "Clear Chromium cache between each conversion")
|
||||
fs.Bool("chromium-clear-cookies", false, "Clear Chromium cookies between each conversion")
|
||||
fs.Bool("chromium-clear-storage", false, "Clear Chromium local storage between each conversion (session storage is already isolated per conversion)")
|
||||
fs.Bool("chromium-disable-javascript", false, "Disable JavaScript")
|
||||
fs.Bool("chromium-disable-routes", false, "Disable the routes")
|
||||
|
||||
@@ -528,6 +529,7 @@ func (mod *Chromium) Provision(ctx *gotenberg.Context) error {
|
||||
denyPublicIPs: flags.MustBool("chromium-deny-public-ips"),
|
||||
clearCache: flags.MustBool("chromium-clear-cache"),
|
||||
clearCookies: flags.MustBool("chromium-clear-cookies"),
|
||||
clearStorage: flags.MustBool("chromium-clear-storage"),
|
||||
disableJavaScript: flags.MustBool("chromium-disable-javascript"),
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"github.com/chromedp/cdproto/emulation"
|
||||
"github.com/chromedp/cdproto/network"
|
||||
"github.com/chromedp/cdproto/page"
|
||||
"github.com/chromedp/cdproto/storage"
|
||||
"github.com/chromedp/chromedp"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
@@ -345,6 +347,54 @@ func clearCookiesActionFunc(logger *slog.Logger, clear bool) chromedp.ActionFunc
|
||||
}
|
||||
}
|
||||
|
||||
// clearStorageActionFunc clears the converted origin's local storage before the
|
||||
// page loads, so state written by a previous conversion of the same origin does
|
||||
// not leak into this one. See https://github.com/gotenberg/gotenberg/issues/919.
|
||||
//
|
||||
// Session storage is not touched: each conversion runs in its own browsing
|
||||
// context (a fresh tab), so it is already isolated and cannot leak. Local
|
||||
// storage is per-origin and shared across tabs of the long-lived browser, so it
|
||||
// is the only web storage that carries over.
|
||||
func clearStorageActionFunc(logger *slog.Logger, clear bool, rawURL string) chromedp.ActionFunc {
|
||||
return func(ctx context.Context) error {
|
||||
if !clear {
|
||||
logger.DebugContext(ctx, "local storage not cleared")
|
||||
return nil
|
||||
}
|
||||
|
||||
origin, ok := httpOrigin(rawURL)
|
||||
if !ok {
|
||||
// A file:// upload gets an opaque, per-request origin that is not
|
||||
// shared between conversions, so there is nothing to clear.
|
||||
logger.DebugContext(ctx, "local storage not cleared: non-http(s) origin is already isolated")
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.DebugContext(ctx, fmt.Sprintf("clear local storage for %s", origin))
|
||||
|
||||
err := storage.ClearDataForOrigin(origin, string(storage.TypeLocalStorage)).Do(ctx)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("clear local storage: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// httpOrigin returns the http(s) security origin (scheme://host[:port]) of
|
||||
// rawURL, and false when rawURL is not http(s). A non-http(s) URL such as a
|
||||
// file:// upload has an opaque origin that no other conversion shares.
|
||||
func httpOrigin(rawURL string) (string, bool) {
|
||||
parsed, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
||||
return "", false
|
||||
}
|
||||
return fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host), true
|
||||
}
|
||||
|
||||
func disableJavaScriptActionFunc(logger *slog.Logger, disable bool) chromedp.ActionFunc {
|
||||
return func(ctx context.Context) error {
|
||||
// See https://github.com/gotenberg/gotenberg/issues/175.
|
||||
|
||||
@@ -22,6 +22,41 @@ Feature: /forms/chromium/convert/url
|
||||
Page 1
|
||||
"""
|
||||
|
||||
# localStorage is per-origin and shared by the long-lived browser, so without
|
||||
# clearing it accumulates across same-origin conversions.
|
||||
# See https://github.com/gotenberg/gotenberg/issues/919.
|
||||
Scenario: POST /forms/chromium/convert/url (localStorage leaks without clearing)
|
||||
Given I have a default Gotenberg container
|
||||
Given I have a static server
|
||||
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s):
|
||||
| url | http://host.docker.internal:%d/html/testdata/local-storage-html/index.html | field |
|
||||
| Gotenberg-Output-Filename | foo | header |
|
||||
Then the response status code should be 200
|
||||
Then the "foo.pdf" PDF should have content matching "localStorageCount=1" at page 1
|
||||
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s):
|
||||
| url | http://host.docker.internal:%d/html/testdata/local-storage-html/index.html | field |
|
||||
| Gotenberg-Output-Filename | foo | header |
|
||||
Then the response status code should be 200
|
||||
Then the "foo.pdf" PDF should have content matching "localStorageCount=2" at page 1
|
||||
|
||||
# With --chromium-clear-storage every conversion clears the origin's
|
||||
# localStorage first, so the counter never carries over.
|
||||
# See https://github.com/gotenberg/gotenberg/issues/919.
|
||||
Scenario: POST /forms/chromium/convert/url (Clear Storage)
|
||||
Given I have a Gotenberg container with the following environment variable(s):
|
||||
| CHROMIUM_CLEAR_STORAGE | true |
|
||||
Given I have a static server
|
||||
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s):
|
||||
| url | http://host.docker.internal:%d/html/testdata/local-storage-html/index.html | field |
|
||||
| Gotenberg-Output-Filename | foo | header |
|
||||
Then the response status code should be 200
|
||||
Then the "foo.pdf" PDF should have content matching "localStorageCount=1" at page 1
|
||||
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s):
|
||||
| url | http://host.docker.internal:%d/html/testdata/local-storage-html/index.html | field |
|
||||
| Gotenberg-Output-Filename | foo | header |
|
||||
Then the response status code should be 200
|
||||
Then the "foo.pdf" PDF should have content matching "localStorageCount=1" at page 1
|
||||
|
||||
Scenario: POST /forms/chromium/convert/url (Single Page)
|
||||
Given I have a default Gotenberg container
|
||||
Given I have a static server
|
||||
|
||||
@@ -83,6 +83,7 @@ Feature: /debug
|
||||
"chromium-auto-start": "false",
|
||||
"chromium-clear-cache": "false",
|
||||
"chromium-clear-cookies": "false",
|
||||
"chromium-clear-storage": "false",
|
||||
"chromium-deny-list": "[^file:(?!//\\/tmp/).*]",
|
||||
"chromium-deny-private-ips": "false",
|
||||
"chromium-deny-public-ips": "false",
|
||||
@@ -221,6 +222,7 @@ Feature: /debug
|
||||
"chromium-auto-start": "false",
|
||||
"chromium-clear-cache": "false",
|
||||
"chromium-clear-cookies": "false",
|
||||
"chromium-clear-storage": "false",
|
||||
"chromium-deny-list": "[^file:(?!//\\/tmp/).*]",
|
||||
"chromium-deny-private-ips": "false",
|
||||
"chromium-deny-public-ips": "false",
|
||||
|
||||
18
test/integration/testdata/local-storage-html/index.html
vendored
Normal file
18
test/integration/testdata/local-storage-html/index.html
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Local storage counter</title>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Increments a per-origin localStorage counter and renders it, so a value
|
||||
above 1 means a previous conversion of the same origin leaked into this
|
||||
one. See https://github.com/gotenberg/gotenberg/issues/919. -->
|
||||
<pre id="out"></pre>
|
||||
<script>
|
||||
var n = (parseInt(localStorage.getItem("n"), 10) || 0) + 1;
|
||||
localStorage.setItem("n", n);
|
||||
document.getElementById("out").textContent = "localStorageCount=" + n;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user