diff --git a/build/docs/content/02-clients.md b/build/docs/content/02-clients.md index 560601e6..39bcd0cc 100644 --- a/build/docs/content/02-clients.md +++ b/build/docs/content/02-clients.md @@ -18,7 +18,7 @@ Unless your project already has a PSR7 `HttpClient`, install `php-http/guzzle6-a $ composer require php-http/guzzle6-adapter ``` -Then the PHP client: +Then the [PHP client](https://github.com/thecodingmachine/gotenberg-php-client): ```bash $ composer require thecodingmachine/gotenberg-php-client diff --git a/build/docs/content/04-html.md b/build/docs/content/04-html.md index 060da6ef..00a5fb8c 100644 --- a/build/docs/content/04-html.md +++ b/build/docs/content/04-html.md @@ -59,7 +59,7 @@ use TheCodingMachine\Gotenberg\HTMLRequest; $client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); $index = DocumentFactory::makeFromPath('index.html', 'index.html'); $request = new HTMLRequest($index); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); ``` @@ -146,7 +146,7 @@ $footer = DocumentFactory::makeFromPath('footer.html', 'footer.html'); $request = new HTMLRequest($index); $request->setHeader($header); $request->setFooter($footer); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); ``` @@ -237,7 +237,7 @@ $assets = [ ]; $request = new HTMLRequest($index); $request->setAssets($assets); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); ``` @@ -296,7 +296,7 @@ $request = new HTMLRequest($index); $request->setPaperSize(Request::A4); $request->setMargins(Request::NO_MARGINS); $request->setLandscape(true); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); ``` @@ -345,7 +345,7 @@ $client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client() $index = DocumentFactory::makeFromPath('index.html', 'index.html'); $request = new HTMLRequest($index); $request->setWaitDelay(5.5); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); ``` @@ -397,6 +397,6 @@ $client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client() $index = DocumentFactory::makeFromPath('index.html', 'index.html'); $request = new HTMLRequest($index); $request->setGoogleChromeRpccBufferSize(1048576); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); ``` diff --git a/build/docs/content/05-url.md b/build/docs/content/05-url.md index 6517843a..16c42b83 100644 --- a/build/docs/content/05-url.md +++ b/build/docs/content/05-url.md @@ -51,6 +51,57 @@ use TheCodingMachine\Gotenberg\URLRequest; $client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); $request = new URLRequest('https://google.com'); $request->setMargins(Request::NO_MARGINS); -$dest = "result.pdf"; +$dest = 'result.pdf'; +$client->store($request, $dest); +``` + +## Custom HTTP headers + +You may send your own HTTP headers to the `remoteURL`. + +For instance, by adding the HTTP header `Gotenberg-Remoteurl-Your-Header` to your request, +the API will send a request to the `remoteURL` with the HTTP header `Your-Header`. + +> **Attention:** the API uses a canonical format for the HTTP headers: +> it transforms the first +> letter and any letter following a hyphen to upper case; +> the rest are converted to lowercase. For example, the +> canonical key for `accept-encoding` is `Accept-Encoding`. + +### cURL + +```bash +$ curl --request POST \ + --url http://localhost:3000/convert/url \ + --header 'Content-Type: multipart/form-data' \ + --header 'Gotenberg-Remoteurl-Your-Header: Foo' \ + --form remoteURL=https://google.com \ + -o result.pdf +``` + +### Go + +```golang +import "github.com/thecodingmachine/gotenberg-go-client/v6" + +func main() { + c := &gotenberg.Client{Hostname: "http://localhost:3000"} + req := gotenberg.NewURLRequest("https://google.com") + req.AddRemoteURLHTTPHeader("Your-Header", "Foo") + dest := "result.pdf" + c.Store(req, dest) +} +``` + +### PHP + +```php +use TheCodingMachine\Gotenberg\Client; +use TheCodingMachine\Gotenberg\URLRequest; + +$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); +$request = new URLRequest('https://google.com'); +$request->addRemoteURLHTTPHeader('Your-Header', 'Foo') +$dest = 'result.pdf'; $client->store($request, $dest); ``` diff --git a/build/docs/content/06-markdown.md b/build/docs/content/06-markdown.md index 66c50ee4..65b71702 100644 --- a/build/docs/content/06-markdown.md +++ b/build/docs/content/06-markdown.md @@ -65,6 +65,6 @@ $markdowns = [ DocumentFactory::makeFromPath('file.md', 'file.md'), ]; $request = new MarkdownRequest($index, $markdowns); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); ``` diff --git a/build/docs/content/07-office.md b/build/docs/content/07-office.md index 39e91e37..70a1e2fa 100644 --- a/build/docs/content/07-office.md +++ b/build/docs/content/07-office.md @@ -64,7 +64,7 @@ $files = [ DocumentFactory::makeFromPath('document2.docx', 'document2.docx'), ]; $request = new OfficeRequest($files); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); ``` @@ -112,6 +112,6 @@ $files = [ ]; $request = new OfficeRequest($files); $request->setLandscape(true); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); ``` diff --git a/build/docs/content/08-merge.md b/build/docs/content/08-merge.md index 49ac0b2b..fc0d50ba 100644 --- a/build/docs/content/08-merge.md +++ b/build/docs/content/08-merge.md @@ -50,6 +50,6 @@ $files = [ DocumentFactory::makeFromPath('file2.pdf', 'file2.pdf'), ]; $request = new MergeRequest($files); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); ``` diff --git a/build/docs/content/09-timeout.md b/build/docs/content/09-timeout.md index 9392b84f..7c644cec 100644 --- a/build/docs/content/09-timeout.md +++ b/build/docs/content/09-timeout.md @@ -48,6 +48,6 @@ $client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client() $index = DocumentFactory::makeFromPath('index.html', 'index.html'); $request = new HTMLRequest($index); $request->setWaitTimeout(2.5); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); ``` diff --git a/build/docs/content/10-webhook.md b/build/docs/content/10-webhook.md index fff2456d..06c97f24 100644 --- a/build/docs/content/10-webhook.md +++ b/build/docs/content/10-webhook.md @@ -58,9 +58,7 @@ It takes a float as value (e.g `2.5` for 2.5 seconds). > You may also define this value globally: see the [environment variables](#environment_variables.default_webhook_url_timeout) section. -### Examples - -#### cURL +### cURL ```bash $ curl --request POST \ @@ -71,7 +69,7 @@ $ curl --request POST \ --form webhookURLTimeout=2.5 ``` -#### Go +### Go ```golang import "github.com/thecodingmachine/gotenberg-go-client/v6" @@ -85,7 +83,7 @@ func main() { } ``` -#### PHP +### PHP ```php use TheCodingMachine\Gotenberg\Client; @@ -99,3 +97,56 @@ $request->setWebhookURL('http://myapp.com/webhook/'); $request->setWebhookURLTimeout(2.5); $resp = $client->post($request); ``` + +## Custom HTTP headers + +You may send your own HTTP headers to the `webhookURL`. + +For instance, by adding the HTTP header `Gotenberg-Webhookurl-Your-Header` to your request, +the API will send a request to the `webhookURL` with the HTTP header `Your-Header`. + +> **Attention:** the API uses a canonical format for the HTTP headers: +> it transforms the first +> letter and any letter following a hyphen to upper case; +> the rest are converted to lowercase. For example, the +> canonical key for `accept-encoding` is `Accept-Encoding`. + +### cURL + +```bash +$ curl --request POST \ + --url http://localhost:3000/convert/html \ + --header 'Content-Type: multipart/form-data' \ + --header 'Gotenberg-Webhookurl-Your-Header: Foo' \ + --form files=@index.html \ + --form webhookURL='http://myapp.com/webhook/' +``` + +### Go + +```golang +import "github.com/thecodingmachine/gotenberg-go-client/v6" + +func main() { + c := &gotenberg.Client{Hostname: "http://localhost:3000"} + req, _ := gotenberg.NewHTMLRequest("index.html") + req.WebhookURL("http://myapp.com/webhook/") + req.AddWebhookURLHTTPHeader("Your-Header", "Foo") + resp, _ := c.Post(req) +} +``` + +### PHP + +```php +use TheCodingMachine\Gotenberg\Client; +use TheCodingMachine\Gotenberg\DocumentFactory; +use TheCodingMachine\Gotenberg\HTMLRequest; + +$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); +$index = DocumentFactory::makeFromPath('index.html', 'index.html'); +$request = new HTMLRequest($index); +$request->setWebhookURL('http://myapp.com/webhook/'); +$request->addWebhookURLHTTPHeader('Your-Header', 'Foo'); +$resp = $client->post($request); +``` \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index 4d130685..95af3d47 100755 --- a/docs/index.html +++ b/docs/index.html @@ -233,7 +233,7 @@ Gotenberg API is available at http://localhost:3
$ composer require php-http/guzzle6-adapter
 
-

Then the PHP client:

+

Then the PHP client:

$ composer require thecodingmachine/gotenberg-php-client
 
@@ -474,7 +474,7 @@ use TheCodingMachine\Gotenberg\HTMLRequest; $client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); $index = DocumentFactory::makeFromPath('index.html', 'index.html'); $request = new HTMLRequest($index); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); @@ -569,7 +569,7 @@ $footer = DocumentFactory::makeFromPath('footer.html', 'footer.html& $request = new HTMLRequest($index); $request->setHeader($header); $request->setFooter($footer); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); @@ -665,7 +665,7 @@ $assets = [ ]; $request = new HTMLRequest($index); $request->setAssets($assets); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); @@ -731,7 +731,7 @@ $request = new HTMLRequest($index); $request->setPaperSize(Request::A4); $request->setMargins(Request::NO_MARGINS); $request->setLandscape(true); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); @@ -787,7 +787,7 @@ $client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\ $index = DocumentFactory::makeFromPath('index.html', 'index.html'); $request = new HTMLRequest($index); $request->setWaitDelay(5.5); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); @@ -846,7 +846,7 @@ $client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\ $index = DocumentFactory::makeFromPath('index.html', 'index.html'); $request = new HTMLRequest($index); $request->setGoogleChromeRpccBufferSize(1048576); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); @@ -912,7 +912,65 @@ use TheCodingMachine\Gotenberg\URLRequest; $client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); $request = new URLRequest('https://google.com'); $request->setMargins(Request::NO_MARGINS); -$dest = "result.pdf"; +$dest = 'result.pdf'; +$client->store($request, $dest); + + +

Custom HTTP headers

+ +

You may send your own HTTP headers to the remoteURL.

+ +

For instance, by adding the HTTP header Gotenberg-Remoteurl-Your-Header to your request, +the API will send a request to the remoteURL with the HTTP header Your-Header.

+ +
+

Attention: the API uses a canonical format for the HTTP headers: +it transforms the first +letter and any letter following a hyphen to upper case; +the rest are converted to lowercase. For example, the +canonical key for accept-encoding is Accept-Encoding.

+
+ +

cURL

+ +
$ curl --request POST \
+    --url http://localhost:3000/convert/url \
+    --header 'Content-Type: multipart/form-data' \
+    --header 'Gotenberg-Remoteurl-Your-Header: Foo' \
+    --form remoteURL=https://google.com \
+    -o result.pdf
+
+ +

Go

+ +
import "github.com/thecodingmachine/gotenberg-go-client/v6"
+
+func main() {
+    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
+    req := gotenberg.NewURLRequest("https://google.com")
+    req.AddRemoteURLHTTPHeader("Your-Header", "Foo")
+    dest := "result.pdf"
+    c.Store(req, dest)
+}
+
+ +

PHP

+ +
use TheCodingMachine\Gotenberg\Client;
+use TheCodingMachine\Gotenberg\URLRequest;
+
+$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
+$request = new URLRequest('https://google.com');
+$request->addRemoteURLHTTPHeader('Your-Header', 'Foo')
+$dest = 'result.pdf';
 $client->store($request, $dest);
 
@@ -989,7 +1047,7 @@ $markdowns = [ DocumentFactory::makeFromPath('file.md', 'file.md'), ]; $request = new MarkdownRequest($index, $markdowns); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); @@ -1070,7 +1128,7 @@ $files = [ DocumentFactory::makeFromPath('document2.docx', 'document2.docx'), ]; $request = new OfficeRequest($files); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); @@ -1123,7 +1181,7 @@ $files = [ ]; $request = new OfficeRequest($files); $request->setLandscape(true); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); @@ -1188,7 +1246,7 @@ $files = [ DocumentFactory::makeFromPath('file2.pdf', 'file2.pdf'), ]; $request = new MergeRequest($files); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); @@ -1251,7 +1309,7 @@ $client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\ $index = DocumentFactory::makeFromPath('index.html', 'index.html'); $request = new HTMLRequest($index); $request->setWaitTimeout(2.5); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); @@ -1326,13 +1384,9 @@ $resp = $client->post($request);

