mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-11 09:52:14 +01:00
feat(chromium): add scope to extraHttpHeaders
This commit is contained in:
@@ -228,7 +228,6 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outp
|
||||
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
|
||||
setCookiesActionFunc(logger, options.Cookies),
|
||||
userAgentOverride(logger, options.UserAgent),
|
||||
extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders),
|
||||
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
|
||||
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, options.PrintBackground),
|
||||
forceExactColorsActionFunc(),
|
||||
@@ -252,7 +251,6 @@ func (b *chromiumBrowser) screenshot(ctx context.Context, logger *zap.Logger, ur
|
||||
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
|
||||
setCookiesActionFunc(logger, options.Cookies),
|
||||
userAgentOverride(logger, options.UserAgent),
|
||||
extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders),
|
||||
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
|
||||
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, true),
|
||||
forceExactColorsActionFunc(),
|
||||
@@ -291,8 +289,14 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string
|
||||
defer taskCancel()
|
||||
|
||||
// We validate all others requests against our allow / deny lists.
|
||||
// If a request does not pass the validation, we make it fail.
|
||||
listenForEventRequestPaused(taskCtx, logger, b.arguments.allowList, b.arguments.denyList)
|
||||
// If a request does not pass the validation, we make it fail. It also set
|
||||
// the extra HTTP headers, if any.
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1011.
|
||||
listenForEventRequestPaused(taskCtx, logger, eventRequestPausedOptions{
|
||||
allowList: b.arguments.allowList,
|
||||
denyList: b.arguments.denyList,
|
||||
extraHttpHeaders: options.ExtraHttpHeaders,
|
||||
})
|
||||
|
||||
var (
|
||||
invalidHttpStatusCode error
|
||||
|
||||
@@ -702,8 +702,21 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
return fs
|
||||
}(),
|
||||
options: PdfOptions{
|
||||
Options: Options{ExtraHttpHeaders: map[string]string{
|
||||
"X-Foo": "Bar",
|
||||
Options: Options{ExtraHttpHeaders: []ExtraHttpHeader{
|
||||
{
|
||||
Name: "X-Foo",
|
||||
Value: "foo",
|
||||
},
|
||||
{
|
||||
Name: "X-Bar",
|
||||
Value: "bar",
|
||||
Scope: regexp2.MustCompile(`.*index\.html.*`, 0),
|
||||
},
|
||||
{
|
||||
Name: "X-Baz",
|
||||
Value: "baz",
|
||||
Scope: regexp2.MustCompile(`.*another\.html.*`, 0),
|
||||
},
|
||||
}},
|
||||
},
|
||||
noDeadline: false,
|
||||
@@ -711,6 +724,10 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
expectError: false,
|
||||
expectedLogEntries: []string{
|
||||
"extra HTTP headers:",
|
||||
"extra HTTP header 'X-Foo' will be set for request URL",
|
||||
"extra HTTP header 'X-Bar' (scoped) will be set for request URL",
|
||||
"extra HTTP header 'X-Baz' (scoped) will not be set for request URL",
|
||||
"setting extra HTTP headers for request URL",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1716,6 +1733,41 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
"set cookie",
|
||||
},
|
||||
},
|
||||
{
|
||||
scenario: "user agent override",
|
||||
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>User-Agent override</h1>"), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return fs
|
||||
}(),
|
||||
options: ScreenshotOptions{
|
||||
Options: Options{UserAgent: "foo"},
|
||||
},
|
||||
noDeadline: false,
|
||||
start: true,
|
||||
expectError: false,
|
||||
expectedLogEntries: []string{
|
||||
fmt.Sprintf("user agent override: foo"),
|
||||
},
|
||||
},
|
||||
{
|
||||
scenario: "extra HTTP headers",
|
||||
browser: newChromiumBrowser(
|
||||
@@ -1742,8 +1794,21 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
return fs
|
||||
}(),
|
||||
options: ScreenshotOptions{
|
||||
Options: Options{ExtraHttpHeaders: map[string]string{
|
||||
"X-Foo": "Bar",
|
||||
Options: Options{ExtraHttpHeaders: []ExtraHttpHeader{
|
||||
{
|
||||
Name: "X-Foo",
|
||||
Value: "foo",
|
||||
},
|
||||
{
|
||||
Name: "X-Bar",
|
||||
Value: "bar",
|
||||
Scope: regexp2.MustCompile(`.*index\.html.*`, 0),
|
||||
},
|
||||
{
|
||||
Name: "X-Baz",
|
||||
Value: "baz",
|
||||
Scope: regexp2.MustCompile(`.*another\.html.*`, 0),
|
||||
},
|
||||
}},
|
||||
},
|
||||
noDeadline: false,
|
||||
@@ -1751,6 +1816,10 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
expectError: false,
|
||||
expectedLogEntries: []string{
|
||||
"extra HTTP headers:",
|
||||
"extra HTTP header 'X-Foo' will be set for request URL",
|
||||
"extra HTTP header 'X-Bar' (scoped) will be set for request URL",
|
||||
"extra HTTP header 'X-Baz' (scoped) will not be set for request URL",
|
||||
"setting extra HTTP headers for request URL",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/alexliesenfeld/health"
|
||||
"github.com/chromedp/cdproto/network"
|
||||
"github.com/dlclark/regexp2"
|
||||
flag "github.com/spf13/pflag"
|
||||
"go.uber.org/zap"
|
||||
|
||||
@@ -109,7 +110,7 @@ type Options struct {
|
||||
|
||||
// ExtraHttpHeaders are extra HTTP headers to send by Chromium while
|
||||
// loading he HTML document.
|
||||
ExtraHttpHeaders map[string]string
|
||||
ExtraHttpHeaders []ExtraHttpHeader
|
||||
|
||||
// EmulatedMediaType is the media type to emulate, either "screen" or
|
||||
// "print".
|
||||
@@ -289,6 +290,22 @@ type Cookie struct {
|
||||
SameSite network.CookieSameSite `json:"sameSite,omitempty"`
|
||||
}
|
||||
|
||||
// ExtraHttpHeader are extra HTTP headers to send by Chromium.
|
||||
type ExtraHttpHeader struct {
|
||||
// Name is the header name.
|
||||
// Required.
|
||||
Name string
|
||||
|
||||
// Value is the header value.
|
||||
// Required.
|
||||
Value string
|
||||
|
||||
// Scope is the header scope. If nil, the header will be applied to ALL
|
||||
// requests from the page.
|
||||
// Optional.
|
||||
Scope *regexp2.Regexp
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -20,10 +20,22 @@ import (
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
)
|
||||
|
||||
type eventRequestPausedOptions struct {
|
||||
allowList, denyList *regexp2.Regexp
|
||||
extraHttpHeaders []ExtraHttpHeader
|
||||
}
|
||||
|
||||
// listenForEventRequestPaused listens for requests to check if they are
|
||||
// allowed or not.network.SetBlockedURLS()
|
||||
// allowed or not. It also set the extra HTTP headers, if any.
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1011.
|
||||
// TODO: https://chromedevtools.github.io/devtools-protocol/tot/Network/#method-setBlockedURLs (experimental for now).
|
||||
func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowList *regexp2.Regexp, denyList *regexp2.Regexp) {
|
||||
func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, options eventRequestPausedOptions) {
|
||||
if len(options.extraHttpHeaders) == 0 {
|
||||
logger.Debug("no extra HTTP headers")
|
||||
} else {
|
||||
logger.Debug(fmt.Sprintf("extra HTTP headers: %+v", options.extraHttpHeaders))
|
||||
}
|
||||
|
||||
chromedp.ListenTarget(ctx, func(ev interface{}) {
|
||||
switch e := ev.(type) {
|
||||
case *fetch.EventRequestPaused:
|
||||
@@ -37,7 +49,7 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowL
|
||||
return
|
||||
}
|
||||
|
||||
err := gotenberg.FilterDeadline(allowList, denyList, e.Request.URL, deadline)
|
||||
err := gotenberg.FilterDeadline(options.allowList, options.denyList, e.Request.URL, deadline)
|
||||
if err != nil {
|
||||
logger.Warn(err.Error())
|
||||
allow = false
|
||||
@@ -46,19 +58,78 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowL
|
||||
cctx := chromedp.FromContext(ctx)
|
||||
executorCtx := cdp.WithExecutor(ctx, cctx.Target)
|
||||
|
||||
if allow {
|
||||
req := fetch.ContinueRequest(e.RequestID)
|
||||
if !allow {
|
||||
req := fetch.FailRequest(e.RequestID, network.ErrorReasonAccessDenied)
|
||||
err = req.Do(executorCtx)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("continue request: %s", err))
|
||||
logger.Error(fmt.Sprintf("fail request: %s", err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
req := fetch.FailRequest(e.RequestID, network.ErrorReasonAccessDenied)
|
||||
req := fetch.ContinueRequest(e.RequestID)
|
||||
|
||||
var extraHttpHeadersToSet []ExtraHttpHeader
|
||||
if len(options.extraHttpHeaders) > 0 {
|
||||
// The user want to set extra HTTP headers.
|
||||
|
||||
// First, we have to check if at least one header has to be
|
||||
// set for current request.
|
||||
for _, header := range options.extraHttpHeaders {
|
||||
if header.Scope == nil {
|
||||
// Non-scoped header.
|
||||
logger.Debug(fmt.Sprintf("extra HTTP header '%s' will be set for request URL '%s'", header.Name, e.Request.URL))
|
||||
extraHttpHeadersToSet = append(extraHttpHeadersToSet, header)
|
||||
continue
|
||||
}
|
||||
|
||||
ok, err := header.Scope.MatchString(e.Request.URL)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("fail to match extra HTTP header '%s' scope with URL '%s': %s", header.Name, e.Request.URL, err))
|
||||
} else if ok {
|
||||
logger.Debug(fmt.Sprintf("extra HTTP header '%s' (scoped) will be set for request URL '%s'", header.Name, e.Request.URL))
|
||||
extraHttpHeadersToSet = append(extraHttpHeadersToSet, header)
|
||||
} else {
|
||||
logger.Debug(fmt.Sprintf("scoped extra HTTP header '%s' (scoped) will not be set for request URL '%s'", header.Name, e.Request.URL))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(extraHttpHeadersToSet) > 0 {
|
||||
logger.Debug(fmt.Sprintf("setting extra HTTP headers for request URL '%s': %+v", e.Request.URL, extraHttpHeadersToSet))
|
||||
|
||||
originalHeaders := e.Request.Headers
|
||||
headers := make(map[string]string)
|
||||
|
||||
for key, value := range originalHeaders {
|
||||
strValue, ok := value.(string)
|
||||
if ok {
|
||||
headers[key] = strValue
|
||||
} else {
|
||||
logger.Error(fmt.Sprintf("ignoring header '%s' for URL '%s' since it cannot be cast to a string", key, e.Request.URL))
|
||||
}
|
||||
}
|
||||
|
||||
var headersEntries []*fetch.HeaderEntry
|
||||
for key, value := range headers {
|
||||
headersEntries = append(headersEntries, &fetch.HeaderEntry{
|
||||
Name: key,
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
for _, header := range extraHttpHeadersToSet {
|
||||
headersEntries = append(headersEntries, &fetch.HeaderEntry{
|
||||
Name: header.Name,
|
||||
Value: header.Value,
|
||||
})
|
||||
}
|
||||
|
||||
req.Headers = headersEntries
|
||||
}
|
||||
|
||||
err = req.Do(executorCtx)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("fail request: %s", err))
|
||||
logger.Error(fmt.Sprintf("continue request: %s", err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dlclark/regexp2"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"github.com/russross/blackfriday/v2"
|
||||
@@ -36,7 +37,7 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
|
||||
waitForExpression string
|
||||
cookies []Cookie
|
||||
userAgent string
|
||||
extraHttpHeaders map[string]string
|
||||
extraHttpHeaders []ExtraHttpHeader
|
||||
emulatedMediaType string
|
||||
omitBackground bool
|
||||
)
|
||||
@@ -86,12 +87,59 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(value), &extraHttpHeaders)
|
||||
var headers map[string]string
|
||||
err := json.Unmarshal([]byte(value), &headers)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshal extraHttpHeaders: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
for k, v := range headers {
|
||||
var scope string
|
||||
var valueTokens []string
|
||||
var invalidScopeToken bool
|
||||
|
||||
tokens := strings.Split(v, ";")
|
||||
for _, token := range tokens {
|
||||
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(token)), "scope") {
|
||||
tokenNoSpaces := strings.Join(strings.Fields(token), "")
|
||||
parts := strings.SplitN(tokenNoSpaces, "=", 2)
|
||||
|
||||
if len(parts) == 2 && strings.ToLower(parts[0]) == "scope" && parts[1] != "" {
|
||||
scope = parts[1]
|
||||
} else {
|
||||
err = multierr.Append(err, fmt.Errorf("invalid scope '%s' for header '%s'", scope, k))
|
||||
invalidScopeToken = true
|
||||
break
|
||||
}
|
||||
} else {
|
||||
if token != "" {
|
||||
valueTokens = append(valueTokens, token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if invalidScopeToken {
|
||||
continue
|
||||
}
|
||||
|
||||
var scopeRegexp *regexp2.Regexp
|
||||
if len(scope) > 0 {
|
||||
p, errCompile := regexp2.Compile(scope, 0)
|
||||
if errCompile != nil {
|
||||
err = multierr.Append(err, fmt.Errorf("invalid scope regex pattern for header '%s': %w", k, errCompile))
|
||||
continue
|
||||
}
|
||||
scopeRegexp = p
|
||||
}
|
||||
|
||||
extraHttpHeaders = append(extraHttpHeaders, ExtraHttpHeader{
|
||||
Name: k,
|
||||
Value: strings.Join(valueTokens, "; "),
|
||||
Scope: scopeRegexp,
|
||||
})
|
||||
}
|
||||
|
||||
return err
|
||||
}).
|
||||
Custom("emulatedMediaType", func(value string) error {
|
||||
if value == "" {
|
||||
|
||||
@@ -7,8 +7,10 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/dlclark/regexp2"
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
"go.uber.org/zap"
|
||||
@@ -19,14 +21,18 @@ import (
|
||||
|
||||
func TestFormDataChromiumOptions(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
expectedOptions Options
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
expectedOptions Options
|
||||
compareWithoutDeepEqual bool
|
||||
expectValidationError bool
|
||||
}{
|
||||
{
|
||||
scenario: "no custom form fields",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectedOptions: DefaultOptions(),
|
||||
scenario: "no custom form fields",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectedOptions: DefaultOptions(),
|
||||
compareWithoutDeepEqual: false,
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "invalid failOnHttpStatusCodes form field",
|
||||
@@ -44,6 +50,8 @@ func TestFormDataChromiumOptions(t *testing.T) {
|
||||
options.FailOnHttpStatusCodes = nil
|
||||
return options
|
||||
}(),
|
||||
compareWithoutDeepEqual: false,
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "valid failOnHttpStatusCodes form field",
|
||||
@@ -61,6 +69,8 @@ func TestFormDataChromiumOptions(t *testing.T) {
|
||||
options.FailOnHttpStatusCodes = []int64{399, 499, 599}
|
||||
return options
|
||||
}(),
|
||||
compareWithoutDeepEqual: false,
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "invalid cookies form field",
|
||||
@@ -73,7 +83,9 @@ func TestFormDataChromiumOptions(t *testing.T) {
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
expectedOptions: DefaultOptions(),
|
||||
expectedOptions: DefaultOptions(),
|
||||
compareWithoutDeepEqual: false,
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "invalid cookies form field (missing required values)",
|
||||
@@ -93,6 +105,8 @@ func TestFormDataChromiumOptions(t *testing.T) {
|
||||
options.Cookies = []Cookie{{}}
|
||||
return options
|
||||
}(),
|
||||
compareWithoutDeepEqual: false,
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "valid cookies form field",
|
||||
@@ -114,9 +128,11 @@ func TestFormDataChromiumOptions(t *testing.T) {
|
||||
}}
|
||||
return options
|
||||
}(),
|
||||
compareWithoutDeepEqual: false,
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "invalid extraHttpHeaders form field",
|
||||
scenario: "invalid extraHttpHeaders form field: cannot unmarshall",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetValues(map[string][]string{
|
||||
@@ -126,7 +142,39 @@ func TestFormDataChromiumOptions(t *testing.T) {
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
expectedOptions: DefaultOptions(),
|
||||
expectedOptions: DefaultOptions(),
|
||||
compareWithoutDeepEqual: false,
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "invalid extraHttpHeaders form field: invalid scope",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetValues(map[string][]string{
|
||||
"extraHttpHeaders": {
|
||||
`{"foo":"bar;scope;;"}`,
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
expectedOptions: DefaultOptions(),
|
||||
compareWithoutDeepEqual: false,
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "invalid extraHttpHeaders form field: invalid scope regex pattern",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetValues(map[string][]string{
|
||||
"extraHttpHeaders": {
|
||||
`{"foo":"bar;scope=*."}`,
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
expectedOptions: DefaultOptions(),
|
||||
compareWithoutDeepEqual: false,
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "valid extraHttpHeaders form field",
|
||||
@@ -134,18 +182,28 @@ func TestFormDataChromiumOptions(t *testing.T) {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetValues(map[string][]string{
|
||||
"extraHttpHeaders": {
|
||||
`{"foo":"bar"}`,
|
||||
`{"foo":"bar","baz":"qux;scope=https?:\\/\\/([a-zA-Z0-9-]+\\.)*qux\\.com\\/.*"}`,
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
expectedOptions: func() Options {
|
||||
options := DefaultOptions()
|
||||
options.ExtraHttpHeaders = map[string]string{
|
||||
"foo": "bar",
|
||||
options.ExtraHttpHeaders = []ExtraHttpHeader{
|
||||
{
|
||||
Name: "foo",
|
||||
Value: "bar",
|
||||
},
|
||||
{
|
||||
Name: "baz",
|
||||
Value: "qux",
|
||||
Scope: regexp2.MustCompile(`https?:\/\/([a-zA-Z0-9-]+\.)*qux\.com\/.*`, 0),
|
||||
},
|
||||
}
|
||||
return options
|
||||
}(),
|
||||
compareWithoutDeepEqual: true,
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "invalid emulatedMediaType form field",
|
||||
@@ -158,7 +216,8 @@ func TestFormDataChromiumOptions(t *testing.T) {
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
expectedOptions: DefaultOptions(),
|
||||
expectedOptions: DefaultOptions(),
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "valid emulatedMediaType form field",
|
||||
@@ -176,14 +235,61 @@ func TestFormDataChromiumOptions(t *testing.T) {
|
||||
options.EmulatedMediaType = "screen"
|
||||
return options
|
||||
}(),
|
||||
expectValidationError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
tc.ctx.SetLogger(zap.NewNop())
|
||||
_, actual := FormDataChromiumOptions(tc.ctx.Context)
|
||||
form, actual := FormDataChromiumOptions(tc.ctx.Context)
|
||||
|
||||
if !reflect.DeepEqual(actual, tc.expectedOptions) {
|
||||
t.Fatalf("expected %+v but got: %+v", tc.expectedOptions, actual)
|
||||
if tc.compareWithoutDeepEqual {
|
||||
if len(tc.expectedOptions.ExtraHttpHeaders) != len(actual.ExtraHttpHeaders) {
|
||||
t.Fatalf("expected %d extra HTTP headers, but got %d", len(tc.expectedOptions.ExtraHttpHeaders), len(actual.ExtraHttpHeaders))
|
||||
}
|
||||
|
||||
sort.Slice(tc.expectedOptions.ExtraHttpHeaders, func(i, j int) bool {
|
||||
return tc.expectedOptions.ExtraHttpHeaders[i].Name < tc.expectedOptions.ExtraHttpHeaders[j].Name
|
||||
})
|
||||
sort.Slice(actual.ExtraHttpHeaders, func(i, j int) bool {
|
||||
return actual.ExtraHttpHeaders[i].Name < actual.ExtraHttpHeaders[j].Name
|
||||
})
|
||||
|
||||
for i := range tc.expectedOptions.ExtraHttpHeaders {
|
||||
if tc.expectedOptions.ExtraHttpHeaders[i].Name != actual.ExtraHttpHeaders[i].Name {
|
||||
t.Fatalf("expected '%s' extra HTTP header, but got '%s'", tc.expectedOptions.ExtraHttpHeaders[i].Name, tc.expectedOptions.ExtraHttpHeaders[i].Name)
|
||||
}
|
||||
|
||||
if tc.expectedOptions.ExtraHttpHeaders[i].Value != actual.ExtraHttpHeaders[i].Value {
|
||||
t.Fatalf("expected '%s' as value for extra HTTP header '%s', but got '%s'", tc.expectedOptions.ExtraHttpHeaders[i].Value, tc.expectedOptions.ExtraHttpHeaders[i].Name, actual.ExtraHttpHeaders[i].Value)
|
||||
}
|
||||
|
||||
var expectedScope string
|
||||
if tc.expectedOptions.ExtraHttpHeaders[i].Scope != nil {
|
||||
expectedScope = tc.expectedOptions.ExtraHttpHeaders[i].Scope.String()
|
||||
}
|
||||
var actualScope string
|
||||
if actual.ExtraHttpHeaders[i].Scope != nil {
|
||||
actualScope = actual.ExtraHttpHeaders[i].Scope.String()
|
||||
}
|
||||
|
||||
if expectedScope != actualScope {
|
||||
t.Fatalf("expected '%s' as scope for extra HTTP header '%s', but got '%s'", expectedScope, tc.expectedOptions.ExtraHttpHeaders[i].Name, actualScope)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !reflect.DeepEqual(actual, tc.expectedOptions) {
|
||||
t.Fatalf("expected %+v but got: %+v", tc.expectedOptions, actual)
|
||||
}
|
||||
}
|
||||
|
||||
err := form.Validate()
|
||||
|
||||
if tc.expectValidationError && err == nil {
|
||||
t.Fatal("expected validation error but got none", err)
|
||||
}
|
||||
|
||||
if !tc.expectValidationError && err != nil {
|
||||
t.Fatalf("expected no validation error but got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -191,14 +297,16 @@ func TestFormDataChromiumOptions(t *testing.T) {
|
||||
|
||||
func TestFormDataChromiumPdfOptions(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
expectedOptions PdfOptions
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
expectedOptions PdfOptions
|
||||
expectValidationError bool
|
||||
}{
|
||||
{
|
||||
scenario: "no custom form fields",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectedOptions: DefaultPdfOptions(),
|
||||
scenario: "no custom form fields",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectedOptions: DefaultPdfOptions(),
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "custom form fields (Options & PdfOptions)",
|
||||
@@ -220,29 +328,42 @@ func TestFormDataChromiumPdfOptions(t *testing.T) {
|
||||
options.EmulatedMediaType = "screen"
|
||||
return options
|
||||
}(),
|
||||
expectValidationError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
tc.ctx.SetLogger(zap.NewNop())
|
||||
_, actual := FormDataChromiumPdfOptions(tc.ctx.Context)
|
||||
form, actual := FormDataChromiumPdfOptions(tc.ctx.Context)
|
||||
|
||||
if !reflect.DeepEqual(actual, tc.expectedOptions) {
|
||||
t.Fatalf("expected %+v but got: %+v", tc.expectedOptions, actual)
|
||||
}
|
||||
|
||||
err := form.Validate()
|
||||
|
||||
if tc.expectValidationError && err == nil {
|
||||
t.Fatal("expected validation error but got none", err)
|
||||
}
|
||||
|
||||
if !tc.expectValidationError && err != nil {
|
||||
t.Fatalf("expected no validation error but got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormDataChromiumScreenshotOptions(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
expectedOptions ScreenshotOptions
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
expectedOptions ScreenshotOptions
|
||||
expectValidationError bool
|
||||
}{
|
||||
{
|
||||
scenario: "no custom form fields",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectedOptions: DefaultScreenshotOptions(),
|
||||
scenario: "no custom form fields",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectedOptions: DefaultScreenshotOptions(),
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "invalid format form field",
|
||||
@@ -260,6 +381,7 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
|
||||
options.Format = ""
|
||||
return options
|
||||
}(),
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "valid png format form field",
|
||||
@@ -277,6 +399,7 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
|
||||
options.Format = "png"
|
||||
return options
|
||||
}(),
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "valid jpeg format form field",
|
||||
@@ -294,6 +417,7 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
|
||||
options.Format = "jpeg"
|
||||
return options
|
||||
}(),
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "valid webp format form field",
|
||||
@@ -311,6 +435,7 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
|
||||
options.Format = "webp"
|
||||
return options
|
||||
}(),
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "invalid quality form field (not an integer)",
|
||||
@@ -328,6 +453,7 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
|
||||
options.Quality = 0
|
||||
return options
|
||||
}(),
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "invalid quality form field (< 0)",
|
||||
@@ -345,6 +471,7 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
|
||||
options.Quality = 0
|
||||
return options
|
||||
}(),
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "invalid quality form field (> 100)",
|
||||
@@ -362,6 +489,7 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
|
||||
options.Quality = 0
|
||||
return options
|
||||
}(),
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "valid quality form field",
|
||||
@@ -379,6 +507,7 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
|
||||
options.Quality = 50
|
||||
return options
|
||||
}(),
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "custom form fields (Options & ScreenshotOptions)",
|
||||
@@ -412,29 +541,42 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
|
||||
options.EmulatedMediaType = "screen"
|
||||
return options
|
||||
}(),
|
||||
expectValidationError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
tc.ctx.SetLogger(zap.NewNop())
|
||||
_, actual := FormDataChromiumScreenshotOptions(tc.ctx.Context)
|
||||
form, actual := FormDataChromiumScreenshotOptions(tc.ctx.Context)
|
||||
|
||||
if !reflect.DeepEqual(actual, tc.expectedOptions) {
|
||||
t.Fatalf("expected %+v but got: %+v", tc.expectedOptions, actual)
|
||||
}
|
||||
|
||||
err := form.Validate()
|
||||
|
||||
if tc.expectValidationError && err == nil {
|
||||
t.Fatal("expected validation error but got none", err)
|
||||
}
|
||||
|
||||
if !tc.expectValidationError && err != nil {
|
||||
t.Fatalf("expected no validation error but got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormDataChromiumPdfFormats(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
expectedPdfFormats gotenberg.PdfFormats
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
expectedPdfFormats gotenberg.PdfFormats
|
||||
expectValidationError bool
|
||||
}{
|
||||
{
|
||||
scenario: "no custom form fields",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectedPdfFormats: gotenberg.PdfFormats{},
|
||||
scenario: "no custom form fields",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectedPdfFormats: gotenberg.PdfFormats{},
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "pdfa and pdfua form fields",
|
||||
@@ -450,30 +592,44 @@ func TestFormDataChromiumPdfFormats(t *testing.T) {
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
expectedPdfFormats: gotenberg.PdfFormats{PdfA: "foo", PdfUa: true},
|
||||
expectedPdfFormats: gotenberg.PdfFormats{PdfA: "foo", PdfUa: true},
|
||||
expectValidationError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
tc.ctx.SetLogger(zap.NewNop())
|
||||
actual := FormDataChromiumPdfFormats(tc.ctx.Context.FormData())
|
||||
form := tc.ctx.Context.FormData()
|
||||
actual := FormDataChromiumPdfFormats(form)
|
||||
|
||||
if !reflect.DeepEqual(actual, tc.expectedPdfFormats) {
|
||||
t.Fatalf("expected %+v but got: %+v", tc.expectedPdfFormats, actual)
|
||||
}
|
||||
|
||||
err := form.Validate()
|
||||
|
||||
if tc.expectValidationError && err == nil {
|
||||
t.Fatal("expected validation error but got none", err)
|
||||
}
|
||||
|
||||
if !tc.expectValidationError && err != nil {
|
||||
t.Fatalf("expected no validation error but got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormDataPdfMetadata(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
expectedMetadata map[string]interface{}
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
expectedMetadata map[string]interface{}
|
||||
expectValidationError bool
|
||||
}{
|
||||
{
|
||||
scenario: "no metadata form field",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectedMetadata: nil,
|
||||
scenario: "no metadata form field",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectedMetadata: nil,
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "invalid metadata form field",
|
||||
@@ -486,7 +642,8 @@ func TestFormDataPdfMetadata(t *testing.T) {
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
expectedMetadata: nil,
|
||||
expectedMetadata: nil,
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "valid metadata form field",
|
||||
@@ -502,15 +659,27 @@ func TestFormDataPdfMetadata(t *testing.T) {
|
||||
expectedMetadata: map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
expectValidationError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
tc.ctx.SetLogger(zap.NewNop())
|
||||
actual := FormDataPdfMetadata(tc.ctx.Context.FormData())
|
||||
form := tc.ctx.Context.FormData()
|
||||
actual := FormDataPdfMetadata(form)
|
||||
|
||||
if !reflect.DeepEqual(actual, tc.expectedMetadata) {
|
||||
t.Fatalf("expected %+v but got: %+v", tc.expectedMetadata, actual)
|
||||
}
|
||||
|
||||
err := form.Validate()
|
||||
|
||||
if tc.expectValidationError && err == nil {
|
||||
t.Fatal("expected validation error but got none", err)
|
||||
}
|
||||
|
||||
if !tc.expectValidationError && err != nil {
|
||||
t.Fatalf("expected no validation error but got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,28 +298,33 @@ func userAgentOverride(logger *zap.Logger, userAgent string) chromedp.ActionFunc
|
||||
}
|
||||
}
|
||||
|
||||
func extraHttpHeadersActionFunc(logger *zap.Logger, extraHttpHeaders map[string]string) chromedp.ActionFunc {
|
||||
return func(ctx context.Context) error {
|
||||
if len(extraHttpHeaders) == 0 {
|
||||
logger.Debug("no extra HTTP headers")
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Debug(fmt.Sprintf("extra HTTP headers: %+v", extraHttpHeaders))
|
||||
|
||||
headers := make(network.Headers, len(extraHttpHeaders))
|
||||
for key, value := range extraHttpHeaders {
|
||||
headers[key] = value
|
||||
}
|
||||
|
||||
err := network.SetExtraHTTPHeaders(headers).Do(ctx)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("set extra HTTP headers: %w", err)
|
||||
}
|
||||
}
|
||||
// This code has been replaced with the listenForEventRequestPaused function.
|
||||
// Indeed, the user may want to scope the headers per domain, but using
|
||||
// network.SetExtraHTTPHeaders set the headers for ALL requests from the page.
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1011.
|
||||
//
|
||||
//func extraHttpHeadersActionFunc(logger *zap.Logger, extraHttpHeaders map[string]string) chromedp.ActionFunc {
|
||||
// return func(ctx context.Context) error {
|
||||
// if len(extraHttpHeaders) == 0 {
|
||||
// logger.Debug("no extra HTTP headers")
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// logger.Debug(fmt.Sprintf("extra HTTP headers: %+v", extraHttpHeaders))
|
||||
//
|
||||
// headers := make(network.Headers, len(extraHttpHeaders))
|
||||
// for key, value := range extraHttpHeaders {
|
||||
// headers[key] = value
|
||||
// }
|
||||
//
|
||||
// err := network.SetExtraHTTPHeaders(headers).Do(ctx)
|
||||
// if err == nil {
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// return fmt.Errorf("set extra HTTP headers: %w", err)
|
||||
// }
|
||||
//}
|
||||
|
||||
func navigateActionFunc(logger *zap.Logger, url string, skipNetworkIdleEvent bool) chromedp.ActionFunc {
|
||||
return func(ctx context.Context) error {
|
||||
|
||||
Reference in New Issue
Block a user