diff --git a/pkg/modules/chromium/browser.go b/pkg/modules/chromium/browser.go
index df0516cf..060f2acb 100644
--- a/pkg/modules/chromium/browser.go
+++ b/pkg/modules/chromium/browser.go
@@ -226,6 +226,7 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outp
clearCacheActionFunc(logger, b.arguments.clearCache),
clearCookiesActionFunc(logger, b.arguments.clearCookies),
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
+ setCookiesActionFunc(logger, options.Cookies),
extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders),
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, options.PrintBackground),
@@ -248,6 +249,7 @@ func (b *chromiumBrowser) screenshot(ctx context.Context, logger *zap.Logger, ur
clearCacheActionFunc(logger, b.arguments.clearCache),
clearCookiesActionFunc(logger, b.arguments.clearCookies),
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
+ setCookiesActionFunc(logger, options.Cookies),
extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders),
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, true),
diff --git a/pkg/modules/chromium/browser_test.go b/pkg/modules/chromium/browser_test.go
index b33b5ae3..89d8a937 100644
--- a/pkg/modules/chromium/browser_test.go
+++ b/pkg/modules/chromium/browser_test.go
@@ -579,6 +579,41 @@ func TestChromiumBrowser_pdf(t *testing.T) {
"JavaScript disabled, skipping wait expression",
},
},
+ {
+ scenario: "set cookies",
+ browser: newChromiumBrowser(
+ browserArguments{
+ binPath: os.Getenv("CHROMIUM_BIN_PATH"),
+ wsUrlReadTimeout: 5 * time.Second,
+ allowList: regexp2.MustCompile("", 0),
+ denyList: regexp2.MustCompile("", 0),
+ },
+ ),
+ fs: func() *gotenberg.FileSystem {
+ fs := gotenberg.NewFileSystem()
+
+ err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
+ if err != nil {
+ t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
+ }
+
+ err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("
Set cookies
"), 0o755)
+ if err != nil {
+ t.Fatalf("expected no error but got: %v", err)
+ }
+
+ return fs
+ }(),
+ options: PdfOptions{
+ Options: Options{Cookies: []Cookie{{Name: "foo", Value: "bar", Domain: ".foo.bar"}}},
+ },
+ noDeadline: false,
+ start: true,
+ expectError: false,
+ expectedLogEntries: []string{
+ "set cookie",
+ },
+ },
{
scenario: "extra HTTP headers",
browser: newChromiumBrowser(
@@ -1125,6 +1160,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
"cache not cleared",
"cookies not cleared",
"JavaScript not disabled",
+ "no cookies to set",
"no extra HTTP headers",
"navigate to",
"default white background not hidden",
@@ -1549,6 +1585,41 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
"JavaScript disabled, skipping wait expression",
},
},
+ {
+ scenario: "set cookies",
+ browser: newChromiumBrowser(
+ browserArguments{
+ binPath: os.Getenv("CHROMIUM_BIN_PATH"),
+ wsUrlReadTimeout: 5 * time.Second,
+ allowList: regexp2.MustCompile("", 0),
+ denyList: regexp2.MustCompile("", 0),
+ },
+ ),
+ fs: func() *gotenberg.FileSystem {
+ fs := gotenberg.NewFileSystem()
+
+ err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
+ if err != nil {
+ t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
+ }
+
+ err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("Set cookies
"), 0o755)
+ if err != nil {
+ t.Fatalf("expected no error but got: %v", err)
+ }
+
+ return fs
+ }(),
+ options: ScreenshotOptions{
+ Options: Options{Cookies: []Cookie{{Name: "fpp", Value: "bar", Domain: ".foo.bar"}}},
+ },
+ noDeadline: false,
+ start: true,
+ expectError: false,
+ expectedLogEntries: []string{
+ "set cookie",
+ },
+ },
{
scenario: "extra HTTP headers",
browser: newChromiumBrowser(
@@ -1964,6 +2035,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
"cache not cleared",
"cookies not cleared",
"JavaScript not disabled",
+ "no cookies to set",
"no extra HTTP headers",
"navigate to",
"default white background not hidden",
diff --git a/pkg/modules/chromium/chromium.go b/pkg/modules/chromium/chromium.go
index 0ab4ad36..1b6fd79d 100644
--- a/pkg/modules/chromium/chromium.go
+++ b/pkg/modules/chromium/chromium.go
@@ -8,6 +8,7 @@ import (
"time"
"github.com/alexliesenfeld/health"
+ "github.com/chromedp/cdproto/network"
flag "github.com/spf13/pflag"
"go.uber.org/zap"
@@ -103,6 +104,10 @@ type Options struct {
// Optional.
WaitForExpression string
+ // Cookies are the cookies to put in the Chromium cookies' jar.
+ // Optional
+ Cookies []Cookie
+
// ExtraHttpHeaders are the HTTP headers to send by Chromium while loading
// the HTML document.
// Optional.
@@ -128,6 +133,7 @@ func DefaultOptions() Options {
WaitDelay: 0,
WaitWindowStatus: "",
WaitForExpression: "",
+ Cookies: nil,
ExtraHttpHeaders: nil,
EmulatedMediaType: "",
OmitBackground: false,
@@ -258,6 +264,38 @@ func DefaultScreenshotOptions() ScreenshotOptions {
}
}
+// Cookie gathers the available entries for setting a cookie in the Chromium
+// cookies' jar.
+type Cookie struct {
+ // Name is the cookie name.
+ // Required.
+ Name string `json:"name"`
+
+ // Value is the cookie value.
+ // Required.
+ Value string `json:"value"`
+
+ // Domain is the cookie domain.
+ // Required.
+ Domain string `json:"domain"`
+
+ // Path is the cookie path.
+ // Optional.
+ Path string `json:"path,omitempty"`
+
+ // Secure sets the cookie secure if true.
+ // Optional.
+ Secure bool `json:"secure,omitempty"`
+
+ // HttpOnly sets the cookie as HTTP-only if true.
+ // Optional.
+ HttpOnly bool `json:"httpOnly,omitempty"`
+
+ // SameSite is cookie 'Same-Site' status.
+ // Optional.
+ SameSite network.CookieSameSite `json:"sameSite,omitempty"`
+}
+
// Api helps to interact with Chromium for converting HTML documents to PDF.
type Api interface {
Pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error
diff --git a/pkg/modules/chromium/routes.go b/pkg/modules/chromium/routes.go
index 51409e93..d45203e5 100644
--- a/pkg/modules/chromium/routes.go
+++ b/pkg/modules/chromium/routes.go
@@ -34,6 +34,7 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
waitDelay time.Duration
waitWindowStatus string
waitForExpression string
+ cookies []Cookie
extraHttpHeaders map[string]string
emulatedMediaType string
omitBackground bool
@@ -58,6 +59,25 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay).
String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus).
String("waitForExpression", &waitForExpression, defaultOptions.WaitForExpression).
+ Custom("cookies", func(value string) error {
+ if value == "" {
+ cookies = defaultOptions.Cookies
+ return nil
+ }
+
+ err := json.Unmarshal([]byte(value), &cookies)
+ if err != nil {
+ return fmt.Errorf("unmarshal cookies: %w", err)
+ }
+
+ for i, cookie := range cookies {
+ if strings.TrimSpace(cookie.Name) == "" || strings.TrimSpace(cookie.Value) == "" || strings.TrimSpace(cookie.Domain) == "" {
+ err = multierr.Append(err, fmt.Errorf("cookie %d must have its name, value and domain set", i))
+ }
+ }
+
+ return err
+ }).
Custom("extraHttpHeaders", func(value string) error {
if value == "" {
extraHttpHeaders = defaultOptions.ExtraHttpHeaders
@@ -94,6 +114,7 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
WaitDelay: waitDelay,
WaitWindowStatus: waitWindowStatus,
WaitForExpression: waitForExpression,
+ Cookies: cookies,
ExtraHttpHeaders: extraHttpHeaders,
EmulatedMediaType: emulatedMediaType,
OmitBackground: omitBackground,
diff --git a/pkg/modules/chromium/routes_test.go b/pkg/modules/chromium/routes_test.go
index 56e1a69d..af0d4885 100644
--- a/pkg/modules/chromium/routes_test.go
+++ b/pkg/modules/chromium/routes_test.go
@@ -62,6 +62,59 @@ func TestFormDataChromiumOptions(t *testing.T) {
return options
}(),
},
+ {
+ scenario: "invalid cookies form field",
+ ctx: func() *api.ContextMock {
+ ctx := &api.ContextMock{Context: new(api.Context)}
+ ctx.SetValues(map[string][]string{
+ "cookies": {
+ "foo",
+ },
+ })
+ return ctx
+ }(),
+ expectedOptions: DefaultOptions(),
+ },
+ {
+ scenario: "invalid cookies form field (missing required values)",
+ ctx: func() *api.ContextMock {
+ ctx := &api.ContextMock{Context: new(api.Context)}
+ ctx.SetValues(map[string][]string{
+ "cookies": {
+ "[{}]",
+ },
+ })
+ return ctx
+ }(),
+ expectedOptions: func() Options {
+ options := DefaultOptions()
+ // No validation in this method, so it still instantiates
+ // an empty item.
+ options.Cookies = []Cookie{{}}
+ return options
+ }(),
+ },
+ {
+ scenario: "valid cookies form field",
+ ctx: func() *api.ContextMock {
+ ctx := &api.ContextMock{Context: new(api.Context)}
+ ctx.SetValues(map[string][]string{
+ "cookies": {
+ `[{"name":"foo","value":"bar","domain":".foo.bar"}]`,
+ },
+ })
+ return ctx
+ }(),
+ expectedOptions: func() Options {
+ options := DefaultOptions()
+ options.Cookies = []Cookie{{
+ Name: "foo",
+ Value: "bar",
+ Domain: ".foo.bar",
+ }}
+ return options
+ }(),
+ },
{
scenario: "invalid extraHttpHeaders form field",
ctx: func() *api.ContextMock {
diff --git a/pkg/modules/chromium/tasks.go b/pkg/modules/chromium/tasks.go
index d1d4b8f4..4047dde7 100644
--- a/pkg/modules/chromium/tasks.go
+++ b/pkg/modules/chromium/tasks.go
@@ -3,6 +3,7 @@ package chromium
import (
"bufio"
"context"
+ "errors"
"fmt"
"os"
"time"
@@ -210,6 +211,55 @@ func disableJavaScriptActionFunc(logger *zap.Logger, disable bool) chromedp.Acti
}
}
+func setCookiesActionFunc(logger *zap.Logger, cookies []Cookie) chromedp.ActionFunc {
+ return func(ctx context.Context) error {
+ if len(cookies) == 0 {
+ logger.Debug("no cookies to set")
+ return nil
+ }
+
+ deadline, ok := ctx.Deadline()
+ if !ok {
+ return errors.New("context has no deadline, cannot set cookies")
+ }
+ epochTime := cdp.TimeSinceEpoch(deadline)
+
+ cookiePretty := func(c *network.SetCookieParams) string {
+ return fmt.Sprintf(
+ "Name: '%s', Value: '%s', Domain: '%s', Path: '%s', Secure: %t, HTTPOnly: %t, SameSite: '%s', Expires: %s",
+ c.Name,
+ c.Value,
+ c.Domain,
+ c.Path,
+ c.Secure,
+ c.HTTPOnly,
+ c.SameSite.String(),
+ c.Expires.Time().String(),
+ )
+ }
+
+ for _, cookie := range cookies {
+ cookieParams := network.
+ SetCookie(cookie.Name, cookie.Value).
+ WithDomain(cookie.Domain).
+ WithPath(cookie.Path).
+ WithSecure(cookie.Secure).
+ WithHTTPOnly(cookie.HttpOnly).
+ WithSameSite(cookie.SameSite).
+ WithExpires(&epochTime)
+
+ err := cookieParams.Do(ctx)
+ if err != nil {
+ return fmt.Errorf("set cookie %s: %w", cookiePretty(cookieParams), err)
+ }
+
+ logger.Debug(fmt.Sprintf("set cookie %s", cookiePretty(cookieParams)))
+ }
+
+ return nil
+ }
+}
+
func extraHttpHeadersActionFunc(logger *zap.Logger, extraHttpHeaders map[string]string) chromedp.ActionFunc {
return func(ctx context.Context) error {
if len(extraHttpHeaders) == 0 {