mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-14 11:22:15 +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
|
denyPublicIPs bool
|
||||||
clearCache bool
|
clearCache bool
|
||||||
clearCookies bool
|
clearCookies bool
|
||||||
|
clearStorage bool
|
||||||
disableJavaScript bool
|
disableJavaScript bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,6 +368,7 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *slog.Logger, url, out
|
|||||||
runtime.Enable(),
|
runtime.Enable(),
|
||||||
clearCacheActionFunc(logger, b.arguments.clearCache),
|
clearCacheActionFunc(logger, b.arguments.clearCache),
|
||||||
clearCookiesActionFunc(logger, b.arguments.clearCookies),
|
clearCookiesActionFunc(logger, b.arguments.clearCookies),
|
||||||
|
clearStorageActionFunc(logger, b.arguments.clearStorage, url),
|
||||||
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
|
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
|
||||||
setCookiesActionFunc(logger, options.Cookies),
|
setCookiesActionFunc(logger, options.Cookies),
|
||||||
userAgentOverride(logger, options.UserAgent),
|
userAgentOverride(logger, options.UserAgent),
|
||||||
@@ -393,6 +395,7 @@ func (b *chromiumBrowser) screenshot(ctx context.Context, logger *slog.Logger, u
|
|||||||
runtime.Enable(),
|
runtime.Enable(),
|
||||||
clearCacheActionFunc(logger, b.arguments.clearCache),
|
clearCacheActionFunc(logger, b.arguments.clearCache),
|
||||||
clearCookiesActionFunc(logger, b.arguments.clearCookies),
|
clearCookiesActionFunc(logger, b.arguments.clearCookies),
|
||||||
|
clearStorageActionFunc(logger, b.arguments.clearStorage, url),
|
||||||
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
|
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
|
||||||
setCookiesActionFunc(logger, options.Cookies),
|
setCookiesActionFunc(logger, options.Cookies),
|
||||||
userAgentOverride(logger, options.UserAgent),
|
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-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-cache", false, "Clear Chromium cache between each conversion")
|
||||||
fs.Bool("chromium-clear-cookies", false, "Clear Chromium cookies 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-javascript", false, "Disable JavaScript")
|
||||||
fs.Bool("chromium-disable-routes", false, "Disable the routes")
|
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"),
|
denyPublicIPs: flags.MustBool("chromium-deny-public-ips"),
|
||||||
clearCache: flags.MustBool("chromium-clear-cache"),
|
clearCache: flags.MustBool("chromium-clear-cache"),
|
||||||
clearCookies: flags.MustBool("chromium-clear-cookies"),
|
clearCookies: flags.MustBool("chromium-clear-cookies"),
|
||||||
|
clearStorage: flags.MustBool("chromium-clear-storage"),
|
||||||
disableJavaScript: flags.MustBool("chromium-disable-javascript"),
|
disableJavaScript: flags.MustBool("chromium-disable-javascript"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
@@ -14,6 +15,7 @@ import (
|
|||||||
"github.com/chromedp/cdproto/emulation"
|
"github.com/chromedp/cdproto/emulation"
|
||||||
"github.com/chromedp/cdproto/network"
|
"github.com/chromedp/cdproto/network"
|
||||||
"github.com/chromedp/cdproto/page"
|
"github.com/chromedp/cdproto/page"
|
||||||
|
"github.com/chromedp/cdproto/storage"
|
||||||
"github.com/chromedp/chromedp"
|
"github.com/chromedp/chromedp"
|
||||||
"go.opentelemetry.io/otel/attribute"
|
"go.opentelemetry.io/otel/attribute"
|
||||||
"go.opentelemetry.io/otel/codes"
|
"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 {
|
func disableJavaScriptActionFunc(logger *slog.Logger, disable bool) chromedp.ActionFunc {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
// See https://github.com/gotenberg/gotenberg/issues/175.
|
// See https://github.com/gotenberg/gotenberg/issues/175.
|
||||||
|
|||||||
@@ -22,6 +22,41 @@ Feature: /forms/chromium/convert/url
|
|||||||
Page 1
|
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)
|
Scenario: POST /forms/chromium/convert/url (Single Page)
|
||||||
Given I have a default Gotenberg container
|
Given I have a default Gotenberg container
|
||||||
Given I have a static server
|
Given I have a static server
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ Feature: /debug
|
|||||||
"chromium-auto-start": "false",
|
"chromium-auto-start": "false",
|
||||||
"chromium-clear-cache": "false",
|
"chromium-clear-cache": "false",
|
||||||
"chromium-clear-cookies": "false",
|
"chromium-clear-cookies": "false",
|
||||||
|
"chromium-clear-storage": "false",
|
||||||
"chromium-deny-list": "[^file:(?!//\\/tmp/).*]",
|
"chromium-deny-list": "[^file:(?!//\\/tmp/).*]",
|
||||||
"chromium-deny-private-ips": "false",
|
"chromium-deny-private-ips": "false",
|
||||||
"chromium-deny-public-ips": "false",
|
"chromium-deny-public-ips": "false",
|
||||||
@@ -221,6 +222,7 @@ Feature: /debug
|
|||||||
"chromium-auto-start": "false",
|
"chromium-auto-start": "false",
|
||||||
"chromium-clear-cache": "false",
|
"chromium-clear-cache": "false",
|
||||||
"chromium-clear-cookies": "false",
|
"chromium-clear-cookies": "false",
|
||||||
|
"chromium-clear-storage": "false",
|
||||||
"chromium-deny-list": "[^file:(?!//\\/tmp/).*]",
|
"chromium-deny-list": "[^file:(?!//\\/tmp/).*]",
|
||||||
"chromium-deny-private-ips": "false",
|
"chromium-deny-private-ips": "false",
|
||||||
"chromium-deny-public-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