You may also define this value globally: see the environment variables section.

-

Examples

- -

cURL

+cURL
$ curl --request POST \
     --url http://localhost:3000/convert/html \
@@ -1342,9 +1396,9 @@ $resp = $client->post($request);
     --form webhookURLTimeout=2.5
 
-

Go

+Go

import "github.com/thecodingmachine/gotenberg-go-client/v6"
 
@@ -1357,9 +1411,9 @@ $resp = $client->post($request);
 }
 
-

PHP

+PHP

use TheCodingMachine\Gotenberg\Client;
 use TheCodingMachine\Gotenberg\DocumentFactory;
@@ -1371,6 +1425,66 @@ $request = new HTMLRequest($index);
 $request->setWebhookURL('http://myapp.com/webhook/');
 $request->setWebhookURLTimeout(2.5);
 $resp = $client->post($request);
+
+ +

Custom HTTP headers

+ +

You may send your own HTTP headers to the webhookURL.

+ +

For instance, by adding the HTTP header Gotenberg-Webhookurl-Your-Header to your request, +the API will send a request to the webhookURL with the HTTP header Your-Header.

+ +
+

Attention: the API uses a canonical format for the HTTP headers: +it transforms the first +letter and any letter following a hyphen to upper case; +the rest are converted to lowercase. For example, the +canonical key for accept-encoding is Accept-Encoding.

