feat(chromium): set cookies

This commit is contained in:
Julien Neuhart
2024-04-03 11:53:40 +02:00
parent 75c4a4f253
commit 1d66b43da1
6 changed files with 236 additions and 0 deletions

View File

@@ -226,6 +226,7 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outp
clearCacheActionFunc(logger, b.arguments.clearCache), clearCacheActionFunc(logger, b.arguments.clearCache),
clearCookiesActionFunc(logger, b.arguments.clearCookies), clearCookiesActionFunc(logger, b.arguments.clearCookies),
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript), disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
setCookiesActionFunc(logger, options.Cookies),
extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders), extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders),
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent), navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, options.PrintBackground), 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), clearCacheActionFunc(logger, b.arguments.clearCache),
clearCookiesActionFunc(logger, b.arguments.clearCookies), clearCookiesActionFunc(logger, b.arguments.clearCookies),
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript), disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
setCookiesActionFunc(logger, options.Cookies),
extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders), extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders),
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent), navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, true), hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, true),

View File

@@ -579,6 +579,41 @@ func TestChromiumBrowser_pdf(t *testing.T) {
"JavaScript disabled, skipping wait expression", "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("<h1>Set cookies</h1>"), 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", scenario: "extra HTTP headers",
browser: newChromiumBrowser( browser: newChromiumBrowser(
@@ -1125,6 +1160,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
"cache not cleared", "cache not cleared",
"cookies not cleared", "cookies not cleared",
"JavaScript not disabled", "JavaScript not disabled",
"no cookies to set",
"no extra HTTP headers", "no extra HTTP headers",
"navigate to", "navigate to",
"default white background not hidden", "default white background not hidden",
@@ -1549,6 +1585,41 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
"JavaScript disabled, skipping wait expression", "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("<h1>Set cookies</h1>"), 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", scenario: "extra HTTP headers",
browser: newChromiumBrowser( browser: newChromiumBrowser(
@@ -1964,6 +2035,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
"cache not cleared", "cache not cleared",
"cookies not cleared", "cookies not cleared",
"JavaScript not disabled", "JavaScript not disabled",
"no cookies to set",
"no extra HTTP headers", "no extra HTTP headers",
"navigate to", "navigate to",
"default white background not hidden", "default white background not hidden",

View File

@@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/alexliesenfeld/health" "github.com/alexliesenfeld/health"
"github.com/chromedp/cdproto/network"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
"go.uber.org/zap" "go.uber.org/zap"
@@ -103,6 +104,10 @@ type Options struct {
// Optional. // Optional.
WaitForExpression string 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 // ExtraHttpHeaders are the HTTP headers to send by Chromium while loading
// the HTML document. // the HTML document.
// Optional. // Optional.
@@ -128,6 +133,7 @@ func DefaultOptions() Options {
WaitDelay: 0, WaitDelay: 0,
WaitWindowStatus: "", WaitWindowStatus: "",
WaitForExpression: "", WaitForExpression: "",
Cookies: nil,
ExtraHttpHeaders: nil, ExtraHttpHeaders: nil,
EmulatedMediaType: "", EmulatedMediaType: "",
OmitBackground: false, 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. // Api helps to interact with Chromium for converting HTML documents to PDF.
type Api interface { type Api interface {
Pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error Pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error

View File

@@ -34,6 +34,7 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
waitDelay time.Duration waitDelay time.Duration
waitWindowStatus string waitWindowStatus string
waitForExpression string waitForExpression string
cookies []Cookie
extraHttpHeaders map[string]string extraHttpHeaders map[string]string
emulatedMediaType string emulatedMediaType string
omitBackground bool omitBackground bool
@@ -58,6 +59,25 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay). Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay).
String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus). String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus).
String("waitForExpression", &waitForExpression, defaultOptions.WaitForExpression). 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 { Custom("extraHttpHeaders", func(value string) error {
if value == "" { if value == "" {
extraHttpHeaders = defaultOptions.ExtraHttpHeaders extraHttpHeaders = defaultOptions.ExtraHttpHeaders
@@ -94,6 +114,7 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
WaitDelay: waitDelay, WaitDelay: waitDelay,
WaitWindowStatus: waitWindowStatus, WaitWindowStatus: waitWindowStatus,
WaitForExpression: waitForExpression, WaitForExpression: waitForExpression,
Cookies: cookies,
ExtraHttpHeaders: extraHttpHeaders, ExtraHttpHeaders: extraHttpHeaders,
EmulatedMediaType: emulatedMediaType, EmulatedMediaType: emulatedMediaType,
OmitBackground: omitBackground, OmitBackground: omitBackground,

View File

@@ -62,6 +62,59 @@ func TestFormDataChromiumOptions(t *testing.T) {
return options 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", scenario: "invalid extraHttpHeaders form field",
ctx: func() *api.ContextMock { ctx: func() *api.ContextMock {

View File

@@ -3,6 +3,7 @@ package chromium
import ( import (
"bufio" "bufio"
"context" "context"
"errors"
"fmt" "fmt"
"os" "os"
"time" "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 { func extraHttpHeadersActionFunc(logger *zap.Logger, extraHttpHeaders map[string]string) chromedp.ActionFunc {
return func(ctx context.Context) error { return func(ctx context.Context) error {
if len(extraHttpHeaders) == 0 { if len(extraHttpHeaders) == 0 {