updating with base branch

This commit is contained in:
Julien Neuhart
2019-12-10 15:17:25 +01:00
47 changed files with 1728 additions and 594 deletions

View File

@@ -8,21 +8,61 @@ import (
"github.com/labstack/echo/v4"
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/context"
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xrand"
"github.com/thecodingmachine/gotenberg/internal/pkg/xtime"
)
const (
pingEndpoint string = "/ping"
mergeEndpoint string = "/merge"
convertGroupEndpoint string = "/convert"
htmlEndpoint string = "/html"
urlEndpoint string = "/url"
markdownEndpoint string = "/markdown"
officeEndpoint string = "/office"
)
func pingEndpoint(config conf.Config) string {
return fmt.Sprintf("%s%s", config.RootPath(), "ping")
}
func mergeEndpoint(config conf.Config) string {
return fmt.Sprintf("%s%s", config.RootPath(), "merge")
}
func htmlEndpoint(config conf.Config) string {
return fmt.Sprintf("%s%s", config.RootPath(), "convert/html")
}
func urlEndpoint(config conf.Config) string {
return fmt.Sprintf("%s%s", config.RootPath(), "convert/url")
}
func markdownEndpoint(config conf.Config) string {
return fmt.Sprintf("%s%s", config.RootPath(), "convert/markdown")
}
func officeEndpoint(config conf.Config) string {
return fmt.Sprintf("%s%s", config.RootPath(), "convert/office")
}
func isMultipartFormDataEndpoint(config conf.Config, path string) bool {
var multipartFormDataEndpoints []string
multipartFormDataEndpoints = append(multipartFormDataEndpoints, mergeEndpoint(config))
if !config.DisableGoogleChrome() {
multipartFormDataEndpoints = append(
multipartFormDataEndpoints,
htmlEndpoint(config),
urlEndpoint(config),
markdownEndpoint(config),
)
}
if !config.DisableUnoconv() {
multipartFormDataEndpoints = append(
multipartFormDataEndpoints,
officeEndpoint(config),
)
}
for _, endpoint := range multipartFormDataEndpoints {
if endpoint == path {
return true
}
}
return false
}
// pingHandler is the handler for healthcheck.
func pingHandler(c echo.Context) error {
@@ -98,6 +138,7 @@ func urlHandler(c echo.Context) error {
if err != nil {
return err
}
opts.CustomHTTPHeaders = resource.RemoteURLCustomHTTPHeaders(r)
if !r.HasArg(resource.RemoteURLArgKey) {
return xerror.Invalid(
op,
@@ -282,20 +323,50 @@ func convertAsync(ctx context.Context, p printer.Printer, filename, fpath string
defer f.Close() // nolint: errcheck
logger.DebugfOp(
op,
"sending result file '%s' to '%s'",
"preparing to send result file '%s' to '%s'...",
filename,
webhookURL,
)
httpClient := &http.Client{
Timeout: xtime.Duration(webhookURLTimeout),
}
resp, err := httpClient.Post(webhookURL, "application/pdf", f) /* #nosec */
req, err := http.NewRequest(http.MethodPost, webhookURL, f)
if err != nil {
xerr := xerror.New(op, err)
logger.ErrorOp(xerror.Op(xerr), xerr)
return
}
req.Header.Set(echo.HeaderContentType, "application/pdf")
// set custom headers (if any).
customHTTPHeaders := resource.WebhookURLCustomHTTPHeaders(r)
if len(customHTTPHeaders) > 0 {
for key, value := range customHTTPHeaders {
req.Header.Set(key, value)
logger.DebugfOp(op, "set '%s' to custom HTTP header '%s'", value, key)
}
} else {
logger.DebugOp(op, "skipping custom HTTP headers as none have been provided...")
}
// send the result file.
logger.DebugfOp(
op,
"sending result file '%s' to '%s'...",
filename,
webhookURL,
)
resp, err := httpClient.Do(req) /* #nosec */
if err != nil {
xerr := xerror.New(op, err)
logger.ErrorOp(xerror.Op(xerr), xerr)
return
}
defer resp.Body.Close() // nolint: errcheck
logger.DebugfOp(
op,
"result file '%s' sent to '%s'",
filename,
webhookURL,
)
}()
return nil
}

View File

@@ -19,39 +19,51 @@ func TestPingHandler(t *testing.T) {
// should return 200.
config := conf.DefaultConfig()
srv := New(config)
req := httptest.NewRequest(http.MethodGet, pingEndpoint, nil)
endpoint := pingEndpoint(config)
req := httptest.NewRequest(http.MethodGet, endpoint, nil)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// should return 405 as Method is wrong.
req = httptest.NewRequest(http.MethodPost, endpoint, nil)
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
}
func TestMergeHandler(t *testing.T) {
config := conf.DefaultConfig()
srv := New(config)
endpoint := mergeEndpoint(config)
// should return 200.
body, contentType := test.MergeMultipartForm(t, nil)
req := httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
req := httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// should return 405 as Method is wrong.
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
// should return 415 as Content-Type is wrong.
body, _ = test.MergeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, endpoint, body)
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
// should return 400 as "waitTimeout" form field
// value is < 0.
body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "waitTimeout" form field
// value is is > config.MaximumWaitTimeout().
body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "31"})
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "waitTimeout" form field
// value is invalid.
body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 504.
body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "0"})
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusGatewayTimeout, srv, req)
}
@@ -59,12 +71,19 @@ func TestMergeHandler(t *testing.T) {
func TestHTMLHandler(t *testing.T) {
config := conf.DefaultConfig()
srv := New(config)
endpoint := fmt.Sprintf("%s%s", convertGroupEndpoint, htmlEndpoint)
endpoint := htmlEndpoint(config)
// should return 200.
body, contentType := test.HTMLMultipartForm(t, nil)
req := httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// should return 405 as Method is wrong.
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
// should return 415 as Content-Type is wrong.
body, _ = test.HTMLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, endpoint, body)
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
// should return 400 as "waitTimeout" form field
// value is < 0.
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
@@ -184,17 +203,42 @@ func TestHTMLHandler(t *testing.T) {
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "googleChromeRpccBufferSize" form field
// value is < 0.
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "googleChromeRpccBufferSize" form field
// value is is > config.MaximumGoogleChromeRpccBufferSize().
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "104857601"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "googleChromeRpccBufferSize" form field
// value is invalid.
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "not an int"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
}
func TestURLHandler(t *testing.T) {
config := conf.DefaultConfig()
srv := New(config)
endpoint := fmt.Sprintf("%s%s", convertGroupEndpoint, urlEndpoint)
endpoint := urlEndpoint(config)
// should return 200.
body, contentType := test.URLMultipartForm(t, nil)
req := httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// should return 405 as Method is wrong.
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
// should return 415 as Content-Type is wrong.
body, _ = test.URLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, endpoint, body)
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
// should return 400 as "waitTimeout" form field
// value is < 0.
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
@@ -314,17 +358,42 @@ func TestURLHandler(t *testing.T) {
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "googleChromeRpccBufferSize" form field
// value is < 0.
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "googleChromeRpccBufferSize" form field
// value is is > config.MaximumGoogleChromeRpccBufferSize().
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "104857601"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "googleChromeRpccBufferSize" form field
// value is invalid.
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "not an int"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
}
func TestMarkdownHandler(t *testing.T) {
config := conf.DefaultConfig()
srv := New(config)
endpoint := fmt.Sprintf("%s%s", convertGroupEndpoint, markdownEndpoint)
endpoint := markdownEndpoint(config)
// should return 200.
body, contentType := test.MarkdownMultipartForm(t, nil)
req := httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// should return 405 as Method is wrong.
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
// should return 415 as Content-Type is wrong.
body, _ = test.MarkdownMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, endpoint, body)
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
// should return 400 as "waitTimeout" form field
// value is < 0.
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
@@ -444,17 +513,42 @@ func TestMarkdownHandler(t *testing.T) {
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "googleChromeRpccBufferSize" form field
// value is < 0.
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "googleChromeRpccBufferSize" form field
// value is is > config.MaximumGoogleChromeRpccBufferSize().
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "104857601"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "googleChromeRpccBufferSize" form field
// value is invalid.
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "not an int"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
}
func TestOfficeHandler(t *testing.T) {
config := conf.DefaultConfig()
srv := New(config)
endpoint := fmt.Sprintf("%s%s", convertGroupEndpoint, officeEndpoint)
endpoint := officeEndpoint(config)
// should return 200.
body, contentType := test.OfficeMultipartForm(t, nil)
req := httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// should return 405 as Method is wrong.
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
// should return 415 as Content-Type is wrong.
body, _ = test.OfficeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, endpoint, body)
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
// should return 400 as "waitTimeout" form field
// value is < 0.
body, contentType = test.OfficeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
@@ -487,11 +581,18 @@ func TestOfficeHandler(t *testing.T) {
}
func TestWebhook(t *testing.T) {
customHeaderRealKey := http.CanonicalHeaderKey("MyCustomHeader")
customHeaderKey := fmt.Sprintf("%s%s", resource.WebhookURLCustomHTTPHeaderCanonicalBaseKey, customHeaderRealKey)
customHeaderValue := "foo"
status := make(chan error, 2)
rcv := echo.New()
rcv.POST("/foo", func(c echo.Context) error {
if c.Request().Header.Get("Content-type") != "application/pdf" {
status <- fmt.Errorf("wrong Content-type: got %s want %s", c.Request().Header.Get("Content-type"), "application/pdf")
if c.Request().Header.Get(echo.HeaderContentType) != "application/pdf" {
status <- fmt.Errorf("wrong Content-type: got '%s' want '%s'", c.Request().Header.Get(echo.HeaderContentType), "application/pdf")
return nil
}
if c.Request().Header.Get(customHeaderRealKey) != customHeaderValue {
status <- fmt.Errorf("wrong '%s': got '%s' want '%s'", customHeaderRealKey, c.Request().Header.Get(customHeaderRealKey), customHeaderValue)
return nil
}
body, err := ioutil.ReadAll(c.Request().Body)
@@ -513,8 +614,9 @@ func TestWebhook(t *testing.T) {
srv := New(config)
// our custom server should receive the PDF.
body, contentType := test.MergeMultipartForm(t, map[string]string{string(resource.WebhookURLArgKey): "http://localhost:3001/foo"})
req := httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
req := httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
req.Header.Set(customHeaderKey, customHeaderValue)
test.AssertStatusCode(t, http.StatusOK, srv, req)
err := <-status
assert.NoError(t, err)
@@ -524,9 +626,9 @@ func TestResultFilename(t *testing.T) {
config := conf.DefaultConfig()
srv := New(config)
body, contentType := test.MergeMultipartForm(t, map[string]string{string(resource.ResultFilenameArgKey): "foo.pdf"})
req := httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
req := httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
rec := httptest.NewRecorder()
srv.ServeHTTP(rec, req)
assert.Equal(t, "attachment; filename=\"foo.pdf\"", rec.Header().Get("Content-Disposition"))
assert.Equal(t, "attachment; filename=\"foo.pdf\"", rec.Header().Get(echo.HeaderContentDisposition))
}

View File

@@ -2,6 +2,7 @@ package xhttp
import (
"net/http"
"strings"
"github.com/labstack/echo/v4"
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/context"
@@ -25,12 +26,28 @@ func contextMiddleware(config conf.Config) echo.MiddlewareFunc {
// extend the current echo context with our custom
// context.
ctx := context.New(c, logger, config)
// if its an healthcheck request, there
// is no need to create a Resource.
if ctx.Path() == pingEndpoint {
// if it's not a multipart/form-data request,
// there is no need to create a Resource.
if !isMultipartFormDataEndpoint(config, ctx.Path()) {
// validate method for healthcheck endpoint.
if ctx.Path() == pingEndpoint(config) && ctx.Request().Method != http.MethodGet {
err := doErr(ctx, echo.NewHTTPError(http.StatusMethodNotAllowed))
return ctx.LogRequestResult(err, false)
}
return next(ctx)
}
// if the endpoint is not for healthcheck, create a
// validate method.
if ctx.Request().Method != http.MethodPost {
err := doErr(ctx, echo.NewHTTPError(http.StatusMethodNotAllowed))
return ctx.LogRequestResult(err, false)
}
// validate Content-Type.
contentType := ctx.Request().Header.Get("Content-Type")
if !strings.Contains(contentType, "multipart/form-data") {
err := doErr(ctx, echo.NewHTTPError(http.StatusUnsupportedMediaType))
return ctx.LogRequestResult(err, false)
}
// it's a multipart/form-data request, create a
// Resource.
if err := ctx.WithResource(trace); err != nil {
err = doCleanup(ctx, err)
@@ -43,14 +60,14 @@ func contextMiddleware(config conf.Config) echo.MiddlewareFunc {
}
// loggerMiddleware logs the result of a request.
func loggerMiddleware() echo.MiddlewareFunc {
func loggerMiddleware(config conf.Config) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
ctx := context.MustCastFromEchoContext(c)
err := next(ctx)
// we do not want to log healthcheck requests if
// log level is not set to DEBUG.
isDebug := ctx.Path() == pingEndpoint
isDebug := ctx.Path() == pingEndpoint(config)
return ctx.LogRequestResult(err, isDebug)
}
}

View File

@@ -52,19 +52,25 @@ func chromePrinterOptions(r resource.Resource, config conf.Config) (printer.Chro
if err != nil {
return printer.ChromePrinterOptions{}, err
}
googleChromeRpccBufferSize, err := resource.GoogleChromeRpccBufferSizeArg(r, config)
if err != nil {
return printer.ChromePrinterOptions{}, err
}
return printer.ChromePrinterOptions{
WaitTimeout: waitTimeout,
WaitDelay: waitDelay,
HeaderHTML: headerHTML,
FooterHTML: footerHTML,
PaperWidth: paperWidth,
PaperHeight: paperHeight,
MarginTop: marginTop,
MarginBottom: marginBottom,
MarginLeft: marginLeft,
MarginRight: marginRight,
Landscape: landscape,
PageRanges: pageRanges,
WaitTimeout: waitTimeout,
WaitDelay: waitDelay,
HeaderHTML: headerHTML,
FooterHTML: footerHTML,
PaperWidth: paperWidth,
PaperHeight: paperHeight,
MarginTop: marginTop,
MarginBottom: marginBottom,
MarginLeft: marginLeft,
MarginRight: marginRight,
Landscape: landscape,
PageRanges: pageRanges,
RpccBufferSize: googleChromeRpccBufferSize,
CustomHTTPHeaders: make(map[string]string),
}, nil
}
opts, err := resolver()

View File

@@ -12,7 +12,6 @@ import (
"github.com/labstack/echo/v4"
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/internal/pkg/normalize"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
)
@@ -79,6 +78,10 @@ func (ctx *Context) WithResource(directoryName string) error {
if err != nil {
return r, err
}
// retrieve custom headers from request.
for key, value := range ctx.Request().Header {
r.WithCustomHTTPHeader(key, value[0])
}
// retrieve form values from request.
for _, key := range resource.ArgKeys() {
r.WithArg(key, ctx.FormValue(string(key)))
@@ -103,11 +106,7 @@ func (ctx *Context) WithResource(directoryName string) error {
return r, err
}
defer in.Close() // nolint: errcheck
filename, err := normalize.String(fh.Filename)
if err != nil {
return r, err
}
if err := r.WithFile(filename, in); err != nil {
if err := r.WithFile(fh.Filename, in); err != nil {
return r, err
}
}

View File

@@ -54,6 +54,9 @@ const (
// PageRangesArgKey is the key
// of the argument "pageRanges".
PageRangesArgKey ArgKey = "pageRanges"
// GoogleChromeRpccBufferSizeArgKey is the key
// of the argument "googleChromeRpccBufferSize".
GoogleChromeRpccBufferSizeArgKey ArgKey = "googleChromeRpccBufferSize"
)
/*
@@ -77,6 +80,7 @@ func ArgKeys() []ArgKey {
MarginRightArgKey,
LandscapeArgKey,
PageRangesArgKey,
GoogleChromeRpccBufferSizeArgKey,
}
}
@@ -269,3 +273,24 @@ func MarginArgs(r Resource, config conf.Config) (float64, float64, float64, floa
marginRight,
nil
}
/*
GoogleChromeRpccBufferSizeArg is a helper for retrieving
the "googleChromeRpccBufferSize" argument as int64.
It also validates it against the application
configuration.
*/
func GoogleChromeRpccBufferSizeArg(r Resource, config conf.Config) (int64, error) {
const op string = "resource.GoogleChromeRpccBufferSizeArg"
result, err := r.Int64Arg(
GoogleChromeRpccBufferSizeArgKey,
config.DefaultGoogleChromeRpccBufferSize(),
xassert.Int64NotInferiorTo(0.0),
xassert.Int64NotSuperiorTo(config.MaximumGoogleChromeRpccBufferSize()),
)
if err != nil {
return result, xerror.New(op, err)
}
return result, nil
}

View File

@@ -25,6 +25,7 @@ func TestArgKeys(t *testing.T) {
MarginRightArgKey,
LandscapeArgKey,
PageRangesArgKey,
GoogleChromeRpccBufferSizeArgKey,
}
assert.Equal(t, expected, ArgKeys())
}
@@ -301,3 +302,47 @@ func TestMarginArgs(t *testing.T) {
err = r.Close()
assert.Nil(t, err)
}
func TestGoogleChromeRpccBufferSizeArg(t *testing.T) {
const resourceDirectoryName string = "foo"
var expected int64
logger := test.DebugLogger()
config := conf.DefaultConfig()
r, err := New(logger, resourceDirectoryName)
assert.Nil(t, err)
// argument does not exist.
expected = config.DefaultGoogleChromeRpccBufferSize()
v, err := GoogleChromeRpccBufferSizeArg(r, config)
assert.Nil(t, err)
assert.Equal(t, expected, v)
// argument exist.
expected = 10
r.WithArg(GoogleChromeRpccBufferSizeArgKey, "10")
v, err = GoogleChromeRpccBufferSizeArg(r, config)
assert.Nil(t, err)
assert.Equal(t, expected, v)
// should not be OK as argument
// value is < 0.
expected = config.DefaultGoogleChromeRpccBufferSize()
r.WithArg(GoogleChromeRpccBufferSizeArgKey, "-1")
v, err = GoogleChromeRpccBufferSizeArg(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, v)
// should not be OK as argument
// value is > config.MaximumGoogleChromeRpccBufferSize().
expected = config.DefaultGoogleChromeRpccBufferSize()
r.WithArg(GoogleChromeRpccBufferSizeArgKey, "104857601")
v, err = GoogleChromeRpccBufferSizeArg(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, v)
// should not be OK as
// argument value is invalid.
expected = config.DefaultGoogleChromeRpccBufferSize()
r.WithArg(GoogleChromeRpccBufferSizeArgKey, "foo")
v, err = GoogleChromeRpccBufferSizeArg(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, v)
// finally...
err = r.Close()
assert.Nil(t, err)
}

View File

@@ -0,0 +1,37 @@
package resource
import (
"strings"
)
const (
// RemoteURLCustomHTTPHeaderCanonicalBaseKey is the base key
// of custom headers send to the remote URL.
RemoteURLCustomHTTPHeaderCanonicalBaseKey string = "Gotenberg-Remoteurl-"
// WebhookURLCustomHTTPHeaderCanonicalBaseKey is the base key
// of custom headers send to the webhook URL.
WebhookURLCustomHTTPHeaderCanonicalBaseKey string = "Gotenberg-Webhookurl-"
)
func fetchCustomHTTPHeaders(r Resource, baseKey string) map[string]string {
customHeaders := make(map[string]string)
for key, value := range r.customHeaders {
if strings.Contains(key, baseKey) {
realKey := strings.Replace(key, baseKey, "", 1)
customHeaders[realKey] = value
}
}
return customHeaders
}
// RemoteURLCustomHTTPHeaders is a helper for retrieving
// the custom headers for the URL conversion.
func RemoteURLCustomHTTPHeaders(r Resource) map[string]string {
return fetchCustomHTTPHeaders(r, RemoteURLCustomHTTPHeaderCanonicalBaseKey)
}
// WebhookURLCustomHTTPHeaders is a helper for retrieving
// the custom headers for the webhook URL.
func WebhookURLCustomHTTPHeaders(r Resource) map[string]string {
return fetchCustomHTTPHeaders(r, WebhookURLCustomHTTPHeaderCanonicalBaseKey)
}

View File

@@ -0,0 +1,54 @@
package resource
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/thecodingmachine/gotenberg/test"
)
func TestRemoteURLCustomHeaders(t *testing.T) {
const resourceDirectoryName string = "foo"
logger := test.DebugLogger()
r, err := New(logger, resourceDirectoryName)
assert.Nil(t, err)
// should find the custom header.
customHeaderValue := "bar"
customHeaderCanonicalRealKey := "Foo"
customHeaderCanonicalKey := http.CanonicalHeaderKey(fmt.Sprintf("%s%s", RemoteURLCustomHTTPHeaderCanonicalBaseKey, customHeaderCanonicalRealKey))
r.WithCustomHTTPHeader(customHeaderCanonicalKey, customHeaderValue)
r.WithCustomHTTPHeader("Bar", "Bar")
expected := map[string]string{
customHeaderCanonicalRealKey: customHeaderValue,
}
notExpected := map[string]string{
customHeaderCanonicalKey: customHeaderValue,
}
v := RemoteURLCustomHTTPHeaders(r)
assert.Equal(t, expected, v)
assert.NotEqual(t, notExpected, v)
}
func TestWebhookURLCustomHeaders(t *testing.T) {
const resourceDirectoryName string = "foo"
logger := test.DebugLogger()
r, err := New(logger, resourceDirectoryName)
assert.Nil(t, err)
// should find the custom header.
customHeaderValue := "bar"
customHeaderCanonicalRealKey := "Foo"
customHeaderCanonicalKey := http.CanonicalHeaderKey(fmt.Sprintf("%s%s", WebhookURLCustomHTTPHeaderCanonicalBaseKey, customHeaderCanonicalRealKey))
r.WithCustomHTTPHeader(customHeaderCanonicalKey, customHeaderValue)
r.WithCustomHTTPHeader("Bar", "Bar")
expected := map[string]string{
customHeaderCanonicalRealKey: customHeaderValue,
}
notExpected := map[string]string{
customHeaderCanonicalKey: customHeaderValue,
}
v := WebhookURLCustomHTTPHeaders(r)
assert.Equal(t, expected, v)
assert.NotEqual(t, notExpected, v)
}

View File

@@ -3,9 +3,12 @@ package resource
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/thecodingmachine/gotenberg/internal/pkg/normalize"
"github.com/thecodingmachine/gotenberg/internal/pkg/xassert"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
@@ -21,10 +24,11 @@ const TemporaryDirectory string = "tmp"
// Resource helps managing
// arguments and files for a conversion.
type Resource struct {
logger xlog.Logger
dirPath string
args map[ArgKey]string
files map[string]file
logger xlog.Logger
dirPath string
customHeaders map[string]string
args map[ArgKey]string
files map[string]file
}
// New creates a Resource where its files will
@@ -48,10 +52,11 @@ func New(logger xlog.Logger, directoryName string) (Resource, error) {
}
logger.DebugfOp(op, "resource directory '%s' created", directoryName)
return Resource{
logger: logger,
dirPath: dirPath,
args: make(map[ArgKey]string),
files: make(map[string]file),
logger: logger,
dirPath: dirPath,
customHeaders: make(map[string]string),
args: make(map[ArgKey]string),
files: make(map[string]file),
}, nil
}
@@ -70,6 +75,21 @@ func (r Resource) Close() error {
return nil
}
// WithCustomHTTPHeader add a new custom header to the Resource.
// Given key should be in canonical format.
func (r *Resource) WithCustomHTTPHeader(key string, value string) {
const op string = "resource.Resource.WithCustomHTTPHeader"
// should already be in canonical format.
canonicalKey := http.CanonicalHeaderKey(key)
if strings.Contains(canonicalKey, RemoteURLCustomHTTPHeaderCanonicalBaseKey) ||
strings.Contains(canonicalKey, WebhookURLCustomHTTPHeaderCanonicalBaseKey) {
r.customHeaders[canonicalKey] = value
r.logger.DebugfOp(op, "added '%s' with value '%s' to resource custom HTTP headers", canonicalKey, value)
return
}
r.logger.DebugfOp(op, "skipping '%s' as it is not a custom HTTP header...", canonicalKey)
}
// WithArg add a new argument to the Resource.
func (r *Resource) WithArg(key ArgKey, value string) {
const op string = "resource.Resource.WithArg"
@@ -80,13 +100,24 @@ func (r *Resource) WithArg(key ArgKey, value string) {
// WithFile add a new file to the Resource.
func (r *Resource) WithFile(filename string, in io.Reader) error {
const op string = "resource.Resource.WithFile"
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
file := file{fpath: fpath}
if err := file.write(in); err != nil {
resolver := func() error {
// see https://github.com/thecodingmachine/gotenberg/issues/104.
normalized, err := normalize.String(filename)
if err != nil {
return err
}
fpath := fmt.Sprintf("%s/%s", r.dirPath, normalized)
file := file{fpath: fpath}
if err := file.write(in); err != nil {
return err
}
r.files[filename] = file
r.logger.DebugfOp(op, "resource file '%s' created", filename)
return nil
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
r.files[filename] = file
r.logger.DebugfOp(op, "resource file '%s' created", filename)
return nil
}

View File

@@ -11,22 +11,21 @@ func New(config conf.Config) *echo.Echo {
srv.HideBanner = true
srv.HidePort = true
srv.Use(contextMiddleware(config))
srv.Use(loggerMiddleware())
srv.Use(loggerMiddleware(config))
srv.Use(cleanupMiddleware())
srv.Use(errorMiddleware())
srv.GET(pingEndpoint, pingHandler)
srv.POST(mergeEndpoint, mergeHandler)
srv.GET(pingEndpoint(config), pingHandler)
srv.POST(mergeEndpoint(config), mergeHandler)
if config.DisableGoogleChrome() && config.DisableUnoconv() {
return srv
}
g := srv.Group(convertGroupEndpoint)
if !config.DisableGoogleChrome() {
g.POST(htmlEndpoint, htmlHandler)
g.POST(urlEndpoint, urlHandler)
g.POST(markdownEndpoint, markdownHandler)
srv.POST(htmlEndpoint(config), htmlHandler)
srv.POST(urlEndpoint(config), urlHandler)
srv.POST(markdownEndpoint(config), markdownHandler)
}
if !config.DisableUnoconv() {
g.POST(officeEndpoint, officeHandler)
srv.POST(officeEndpoint(config), officeHandler)
}
return srv
}

View File

@@ -1,7 +1,6 @@
package xhttp
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
@@ -13,37 +12,46 @@ import (
"github.com/thecodingmachine/gotenberg/test"
)
func TestNonExistingEndpoint(t *testing.T) {
config, err := conf.FromEnv()
assert.Nil(t, err)
srv := New(config)
// "/" endpoint should return 404.
req := httptest.NewRequest(http.MethodGet, "/", nil)
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
}
func TestDisableChromeEndpoints(t *testing.T) {
os.Setenv(conf.DisableGoogleChromeEnvVar, "1")
config, err := conf.FromEnv()
assert.Nil(t, err)
srv := New(config)
// Ping endpoint should return 200.
req := httptest.NewRequest(http.MethodGet, pingEndpoint, nil)
req := httptest.NewRequest(http.MethodGet, pingEndpoint(config), nil)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// Merge endpoint should return 200.
body, contentType := test.MergeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
req = httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// HTML endpoint should return 404.
body, contentType = test.HTMLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, htmlEndpoint), body)
req = httptest.NewRequest(http.MethodPost, htmlEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
// URL endpoint should return 404.
body, contentType = test.URLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, urlEndpoint), body)
req = httptest.NewRequest(http.MethodPost, urlEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
// Markdown endpoint should return 404.
body, contentType = test.MarkdownMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, markdownEndpoint), body)
req = httptest.NewRequest(http.MethodPost, markdownEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
// Office endpoint should return 200.
body, contentType = test.OfficeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, officeEndpoint), body)
req = httptest.NewRequest(http.MethodPost, officeEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// finally...
@@ -56,31 +64,31 @@ func TestDisableUnoconvEndpoints(t *testing.T) {
assert.Nil(t, err)
srv := New(config)
// Ping endpoint should return 200.
req := httptest.NewRequest(http.MethodGet, pingEndpoint, nil)
req := httptest.NewRequest(http.MethodGet, pingEndpoint(config), nil)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// Merge endpoint should return 200.
body, contentType := test.MergeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
req = httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// HTML endpoint should return 200.
body, contentType = test.HTMLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, htmlEndpoint), body)
req = httptest.NewRequest(http.MethodPost, htmlEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// URL endpoint should return 200.
body, contentType = test.URLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, urlEndpoint), body)
req = httptest.NewRequest(http.MethodPost, urlEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// Markdown endpoint should return 404.
// Markdown endpoint should return 200.
body, contentType = test.MarkdownMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, markdownEndpoint), body)
req = httptest.NewRequest(http.MethodPost, markdownEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// Office endpoint should return 404.
body, contentType = test.OfficeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, officeEndpoint), body)
req = httptest.NewRequest(http.MethodPost, officeEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
// finally...
@@ -93,34 +101,71 @@ func TestDisableChromeAndUnoconvEndpoints(t *testing.T) {
assert.Nil(t, err)
srv := New(config)
// Ping endpoint should return 200.
req := httptest.NewRequest(http.MethodGet, pingEndpoint, nil)
req := httptest.NewRequest(http.MethodGet, pingEndpoint(config), nil)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// Merge endpoint should return 200.
body, contentType := test.MergeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
req = httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// HTML endpoint should return 404.
body, contentType = test.HTMLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, htmlEndpoint), body)
req = httptest.NewRequest(http.MethodPost, htmlEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
// URL endpoint should return 404.
body, contentType = test.URLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, urlEndpoint), body)
req = httptest.NewRequest(http.MethodPost, urlEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
// Markdown endpoint should return 404.
body, contentType = test.MarkdownMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, markdownEndpoint), body)
req = httptest.NewRequest(http.MethodPost, markdownEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
// Office endpoint should return 404.
body, contentType = test.OfficeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, officeEndpoint), body)
req = httptest.NewRequest(http.MethodPost, officeEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
// finally...
os.Setenv(conf.DisableGoogleChromeEnvVar, "0")
os.Setenv(conf.DisableUnoconvEnvVar, "0")
}
func TestCustomRootPath(t *testing.T) {
os.Setenv(conf.RootPathEnvVar, "/foo/")
config, err := conf.FromEnv()
assert.Nil(t, err)
srv := New(config)
// Ping endpoint should return 200.
req := httptest.NewRequest(http.MethodGet, pingEndpoint(config), nil)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// Merge endpoint should return 200.
body, contentType := test.MergeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// HTML endpoint should return 200.
body, contentType = test.HTMLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, htmlEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// URL endpoint should return 200.
body, contentType = test.URLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, urlEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// Markdown endpoint should return 200.
body, contentType = test.MarkdownMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, markdownEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// Office endpoint should return 200.
body, contentType = test.OfficeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, officeEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// finally...
os.Setenv(conf.RootPathEnvVar, "/")
}

View File

@@ -34,35 +34,47 @@ const (
// LogLevelEnvVar contains the name
// of the environment variable "LOG_LEVEL".
LogLevelEnvVar string = "LOG_LEVEL"
// RootPathEnvVar contains the name
// of the environment variable "ROOT_PATH".
RootPathEnvVar string = "ROOT_PATH"
// DefaultGoogleChromeRpccBufferSizeEnvVar contains the name
// of the environment variable "DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE".
DefaultGoogleChromeRpccBufferSizeEnvVar string = "DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE"
)
// Config contains the application
// configuration.
type Config struct {
maximumWaitTimeout float64
maximumWaitDelay float64
maximumWebhookURLTimeout float64
defaultWaitTimeout float64
defaultWebhookURLTimeout float64
defaultListenPort int64
disableGoogleChrome bool
disableUnoconv bool
logLevel xlog.Level
maximumWaitTimeout float64
maximumWaitDelay float64
maximumWebhookURLTimeout float64
defaultWaitTimeout float64
defaultWebhookURLTimeout float64
defaultListenPort int64
disableGoogleChrome bool
disableUnoconv bool
logLevel xlog.Level
rootPath string
maximumGoogleChromeRpccBufferSize int64
defaultGoogleChromeRpccBufferSize int64
}
// DefaultConfig returns the default
// configuration.
func DefaultConfig() Config {
return Config{
maximumWaitTimeout: 30.0,
maximumWaitDelay: 10.0,
maximumWebhookURLTimeout: 30.0,
defaultWaitTimeout: 10.0,
defaultWebhookURLTimeout: 10.0,
defaultListenPort: 3000,
disableGoogleChrome: false,
disableUnoconv: false,
logLevel: xlog.InfoLevel,
maximumWaitTimeout: 30.0,
maximumWaitDelay: 10.0,
maximumWebhookURLTimeout: 30.0,
defaultWaitTimeout: 10.0,
defaultWebhookURLTimeout: 10.0,
defaultListenPort: 3000,
disableGoogleChrome: false,
disableUnoconv: false,
logLevel: xlog.InfoLevel,
rootPath: "/",
maximumGoogleChromeRpccBufferSize: 104857600, // ~100 MB
defaultGoogleChromeRpccBufferSize: 1048576, // 1 MB
}
}
@@ -156,6 +168,26 @@ func FromEnv() (Config, error) {
if err != nil {
return c, err
}
rootPath, err := xassert.StringFromEnv(
RootPathEnvVar,
c.rootPath,
xassert.StringStartWith("/"),
xassert.StringEndWith("/"),
)
c.rootPath = rootPath
if err != nil {
return c, err
}
defaultGoogleChromeRpccBufferSize, err := xassert.Int64FromEnv(
DefaultGoogleChromeRpccBufferSizeEnvVar,
c.defaultGoogleChromeRpccBufferSize,
xassert.Int64NotInferiorTo(0),
xassert.Int64NotSuperiorTo(c.MaximumGoogleChromeRpccBufferSize()),
)
c.defaultGoogleChromeRpccBufferSize = defaultGoogleChromeRpccBufferSize
if err != nil {
return c, err
}
return c, nil
}
result, err := resolver()
@@ -224,3 +256,21 @@ func (c Config) DisableUnoconv() bool {
func (c Config) LogLevel() xlog.Level {
return c.logLevel
}
// RootPath returns the rooth path from
// the configuration.
func (c Config) RootPath() string {
return c.rootPath
}
// MaximumGoogleChromeRpccBufferSize returns the maximum
// Google Chrome rpcc buffer size from the configuration.
func (c Config) MaximumGoogleChromeRpccBufferSize() int64 {
return c.maximumGoogleChromeRpccBufferSize
}
// DefaultGoogleChromeRpccBufferSize returns the default
// Google Chrome rpcc buffer size from the configuration.
func (c Config) DefaultGoogleChromeRpccBufferSize() int64 {
return c.defaultGoogleChromeRpccBufferSize
}

View File

@@ -320,6 +320,66 @@ func TestLogLevelFromEnv(t *testing.T) {
os.Unsetenv(LogLevelEnvVar)
}
func TestRootPathFromEnv(t *testing.T) {
var (
expected Config
result Config
err error
)
// ROOT_PATH correctly set.
os.Setenv(RootPathEnvVar, "/foo/")
expected = DefaultConfig()
expected.rootPath = "/foo/"
result, err = FromEnv()
assert.Nil(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(RootPathEnvVar)
// ROOT_PATH wrongly set.
os.Setenv(RootPathEnvVar, "foo")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(RootPathEnvVar)
}
func TestDefaultGoogleChromeRpccBufferSizeFromEnv(t *testing.T) {
var (
expected Config
result Config
err error
)
// DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE correctly set.
os.Setenv(DefaultGoogleChromeRpccBufferSizeEnvVar, "100")
expected = DefaultConfig()
expected.defaultGoogleChromeRpccBufferSize = 100
result, err = FromEnv()
assert.Nil(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(DefaultGoogleChromeRpccBufferSizeEnvVar)
// DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE wrongly set.
os.Setenv(DefaultGoogleChromeRpccBufferSizeEnvVar, "foo")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(DefaultGoogleChromeRpccBufferSizeEnvVar)
// DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE < 0.
os.Setenv(DefaultGoogleChromeRpccBufferSizeEnvVar, "-1")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(DefaultGoogleChromeRpccBufferSizeEnvVar)
// DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE > 100 MB (maximumGoogleChromeRpccBufferSize).
os.Setenv(DefaultGoogleChromeRpccBufferSizeEnvVar, "104857601")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(DefaultGoogleChromeRpccBufferSizeEnvVar)
}
func TestGetters(t *testing.T) {
result := DefaultConfig()
assert.Equal(t, result.maximumWaitTimeout, result.MaximumWaitTimeout())
@@ -331,4 +391,7 @@ func TestGetters(t *testing.T) {
assert.Equal(t, result.disableGoogleChrome, result.DisableGoogleChrome())
assert.Equal(t, result.disableUnoconv, result.DisableUnoconv())
assert.Equal(t, result.logLevel, result.LogLevel())
assert.Equal(t, result.rootPath, result.RootPath())
assert.Equal(t, result.maximumGoogleChromeRpccBufferSize, result.MaximumGoogleChromeRpccBufferSize())
assert.Equal(t, result.defaultGoogleChromeRpccBufferSize, result.DefaultGoogleChromeRpccBufferSize())
}

View File

@@ -2,6 +2,7 @@ package printer
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"strings"
@@ -30,18 +31,20 @@ type chromePrinter struct {
// ChromePrinterOptions helps customizing the
// Google Chrome Printer behaviour.
type ChromePrinterOptions struct {
WaitTimeout float64
WaitDelay float64
HeaderHTML string
FooterHTML string
PaperWidth float64
PaperHeight float64
MarginTop float64
MarginBottom float64
MarginLeft float64
MarginRight float64
Landscape bool
PageRanges string
WaitTimeout float64
WaitDelay float64
HeaderHTML string
FooterHTML string
PaperWidth float64
PaperHeight float64
MarginTop float64
MarginBottom float64
MarginLeft float64
MarginRight float64
Landscape bool
PageRanges string
RpccBufferSize int64
CustomHTTPHeaders map[string]string
}
// DefaultChromePrinterOptions returns the default
@@ -49,18 +52,20 @@ type ChromePrinterOptions struct {
func DefaultChromePrinterOptions(config conf.Config) ChromePrinterOptions {
const defaultHeaderFooterHTML string = "<html><head></head><body></body></html>"
return ChromePrinterOptions{
WaitTimeout: config.DefaultWaitTimeout(),
WaitDelay: 0.0,
HeaderHTML: defaultHeaderFooterHTML,
FooterHTML: defaultHeaderFooterHTML,
PaperWidth: 8.27,
PaperHeight: 11.7,
MarginTop: 1.0,
MarginBottom: 1.0,
MarginLeft: 1.0,
MarginRight: 1.0,
Landscape: false,
PageRanges: "",
WaitTimeout: config.DefaultWaitTimeout(),
WaitDelay: 0.0,
HeaderHTML: defaultHeaderFooterHTML,
FooterHTML: defaultHeaderFooterHTML,
PaperWidth: 8.27,
PaperHeight: 11.7,
MarginTop: 1.0,
MarginBottom: 1.0,
MarginLeft: 1.0,
MarginRight: 1.0,
Landscape: false,
PageRanges: "",
RpccBufferSize: config.DefaultGoogleChromeRpccBufferSize(),
CustomHTTPHeaders: make(map[string]string),
}
}
@@ -113,7 +118,18 @@ func (p chromePrinter) Print(destination string) error {
}
// connect the client to the new target.
newTargetWsURL := fmt.Sprintf("ws://127.0.0.1:9222/devtools/page/%s", newTarget.TargetID)
newContextConn, err := rpcc.DialContext(ctx, newTargetWsURL)
newContextConn, err := rpcc.DialContext(
ctx,
newTargetWsURL,
/*
see:
https://github.com/thecodingmachine/gotenberg/issues/108
https://github.com/mafredri/cdp/issues/4
https://github.com/ChromeDevTools/devtools-protocol/issues/24
*/
rpcc.WithWriteBufferSize(int(p.opts.RpccBufferSize)),
rpcc.WithCompression(),
)
if err != nil {
return err
}
@@ -133,6 +149,10 @@ func (p chromePrinter) Print(destination string) error {
if err := p.enableEvents(ctx, targetClient); err != nil {
return err
}
// add custom headers (if any).
if err := p.setCustomHTTPHeaders(ctx, targetClient); err != nil {
return err
}
// listen for all events.
if err := p.listenEvents(ctx, targetClient); err != nil {
return err
@@ -166,7 +186,7 @@ func (p chromePrinter) Print(destination string) error {
printToPdfArgs,
)
if err != nil {
// TODO: find a way to check it in the handlers.
// find a way to check it in the handlers?
if strings.Contains(err.Error(), "Page range syntax error") {
return xerror.Invalid(
op,
@@ -174,6 +194,16 @@ func (p chromePrinter) Print(destination string) error {
err,
)
}
if strings.Contains(err.Error(), "rpcc: message too large") {
return xerror.Invalid(
op,
fmt.Sprintf(
"'%d' bytes are not enough: increase the Google Chrome rpcc buffer size (up to 100 MB)",
p.opts.RpccBufferSize,
),
err,
)
}
return err
}
if err := ioutil.WriteFile(destination, print.Data, 0644); err != nil {
@@ -238,6 +268,32 @@ func (p chromePrinter) enableEvents(ctx context.Context, client *cdp.Client) err
return nil
}
func (p chromePrinter) setCustomHTTPHeaders(ctx context.Context, client *cdp.Client) error {
const op string = "printer.chromePrinter.setCustomHTTPHeaders"
resolver := func() error {
if len(p.opts.CustomHTTPHeaders) == 0 {
p.logger.DebugOp(op, "skipping custom HTTP headers as none have been provided...")
return nil
}
customHTTPHeaders := make(map[string]string)
// useless but for the logs.
for key, value := range p.opts.CustomHTTPHeaders {
customHTTPHeaders[key] = value
p.logger.DebugfOp(op, "set '%s' to custom HTTP header '%s'", value, key)
}
b, err := json.Marshal(customHTTPHeaders)
if err != nil {
return err
}
// should always be called after client.Network.Enable.
return client.Network.SetExtraHTTPHeaders(ctx, network.NewSetExtraHTTPHeadersArgs(b))
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
func (p chromePrinter) listenEvents(ctx context.Context, client *cdp.Client) error {
const op string = "printer.chromePrinter.listenEvents"
resolver := func() error {

View File

@@ -2,6 +2,7 @@ package printer
import (
"context"
"sort"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/internal/pkg/xcontext"
@@ -54,6 +55,8 @@ func (p mergePrinter) Print(destination string) error {
defer cancel()
p.ctx = ctx
}
// see https://github.com/thecodingmachine/gotenberg/issues/139.
sort.Strings(p.fpaths)
p.logger.DebugfOp(op, "merging '%v'...", p.fpaths)
resolver := func() error {
var args []string

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"github.com/phayes/freeport"
@@ -56,6 +57,8 @@ func (p officePrinter) Print(destination string) error {
ctx, cancel := xcontext.WithTimeout(p.logger, p.opts.WaitTimeout)
defer cancel()
resolver := func() error {
// see https://github.com/thecodingmachine/gotenberg/issues/139.
sort.Strings(p.fpaths)
fpaths := make([]string, len(p.fpaths))
dirPath := filepath.Dir(destination)
for i, fpath := range p.fpaths {
@@ -111,7 +114,7 @@ func (p officePrinter) unoconv(ctx context.Context, fpath, destination string) e
}
args = append(args, "--output", destination, fpath)
if err := xexec.Run(ctx, p.logger, "unoconv", args...); err != nil {
// TODO: find a way to check it in the handlers.
// find a way to check it in the handlers?
if p.opts.PageRanges != "" && strings.Contains(err.Error(), "exit status 5") {
return xerror.Invalid(
op,

View File

@@ -2,6 +2,7 @@ package xassert
import (
"fmt"
"strings"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
)
@@ -54,7 +55,67 @@ func StringOneOf(values []string) RuleString {
}
}
type ruleStringStartWith struct {
*baseRuleString
startWith string
}
func (r ruleStringStartWith) validate() error {
const op string = "xassert.ruleStringStartWith.validate"
if strings.HasPrefix(r.value, r.startWith) {
return nil
}
return xerror.Invalid(
op,
fmt.Sprintf("'%s' should start with '%s', got '%s'", r.key, r.startWith, r.value),
nil,
)
}
/*
StringStartWith returns a RuleString for
validating that a string starts with
given string.
*/
func StringStartWith(startWith string) RuleString {
return ruleStringStartWith{
&baseRuleString{},
startWith,
}
}
type ruleStringEndWith struct {
*baseRuleString
endWith string
}
func (r ruleStringEndWith) validate() error {
const op string = "xassert.ruleStringEndWith.validate"
if strings.HasSuffix(r.value, r.endWith) {
return nil
}
return xerror.Invalid(
op,
fmt.Sprintf("'%s' should end with '%s', got '%s'", r.key, r.endWith, r.value),
nil,
)
}
/*
StringEndWith returns a RuleString for
validating that a string ends with
given string.
*/
func StringEndWith(endWith string) RuleString {
return ruleStringEndWith{
&baseRuleString{},
endWith,
}
}
// Compile-time checks to ensure type implements desired interfaces.
var (
_ = RuleString(new(ruleStringOneOf))
_ = RuleString(new(ruleStringStartWith))
_ = RuleString(new(ruleStringEndWith))
)

View File

@@ -18,3 +18,27 @@ func TestStringOfOne(t *testing.T) {
err = rule.validate()
test.AssertError(t, err)
}
func TestStringStartWith(t *testing.T) {
rule := StringStartWith("foo")
// should be OK.
rule.with("FOO", "foobarfoo")
err := rule.validate()
assert.Nil(t, err)
// should not be OK.
rule.with("FOO", "qux")
err = rule.validate()
test.AssertError(t, err)
}
func TestStringEndWith(t *testing.T) {
rule := StringEndWith("foo")
// should be OK.
rule.with("FOO", "foobarfoo")
err := rule.validate()
assert.Nil(t, err)
// should not be OK.
rule.with("FOO", "qux")
err = rule.validate()
test.AssertError(t, err)
}