+
+ +

cURL

+ +
$ curl --request POST \
+    --url http://localhost:3000/convert/html \
+    --header 'Content-Type: multipart/form-data' \
+    --header 'Gotenberg-Webhookurl-Your-Header: Foo' \
+    --form files=@index.html \
+    --form webhookURL='http://myapp.com/webhook/'
+
+ +

Go

+ +
import "github.com/thecodingmachine/gotenberg-go-client/v6"
+
+func main() {
+    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
+    req, _ := gotenberg.NewHTMLRequest("index.html")
+    req.WebhookURL("http://myapp.com/webhook/")
+    req.AddWebhookURLHTTPHeader("Your-Header", "Foo")
+    resp, _ := c.Post(req)
+}
+
+ +

PHP

+ +
use TheCodingMachine\Gotenberg\Client;
+use TheCodingMachine\Gotenberg\DocumentFactory;
+use TheCodingMachine\Gotenberg\HTMLRequest;
+
+$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
+$index = DocumentFactory::makeFromPath('index.html', 'index.html');
+$request = new HTMLRequest($index);
+$request->setWebhookURL('http://myapp.com/webhook/');
+$request->addWebhookURLHTTPHeader('Your-Header', 'Foo');
+$resp = $client->post($request);
 
diff --git a/internal/app/xhttp/handler.go b/internal/app/xhttp/handler.go index 0972a480..92cf22ea 100644 --- a/internal/app/xhttp/handler.go +++ b/internal/app/xhttp/handler.go @@ -138,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, @@ -322,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 } diff --git a/internal/app/xhttp/handler_test.go b/internal/app/xhttp/handler_test.go index 8b1f5ee7..7e141d53 100644 --- a/internal/app/xhttp/handler_test.go +++ b/internal/app/xhttp/handler_test.go @@ -581,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) @@ -609,6 +616,7 @@ func TestWebhook(t *testing.T) { body, contentType := test.MergeMultipartForm(t, map[string]string{string(resource.WebhookURLArgKey): "http://localhost:3001/foo"}) 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) @@ -622,5 +630,5 @@ func TestResultFilename(t *testing.T) { 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)) } diff --git a/internal/app/xhttp/pkg/context/context.go b/internal/app/xhttp/pkg/context/context.go index d1065b58..b8e0b892 100644 --- a/internal/app/xhttp/pkg/context/context.go +++ b/internal/app/xhttp/pkg/context/context.go @@ -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 } } diff --git a/internal/app/xhttp/pkg/resource/header.go b/internal/app/xhttp/pkg/resource/header.go new file mode 100644 index 00000000..fa4c8814 --- /dev/null +++ b/internal/app/xhttp/pkg/resource/header.go @@ -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) +} diff --git a/internal/app/xhttp/pkg/resource/header_test.go b/internal/app/xhttp/pkg/resource/header_test.go new file mode 100644 index 00000000..35a944f8 --- /dev/null +++ b/internal/app/xhttp/pkg/resource/header_test.go @@ -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) +} diff --git a/internal/app/xhttp/pkg/resource/resource.go b/internal/app/xhttp/pkg/resource/resource.go index 81cf001a..3a983f18 100644 --- a/internal/app/xhttp/pkg/resource/resource.go +++ b/internal/app/xhttp/pkg/resource/resource.go @@ -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 } diff --git a/internal/pkg/printer/chrome.go b/internal/pkg/printer/chrome.go index 8f1cc473..10337fab 100644 --- a/internal/pkg/printer/chrome.go +++ b/internal/pkg/printer/chrome.go @@ -2,6 +2,7 @@ package printer import ( "context" + "encoding/json" "fmt" "io/ioutil" "strings" @@ -30,18 +31,19 @@ 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 - RpccBufferSize int64 + WaitTimeout float64 + WaitDelay float64 + HeaderHTML string + FooterHTML string + PaperWidth float64 + PaperHeight float64 + MarginTop float64 + MarginBottom float64 + MarginLeft float64 + MarginRight float64 + Landscape bool + RpccBufferSize int64 + CustomHTTPHeaders map[string]string } // DefaultChromePrinterOptions returns the default @@ -49,18 +51,19 @@ type ChromePrinterOptions struct { func DefaultChromePrinterOptions(config conf.Config) ChromePrinterOptions { const defaultHeaderFooterHTML string = "" 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, - RpccBufferSize: config.DefaultGoogleChromeRpccBufferSize(), + 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, + RpccBufferSize: config.DefaultGoogleChromeRpccBufferSize(), + CustomHTTPHeaders: make(map[string]string), } } @@ -144,6 +147,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 @@ -247,6 +254,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 { diff --git a/test/multipartform.go b/test/multipartform.go index 0bc73508..00e8e552 100644 --- a/test/multipartform.go +++ b/test/multipartform.go @@ -79,7 +79,7 @@ func multipartForm( require.Nil(t, err) } if kind == "url" { - err := writer.WriteField("remoteURL", "http://google.com") + err := writer.WriteField("remoteURL", "https://google.com") require.Nil(t, err) } for k, v := range formValues { diff --git a/test/testdata.go b/test/testdata.go index e40ab818..fc72472b 100644 --- a/test/testdata.go +++ b/test/testdata.go @@ -75,6 +75,7 @@ func OfficeFpaths(t *testing.T) []string { fpath(t, "office", "document.docx"), fpath(t, "office", "document.rtf"), fpath(t, "office", "document.txt"), + fpath(t, "office", "document_with_special_éà.txt"), } } diff --git a/test/testdata/office/document_with_special_éà.txt b/test/testdata/office/document_with_special_éà.txt new file mode 100644 index 00000000..16eab9d7 --- /dev/null +++ b/test/testdata/office/document_with_special_éà.txt @@ -0,0 +1,3 @@ +Gutenberg + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. \ No newline at end of file