diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b758496e..e768c49b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -40,6 +40,6 @@ Fixes # - [ ] Have you lint your code locally prior to submission (`make lint`)? - [ ] Have you written new tests for your core changes, as applicable? - [ ] Have you successfully ran tests with your changes locally (`make tests`)? -- [ ] Have you updated the documentation (`make doc`)? +- [ ] Have you updated the documentation (Markdown files under `build > docs > content` and then `make doc`)? - [ ] I have squashed any insignificant commits - [ ] This change has comments for package types, values, functions, and non-obvious lines of code diff --git a/Makefile b/Makefile index 53885c12..5a0d93fb 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,10 @@ GOLANG_VERSION=1.13 VERSION=snapshot DOCKER_USER= DOCKER_PASSWORD= -DOCKER_REPOSITORY=thecodingmachine -GOLANGCI_LINT_VERSION=1.19.1 +DOCKER_REGISTRY=thecodingmachine +GOTENBERG_USER_GID=1001 +GOTENBERG_USER_UID=1001 +GOLANGCI_LINT_VERSION=1.20.1 CODE_COVERAGE=0 TINI_VERSION=0.18.0 MAXIMUM_WAIT_TIMEOUT=30.0 @@ -15,15 +17,17 @@ DEFAULT_LISTEN_PORT=3000 DISABLE_GOOGLE_CHROME=0 DISABLE_UNOCONV=0 LOG_LEVEL=INFO +ROOT_PATH=/ +DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE=1048576 # build the base Docker image. base: - docker build -t $(DOCKER_REPOSITORY)/gotenberg:base -f build/base/Dockerfile . + docker build --build-arg GOTENBERG_USER_GID=$(GOTENBERG_USER_GID) --build-arg GOTENBERG_USER_UID=$(GOTENBERG_USER_UID) -t $(DOCKER_REGISTRY)/gotenberg:base -f build/base/Dockerfile . # build the workspace Docker image. workspace: make base - docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t $(DOCKER_REPOSITORY)/gotenberg:workspace -f build/workspace/Dockerfile . + docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t $(DOCKER_REGISTRY)/gotenberg:workspace -f build/workspace/Dockerfile . # gofmt and goimports all go files. fmt: @@ -33,30 +37,30 @@ fmt: # run all linters. lint: make workspace - docker build --build-arg GOLANGCI_LINT_VERSION=$(GOLANGCI_LINT_VERSION) -t $(DOCKER_REPOSITORY)/gotenberg:lint -f build/lint/Dockerfile . - docker run --rm $(DOCKER_REPOSITORY)/gotenberg:lint + docker build --build-arg GOLANGCI_LINT_VERSION=$(GOLANGCI_LINT_VERSION) -t $(DOCKER_REGISTRY)/gotenberg:lint -f build/lint/Dockerfile . + docker run --rm $(DOCKER_REGISTRY)/gotenberg:lint # run all tests. tests: make workspace - ./scripts/tests.sh $(DOCKER_REPOSITORY) $(CODE_COVERAGE) + ./scripts/tests.sh $(DOCKER_REGISTRY) $(CODE_COVERAGE) # generate documentation. doc: make workspace - docker build -t $(DOCKER_REPOSITORY)/gotenberg:docs -f build/docs/Dockerfile . - docker run --rm -it -v "$(PWD):/gotenberg/docs" $(DOCKER_REPOSITORY)/gotenberg:docs + docker build -t $(DOCKER_REGISTRY)/gotenberg:docs -f build/docs/Dockerfile . + docker run --rm -it -v "$(PWD):/gotenberg/docs" $(DOCKER_REGISTRY)/gotenberg:docs # build Gotenberg Docker image. image: make workspace - docker build --build-arg VERSION=$(VERSION) --build-arg TINI_VERSION=$(TINI_VERSION) -t $(DOCKER_REPOSITORY)/gotenberg:$(VERSION) -f build/package/Dockerfile . + docker build --build-arg VERSION=$(VERSION) --build-arg TINI_VERSION=$(TINI_VERSION) -t $(DOCKER_REGISTRY)/gotenberg:$(VERSION) -f build/package/Dockerfile . # start the API using previously built Docker image. gotenberg: - docker run -it --rm -e MAXIMUM_WAIT_TIMEOUT=$(MAXIMUM_WAIT_TIMEOUT) -e MAXIMUM_WAIT_DELAY=$(MAXIMUM_WAIT_DELAY) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_WEBHOOK_URL_TIMEOUT=$(DEFAULT_WEBHOOK_URL_TIMEOUT) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_LISTEN_PORT=$(DEFAULT_LISTEN_PORT) -e DISABLE_GOOGLE_CHROME=$(DISABLE_GOOGLE_CHROME) -e DISABLE_UNOCONV=$(DISABLE_UNOCONV) -e LOG_LEVEL=$(LOG_LEVEL) -p "$(DEFAULT_LISTEN_PORT):$(DEFAULT_LISTEN_PORT)" $(DOCKER_REPOSITORY)/gotenberg:$(VERSION) + docker run -it --rm -e MAXIMUM_WAIT_TIMEOUT=$(MAXIMUM_WAIT_TIMEOUT) -e MAXIMUM_WAIT_DELAY=$(MAXIMUM_WAIT_DELAY) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_WEBHOOK_URL_TIMEOUT=$(DEFAULT_WEBHOOK_URL_TIMEOUT) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_LISTEN_PORT=$(DEFAULT_LISTEN_PORT) -e DISABLE_GOOGLE_CHROME=$(DISABLE_GOOGLE_CHROME) -e DISABLE_UNOCONV=$(DISABLE_UNOCONV) -e LOG_LEVEL=$(LOG_LEVEL) -e ROOT_PATH=$(ROOT_PATH) -e DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE=$(DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE) -p "$(DEFAULT_LISTEN_PORT):$(DEFAULT_LISTEN_PORT)" $(DOCKER_REPOSITORY)/gotenberg:$(VERSION) # publish Gotenberg images according to version. publish: make workspace - ./scripts/publish.sh $(GOLANG_VERSION) $(TINI_VERSION) $(DOCKER_REPOSITORY) $(VERSION) $(DOCKER_USER) $(DOCKER_PASSWORD) \ No newline at end of file + ./scripts/publish.sh $(GOLANG_VERSION) $(TINI_VERSION) $(DOCKER_REGISTRY) $(VERSION) $(DOCKER_USER) $(DOCKER_PASSWORD) \ No newline at end of file diff --git a/README.md b/README.md index 68d014a3..d5ec5ddd 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- Gotenberg logo + Gotenberg logo

Gotenberg

A Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF.

@@ -39,4 +39,6 @@ to learn how to interact with it! --- -Psst: TheCodingMachine is always looking for [talented coders](https://coders.thecodingmachine.com). +

+ Gotenberg logo +

diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 00000000..4dacc2ee Binary files /dev/null and b/assets/logo.png differ diff --git a/assets/logo2.png b/assets/logo2.png new file mode 100644 index 00000000..de91ad06 Binary files /dev/null and b/assets/logo2.png differ diff --git a/build/base/Dockerfile b/build/base/Dockerfile index c40df230..31f78d84 100644 --- a/build/base/Dockerfile +++ b/build/base/Dockerfile @@ -104,7 +104,10 @@ COPY build/base/fonts.conf /etc/fonts/conf.d/100-gotenberg.conf # | non-root user. # | -RUN groupadd --gid 1001 gotenberg \ - && useradd --uid 1001 --gid gotenberg --shell /bin/bash --home /gotenberg --no-create-home gotenberg \ +ARG GOTENBERG_USER_GID=1001 +ARG GOTENBERG_USER_UID=1001 + +RUN groupadd --gid ${GOTENBERG_USER_GID} gotenberg \ + && useradd --uid ${GOTENBERG_USER_UID} --gid gotenberg --shell /bin/bash --home /gotenberg --no-create-home gotenberg \ && mkdir /gotenberg \ && chown gotenberg: /gotenberg \ No newline at end of file diff --git a/build/docs/content/01-install.md b/build/docs/content/01-install.md index 6054cf75..7e2c4d0e 100644 --- a/build/docs/content/01-install.md +++ b/build/docs/content/01-install.md @@ -4,8 +4,6 @@ title: Install Gotenberg is shipped within a Docker image. -> It uses a dedicated non-root user called `gotenberg` with uid and gid `1001`. - You may start it with: ```bash @@ -14,6 +12,23 @@ $ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:6 > The API will be available at [http://localhost:3000](http://localhost:3000). +The image uses a dedicated non-root user called `gotenberg` with uid and gid `1001`. + +If you wish to change those uid and gid, you will have to: + +* clone the project +* re-build the image +* publish the image in your own Docker registry + +For instance: + +```bash +$ git clone https://github.com/thecodingmachine/gotenberg.git +$ make publish GOTENBERG_USER_GID=your_custom_gid GOTENBERG_USER_UID=your_custom_uid DOCKER_REGISTRY=your_registry DOCKER_USER=registry_user DOCKER_PASSWORD=registry_password VERSION=6.1.0 +``` + +> `master` branch is always up-to-date with the latest version of the API. + ## Docker Compose You may also add it in your Docker Compose stack: @@ -39,7 +54,7 @@ Make sure to provide enough memory and CPU requests (for instance `512Mi` and `0 > The more resources are granted, the quicker will be the conversions. -In the deployment specification of the pod, also specify the uid `1001` of the user `gotenberg`: +In the deployment specification of the pod, also specify the uid of the user `gotenberg`: ``` securityContext: @@ -47,5 +62,9 @@ securityContext: runAsUser: 1001 ``` +## Cloud Run (Google Cloud) + +If you're looking for cost savings, you might be interested by [Cloud Run](https://cloud.google.com/run). + In the following examples, we will assume your Gotenberg API is available at [http://localhost:3000](http://localhost:3000). diff --git a/build/docs/content/02-clients.md b/build/docs/content/02-clients.md index 64de1e23..0f4ce7d2 100644 --- a/build/docs/content/02-clients.md +++ b/build/docs/content/02-clients.md @@ -7,9 +7,11 @@ We provide clients in various languages for easing the interactions with the API ## Go client ```bash -$ go get -u github.com/thecodingmachine/gotenberg-go-client/v6 +$ go get -u github.com/thecodingmachine/gotenberg-go-client/v7 ``` +See also the example from the [README](https://github.com/thecodingmachine/gotenberg-go-client/blob/master/README.md). + ## PHP client Unless your project already has a PSR7 `HttpClient`, install `php-http/guzzle6-adapter`: @@ -18,8 +20,15 @@ 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 ``` + +See also the example from the [README](https://github.com/thecodingmachine/gotenberg-php-client/blob/master/README.md). + +## Community clients + +* [JavaScript/TypeScript client](https://github.com/yumauri/gotenberg-js-client) by [yumauri](https://github.com/yumauri) +* [C# client](https://github.com/ChangemakerStudios/GotenbergSharpApiClient) by [ChangemakerStudios](https://github.com/ChangemakerStudios) diff --git a/build/docs/content/03-environment-variables.md b/build/docs/content/03-environment-variables.md index 7e3c6b97..b1522740 100644 --- a/build/docs/content/03-environment-variables.md +++ b/build/docs/content/03-environment-variables.md @@ -23,6 +23,18 @@ You may customize this value with the environment variable `DEFAULT_LISTEN_PORT` This environment variable accepts any string that can be turned into a port number. +## Root path + +By default, the API root path is `/`. + +You may customize this value with the environment variable `ROOT_PATH`. + +This environment variable accepts a string starting and ending with `/`. + +For instance, `/gotenberg/` is a valid value while `gotenberg` is not. + +> This is useful if you wish to do service discovery via URL paths. + ## Disable Google Chrome In order to save some resources, the Gotenberg image accepts the environment variable `DISABLE_GOOGLE_CHROME` @@ -33,6 +45,19 @@ It takes the strings `"0"` or `"1"` as value where `1` means `true` > If Google Chrome is disabled, the following conversions will **not** be available anymore: > [HTML](#html), [URL](#url) and [Markdown](#markdown) +## Default Google Chrome rpcc buffer size + +When performing a [HTML](#html), [URL](#url) or [Markdown](#markdown) conversion, the API might return +a `400` HTTP code with the message `increase the Google Chrome rpcc buffer size`. + +If so, you may increase this buffer size with the environment variable `DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE`. + +It takes a string representation of an int as value (e.g. `"1048576"` for 1 MB). +The hard limit is 100 MB and is defined by Google Chrome itself. + +> The default Google Chrome rpcc buffer size may also be overridden per request thanks to the form field `googleChromeRpccBufferSize`. +> See the [rpcc buffer size section](#html.rpcc_buffer_size). + ## Disable LibreOffice (unoconv) You may also disable LibreOffice (unoconv) with `DISABLE_UNOCONV`. diff --git a/build/docs/content/04-html.md b/build/docs/content/04-html.md index e008cda7..7ff5532f 100644 --- a/build/docs/content/04-html.md +++ b/build/docs/content/04-html.md @@ -39,14 +39,13 @@ $ curl --request POST \ ### Go ```golang -import "github.com/thecodingmachine/gotenberg-go-client/v6" +import "github.com/thecodingmachine/gotenberg-go-client/v7" -func main() { - c := &gotenberg.Client{Hostname: "http://localhost:3000"} - req, _ := gotenberg.NewHTMLRequest("index.html") - dest := "result.pdf" - c.Store(req, dest) -} +c := &gotenberg.Client{Hostname: "http://localhost:3000"} +index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file") +req := gotenberg.NewHTMLRequest(index) +dest := "result.pdf" +c.Store(req, dest) ``` ### PHP @@ -57,10 +56,10 @@ 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'); +$index = DocumentFactory::makeFromPath('index.html', '/path/to/file'); $request = new HTMLRequest($index); -$dirPath = "/foo"; -$filename = $client->store($request, $dirPath); +$dest = 'result.pdf'; +$client->store($request, $dest); ``` ## Header and footer @@ -103,6 +102,7 @@ There are some limitations: * `footer.html` CSS properties override the ones from `header.html` * only fonts installed in the Docker image are loaded (see the [fonts section](#fonts)) * images only work using a `base64` encoded source (`setHeader($header); $request->setFooter($footer); -$dirPath = "/foo"; -$filename = $client->store($request, $dirPath); +$dest = 'result.pdf'; +$client->store($request, $dest); ``` ## Assets @@ -209,15 +210,17 @@ $ curl --request POST \ ### Go ```golang -import "github.com/thecodingmachine/gotenberg-go-client/v6" +import "github.com/thecodingmachine/gotenberg-go-client/v7" -func main() { - c := &gotenberg.Client{Hostname: "http://localhost:3000"} - req, _ := gotenberg.NewHTMLRequest("index.html") - req.Assets("font.woff", "img.gif", "style.css") - dest := "result.pdf" - c.Store(req, dest) -} +c := &gotenberg.Client{Hostname: "http://localhost:3000"} +index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file") +style, _ := gotenberg.NewDocumentFromPath("style.css", "/path/to/file") +img, _ := gotenberg.NewDocumentFromPath("img.png", "/path/to/file") +font, _ := gotenberg.NewDocumentFromPath("font.woff", "/path/to/file") +req := gotenberg.NewHTMLRequest(index) +req.Assets(style, img, font) +dest := "result.pdf" +c.Store(req, dest) ``` ### PHP @@ -228,15 +231,15 @@ 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'); +$index = DocumentFactory::makeFromPath('index.html', '/path/to/file'); $assets = [ - DocumentFactory::makeFromPath('style.css', 'style.css'), - DocumentFactory::makeFromPath('img.png', 'img.png'), - DocumentFactory::makeFromPath('font.woff', 'font.woff'), + DocumentFactory::makeFromPath('style.css', '/path/to/file'), + DocumentFactory::makeFromPath('img.png', '/path/to/file'), + DocumentFactory::makeFromPath('font.woff', '/path/to/file'), ]; $request = new HTMLRequest($index); $request->setAssets($assets); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); ``` @@ -268,17 +271,16 @@ $ curl --request POST \ ### Go ```golang -import "github.com/thecodingmachine/gotenberg-go-client/v6" +import "github.com/thecodingmachine/gotenberg-go-client/v7" -func main() { - c := &gotenberg.Client{Hostname: "http://localhost:3000"} - req, _ := gotenberg.NewHTMLRequest("index.html") - req.PaperSize(gotenberg.A4) - req.Margins(gotenberg.NoMargins) - req.Landscape(true) - dest := "result.pdf" - c.Store(req, dest) -} +c := &gotenberg.Client{Hostname: "http://localhost:3000"} +index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file") +req := gotenberg.NewHTMLRequest(index) +req.PaperSize(gotenberg.A4) +req.Margins(gotenberg.NoMargins) +req.Landscape(true) +dest := "result.pdf" +c.Store(req, dest) ``` ### PHP @@ -290,11 +292,58 @@ use TheCodingMachine\Gotenberg\HTMLRequest; use TheCodingMachine\Gotenberg\Request; $client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); -$index = DocumentFactory::makeFromPath('index.html', 'index.html'); +$index = DocumentFactory::makeFromPath('index.html', '/path/to/file'); $request = new HTMLRequest($index); $request->setPaperSize(Request::A4); $request->setMargins(Request::NO_MARGINS); $request->setLandscape(true); +$dest = 'result.pdf'; +$client->store($request, $dest); +``` + +## Page ranges + +You may specify the page ranges to convert. + +The format is the same as the one from the print options +of Google Chrome, e.g. `1-5,8,11-13`. + +### cURL + +```bash +$ curl --request POST \ + --url http://localhost:3000/convert/html \ + --header 'Content-Type: multipart/form-data' \ + --form files=@index.html \ + --form pageRanges='1-3,5' \ + -o result.pdf +``` + +### Go + +```golang +import "github.com/thecodingmachine/gotenberg-go-client/v7" + +c := &gotenberg.Client{Hostname: "http://localhost:3000"} +index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file") +req := gotenberg.NewHTMLRequest(index) +req.PageRanges("1-3,5") +dest := "result.pdf" +c.Store(req, dest) +``` + +### PHP + +```php +use TheCodingMachine\Gotenberg\Client; +use TheCodingMachine\Gotenberg\DocumentFactory; +use TheCodingMachine\Gotenberg\HTMLRequest; +use TheCodingMachine\Gotenberg\Request; + +$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); +$index = DocumentFactory::makeFromPath('index.html', '/path/to/file'); +$request = new HTMLRequest($index); +$request->setPageRanges("1-3,5"); $dest = "result.pdf"; $client->store($request, $dest); ``` @@ -321,15 +370,14 @@ $ curl --request POST \ ### Go ```golang -import "github.com/thecodingmachine/gotenberg-go-client/v6" +import "github.com/thecodingmachine/gotenberg-go-client/v7" -func main() { - c := &gotenberg.Client{Hostname: "http://localhost:3000"} - req, _ := gotenberg.NewHTMLRequest("index.html") - req.WaitDelay(5.5) - dest := "result.pdf" - c.Store(req, dest) -} +c := &gotenberg.Client{Hostname: "http://localhost:3000"} +index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file") +req := gotenberg.NewHTMLRequest(index) +req.WaitDelay(5.5) +dest := "result.pdf" +c.Store(req, dest) ``` ### PHP @@ -341,19 +389,23 @@ use TheCodingMachine\Gotenberg\HTMLRequest; use TheCodingMachine\Gotenberg\Request; $client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); -$index = DocumentFactory::makeFromPath('index.html', 'index.html'); +$index = DocumentFactory::makeFromPath('index.html', '/path/to/file'); $request = new HTMLRequest($index); $request->setWaitDelay(5.5); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); ``` -## Page ranges +## Rpcc buffer size -You may specify the page ranges to convert. +The API might return a `400` HTTP code with the message `increase the Google Chrome rpcc buffer size`. -The format is the same as the one from the print options -of Google Chrome, e.g. `1-5,8,11-13`. +If so, you may increase this buffer size with a form field named `googleChromeRpccBufferSize`. + +It takes an int as value (e.g. `1048576` for 1 MB). +The hard limit is 100 MB and is defined by Google Chrome itself. + +> You may also define this value globally: see the [environment variables](#environment_variables.default_google_chrome_rpcc_buffer_size) section. ### cURL @@ -362,22 +414,21 @@ $ curl --request POST \ --url http://localhost:3000/convert/html \ --header 'Content-Type: multipart/form-data' \ --form files=@index.html \ - --form pageRanges='1-3,5' \ + --form googleChromeRpccBufferSize=1048576 \ -o result.pdf ``` ### Go ```golang -import "github.com/thecodingmachine/gotenberg-go-client/v6" +import "github.com/thecodingmachine/gotenberg-go-client/v7" -func main() { - c := &gotenberg.Client{Hostname: "http://localhost:3000"} - req, _ := gotenberg.NewHTMLRequest("index.html") - req.PageRanges("1-3,5") - dest := "result.pdf" - c.Store(req, dest) -} +c := &gotenberg.Client{Hostname: "http://localhost:3000"} +index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file") +req := gotenberg.NewHTMLRequest(index) +req.GoogleChromeRpccBufferSize(1048576) +dest := "result.pdf" +c.Store(req, dest) ``` ### PHP @@ -389,9 +440,9 @@ use TheCodingMachine\Gotenberg\HTMLRequest; use TheCodingMachine\Gotenberg\Request; $client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); -$index = DocumentFactory::makeFromPath('index.html', 'index.html'); +$index = DocumentFactory::makeFromPath('index.html', '/path/to/file'); $request = new HTMLRequest($index); -$request->setPageRanges("1-3,5"); -$dest = "result.pdf"; +$request->setGoogleChromeRpccBufferSize(1048576); +$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..fc307445 100644 --- a/build/docs/content/05-url.md +++ b/build/docs/content/05-url.md @@ -31,15 +31,13 @@ $ curl --request POST \ ### Go ```golang -import "github.com/thecodingmachine/gotenberg-go-client/v6" +import "github.com/thecodingmachine/gotenberg-go-client/v7" -func main() { - c := &gotenberg.Client{Hostname: "http://localhost:3000"} - req := gotenberg.NewURLRequest("https://google.com") - req.Margins(gotenberg.NoMargins) - dest := "result.pdf" - c.Store(req, dest) -} +c := &gotenberg.Client{Hostname: "http://localhost:3000"} +req := gotenberg.NewURLRequest("https://google.com") +req.Margins(gotenberg.NoMargins) +dest := "result.pdf" +c.Store(req, dest) ``` ### PHP @@ -51,6 +49,55 @@ 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/v7" + +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..5838ea7b 100644 --- a/build/docs/content/06-markdown.md +++ b/build/docs/content/06-markdown.md @@ -42,14 +42,14 @@ $ curl --request POST \ ### Go ```golang -import "github.com/thecodingmachine/gotenberg-go-client/v6" +import "github.com/thecodingmachine/gotenberg-go-client/v7" -func main() { - c := &gotenberg.Client{Hostname: "http://localhost:3000"} - req, _ := gotenberg.NewMarkdownRequest("index.html", "file.md") - dest := "result.pdf" - c.Store(req, dest) -} +c := &gotenberg.Client{Hostname: "http://localhost:3000"} +index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file") +markdown, _ := gotenberg.NewDocumentFromPath("file.md", "/path/to/file") +req := gotenberg.NewMarkdownRequest(index, markdown) +dest := "result.pdf" +c.Store(req, dest) ``` ### PHP @@ -60,11 +60,11 @@ use TheCodingMachine\Gotenberg\DocumentFactory; use TheCodingMachine\Gotenberg\MarkdownRequest; $client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); -$index = DocumentFactory::makeFromPath('index.html', 'index.html'); +$index = DocumentFactory::makeFromPath('index.html', '/path/to/file'); $markdowns = [ - DocumentFactory::makeFromPath('file.md', 'file.md'), + DocumentFactory::makeFromPath('file.md', '/path/to/file'), ]; $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 546ee7de..a6ec46e0 100644 --- a/build/docs/content/07-office.md +++ b/build/docs/content/07-office.md @@ -41,14 +41,14 @@ $ curl --request POST \ ### Go ```golang -import "github.com/thecodingmachine/gotenberg-go-client/v6" +import "github.com/thecodingmachine/gotenberg-go-client/v7" -func main() { - c := &gotenberg.Client{Hostname: "http://localhost:3000"} - req, _ := gotenberg.NewOfficeRequest("document.docx", "document2.docx") - dest := "result.pdf" - c.Store(req, dest) -} +c := &gotenberg.Client{Hostname: "http://localhost:3000"} +doc, _ := gotenberg.NewDocumentFromPath("document.docx", "/path/to/file") +doc2, _ := gotenberg.NewDocumentFromPath("document2.docx", "/path/to/file") +req := gotenberg.NewOfficeRequest(doc, doc2) +dest := "result.pdf" +c.Store(req, dest) ``` ### PHP @@ -60,11 +60,11 @@ use TheCodingMachine\Gotenberg\OfficeRequest; $client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); $files = [ - DocumentFactory::makeFromPath('document.docx', 'document.docx'), - DocumentFactory::makeFromPath('document2.docx', 'document2.docx'), + DocumentFactory::makeFromPath('document.docx', '/path/to/file'), + DocumentFactory::makeFromPath('document2.docx', '/path/to/file'), ]; $request = new OfficeRequest($files); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); ``` @@ -88,15 +88,14 @@ $ curl --request POST \ ### Go ```golang -import "github.com/thecodingmachine/gotenberg-go-client/v6" +import "github.com/thecodingmachine/gotenberg-go-client/v7" -func main() { - c := &gotenberg.Client{Hostname: "http://localhost:3000"} - req, _ := gotenberg.NewOfficeRequest("document.docx") - req.Landscape(true) - dest := "result.pdf" - c.Store(req, dest) -} +c := &gotenberg.Client{Hostname: "http://localhost:3000"} +doc, _ := gotenberg.NewDocumentFromPath("document.docx", "/path/to/file") +req := gotenberg.NewOfficeRequest(doc) +req.Landscape(true) +dest := "result.pdf" +c.Store(req, dest) ``` ### PHP @@ -108,11 +107,11 @@ use TheCodingMachine\Gotenberg\OfficeRequest; $client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); $files = [ - DocumentFactory::makeFromPath('document.docx', 'document.docx'), + DocumentFactory::makeFromPath('document.docx', '/path/to/file'), ]; $request = new OfficeRequest($files); $request->setLandscape(true); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); ``` @@ -142,13 +141,12 @@ $ curl --request POST \ ```golang import "github.com/thecodingmachine/gotenberg-go-client/v6" -func main() { - c := &gotenberg.Client{Hostname: "http://localhost:3000"} - req, _ := gotenberg.NewOfficeRequest("document.docx") - req.PageRanges("1-3") - dest := "result.pdf" - c.Store(req, dest) -} +c := &gotenberg.Client{Hostname: "http://localhost:3000"} +doc, _ := gotenberg.NewDocumentFromPath("document.docx", "/path/to/file") +req := gotenberg.NewOfficeRequest(doc) +req.PageRanges("1-3") +dest := "result.pdf" +c.Store(req, dest) ``` ### PHP @@ -160,7 +158,7 @@ use TheCodingMachine\Gotenberg\OfficeRequest; $client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); $files = [ - DocumentFactory::makeFromPath('document.docx', 'document.docx'), + DocumentFactory::makeFromPath('document.docx', '/path/to/file'), ]; $request = new OfficeRequest($files); $request->setPageRanges("1-3"); diff --git a/build/docs/content/08-merge.md b/build/docs/content/08-merge.md index 49ac0b2b..8e693743 100644 --- a/build/docs/content/08-merge.md +++ b/build/docs/content/08-merge.md @@ -27,14 +27,14 @@ $ curl --request POST \ ### Go ```golang -import "github.com/thecodingmachine/gotenberg-go-client/v6" +import "github.com/thecodingmachine/gotenberg-go-client/v7" -func main() { - c := &gotenberg.Client{Hostname: "http://localhost:3000"} - req, _ := gotenberg.NewMergeRequest("file.pdf", "file2.pdf") - dest := "result.pdf" - c.Store(req, dest) -} +c := &gotenberg.Client{Hostname: "http://localhost:3000"} +pdf, _ := gotenberg.NewDocumentFromPath("file.pdf", "/path/to/file") +pdf2, _ := gotenberg.NewDocumentFromPath("file2.pdf", "/path/to/file") +req := gotenberg.NewMergeRequest(pdf, pdf2) +dest := "result.pdf" +c.Store(req, dest) ``` ### PHP @@ -46,10 +46,10 @@ use TheCodingMachine\Gotenberg\MergeRequest; $client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); $files = [ - DocumentFactory::makeFromPath('file.pdf', 'file.pdf'), - DocumentFactory::makeFromPath('file2.pdf', 'file2.pdf'), + DocumentFactory::makeFromPath('file.pdf', '/path/to/file'), + DocumentFactory::makeFromPath('file2.pdf', '/path/to/file'), ]; $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..c7cbbd70 100644 --- a/build/docs/content/09-timeout.md +++ b/build/docs/content/09-timeout.md @@ -26,14 +26,13 @@ $ curl --request POST \ ### Go ```golang -import "github.com/thecodingmachine/gotenberg-go-client/v6" +import "github.com/thecodingmachine/gotenberg-go-client/v7" -func main() { - c := &gotenberg.Client{Hostname: "http://localhost:3000"} - req, _ := gotenberg.NewHTMLRequest("index.html") - req.WaitTimeout(2.5) - resp, _ := c.Post(req) -} +c := &gotenberg.Client{Hostname: "http://localhost:3000"} +index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file") +req := gotenberg.NewHTMLRequest(index) +req.WaitTimeout(2.5) +resp, _ := c.Post(req) ``` ### PHP @@ -45,9 +44,9 @@ use TheCodingMachine\Gotenberg\HTMLRequest; use TheCodingMachine\Gotenberg\Request; $client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); -$index = DocumentFactory::makeFromPath('index.html', 'index.html'); +$index = DocumentFactory::makeFromPath('index.html', '/path/to/file'); $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..3d5e69e1 100644 --- a/build/docs/content/10-webhook.md +++ b/build/docs/content/10-webhook.md @@ -24,14 +24,13 @@ $ curl --request POST \ ### Go ```golang -import "github.com/thecodingmachine/gotenberg-go-client/v6" +import "github.com/thecodingmachine/gotenberg-go-client/v7" -func main() { - c := &gotenberg.Client{Hostname: "http://localhost:3000"} - req, _ := gotenberg.NewHTMLRequest("index.html") - req.WebhookURL("http://myapp.com/webhook/") - resp, _ := c.Post(req) -} +c := &gotenberg.Client{Hostname: "http://localhost:3000"} +index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file") +req := gotenberg.NewHTMLRequest(index) +req.WebhookURL("http://myapp.com/webhook/") +resp, _ := c.Post(req) ``` ### PHP @@ -42,7 +41,7 @@ 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'); +$index = DocumentFactory::makeFromPath('index.html', '/path/to/file'); $request = new HTMLRequest($index); $request->setWebhookURL('http://myapp.com/webhook/'); $resp = $client->post($request); @@ -58,9 +57,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,21 +68,20 @@ $ curl --request POST \ --form webhookURLTimeout=2.5 ``` -#### Go +### Go ```golang -import "github.com/thecodingmachine/gotenberg-go-client/v6" +import "github.com/thecodingmachine/gotenberg-go-client/v7" -func main() { - c := &gotenberg.Client{Hostname: "http://localhost:3000"} - req, _ := gotenberg.NewHTMLRequest("index.html") - req.WebhookURL("http://myapp.com/webhook/") - req.WebhookURLTimeout(2.5) - resp, _ := c.Post(req) -} +c := &gotenberg.Client{Hostname: "http://localhost:3000"} +index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file") +req := gotenberg.NewHTMLRequest(index) +req.WebhookURL("http://myapp.com/webhook/") +req.WebhookURLTimeout(2.5) +resp, _ := c.Post(req) ``` -#### PHP +### PHP ```php use TheCodingMachine\Gotenberg\Client; @@ -93,9 +89,61 @@ 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'); +$index = DocumentFactory::makeFromPath('index.html', '/path/to/file'); $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 + +```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/v7" + +c := &gotenberg.Client{Hostname: "http://localhost:3000"} +index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file") +req := gotenberg.NewHTMLRequest(index) +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', '/path/to/file'); +$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/build/docs/content/11-result-filename.md b/build/docs/content/11-result-filename.md index aabb4177..94bc230f 100644 --- a/build/docs/content/11-result-filename.md +++ b/build/docs/content/11-result-filename.md @@ -24,14 +24,13 @@ $ curl --request POST \ ### Go ```golang -import "github.com/thecodingmachine/gotenberg-go-client/v6" +import "github.com/thecodingmachine/gotenberg-go-client/v7" -func main() { - c := &gotenberg.Client{Hostname: "http://localhost:3000"} - req, _ := gotenberg.NewHTMLRequest("index.html") - req.ResultFilename("foo.pdf") - resp, _ := c.Post(req) -} +c := &gotenberg.Client{Hostname: "http://localhost:3000"} +index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file") +req := gotenberg.NewHTMLRequest(index) +req.ResultFilename("foo.pdf") +resp, _ := c.Post(req) ``` ### PHP @@ -43,7 +42,7 @@ use TheCodingMachine\Gotenberg\HTMLRequest; use TheCodingMachine\Gotenberg\Request; $client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); -$index = DocumentFactory::makeFromPath('index.html', 'index.html'); +$index = DocumentFactory::makeFromPath('index.html', '/path/to/file'); $request = new HTMLRequest($index); $request->setResultFilename('foo.pdf'); $resp = $client->post($request); diff --git a/build/docs/content/12-scalability.md b/build/docs/content/12-scalability.md index 7b30ef41..b1a062fc 100644 --- a/build/docs/content/12-scalability.md +++ b/build/docs/content/12-scalability.md @@ -5,10 +5,10 @@ title: Scalability The API uses under the hood intricate programs. Gotenberg tries to abstract as much complexity as possible but it can -only do it to a certain extend. +only do it to a certain extent. For instance, [Office](#office) and [Merge](#merge) endpoints will start respectively as many LibreOffice (unoconv) and PDTk -instances are there are requests. The limitation here is the available memory and CPU usage. +instances as there are requests. The limitation here is the available memory and CPU usage. On another hand, for the [HTML](#html), [URL](#url) and [Markdown](#markdown) endpoints, the API does only 6 conversions in parallel. Indeed, Google Chrome misbehaves if there are too many concurrent conversions. diff --git a/build/docs/content/15-links.md b/build/docs/content/15-links.md index 23ac7d55..17f2aa7c 100644 --- a/build/docs/content/15-links.md +++ b/build/docs/content/15-links.md @@ -2,8 +2,10 @@ title: Links --- +

+ Gotenberg logo +

+ * Follow the progress on the [GitHub repository](https://github.com/thecodingmachine/gotenberg) * Follow [@gulnap](https://twitter.com/gulnap) on Twitter * Thanks to [@mafredri](https://github.com/mafredri) for its help and its wonderful [cdp](https://github.com/mafredri/cdp) library - -Psst: TheCodingMachine is always looking for [talented coders](https://coders.thecodingmachine.com). diff --git a/build/lint/Dockerfile b/build/lint/Dockerfile index fd08c9c5..42a8c054 100644 --- a/build/lint/Dockerfile +++ b/build/lint/Dockerfile @@ -33,4 +33,4 @@ RUN go mod download &&\ # Copy our code source. COPY --chown=gotenberg:gotenberg . . -CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl", "--disable=funlen" ] \ No newline at end of file +CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl", "--disable=funlen", "--disable=wsl", "--disable=gocognit" ] \ No newline at end of file diff --git a/build/package/Dockerfile b/build/package/Dockerfile index dc36ad1f..09a8f9af 100644 --- a/build/package/Dockerfile +++ b/build/package/Dockerfile @@ -2,7 +2,7 @@ # | Binary # |-------------------------------------------------------------------------- # | -# | Buils Gotenberg binary. +# | Builds Gotenberg binary. # | FROM thecodingmachine/gotenberg:workspace AS workspace @@ -42,7 +42,7 @@ LABEL authors="Julien Neuhart " ARG TINI_VERSION -ADD https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini /tini +ADD --chown=gotenberg https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-static /tini RUN chmod +x /tini ENTRYPOINT [ "/tini", "--" ] @@ -59,4 +59,4 @@ USER gotenberg WORKDIR /gotenberg EXPOSE 3000 -CMD [ "gotenberg" ] \ No newline at end of file +CMD [ "gotenberg" ] diff --git a/docs/index.html b/docs/index.html index b03e62e3..d2ec8df3 100755 --- a/docs/index.html +++ b/docs/index.html @@ -32,7 +32,7 @@
- Gotenberg logo + Gotenberg logo Gotenberg A Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF.
@@ -132,33 +132,49 @@ Install

Gotenberg is shipped within a Docker image.

-
-

It uses a dedicated non-root user called gotenberg with uid and gid 1001.

-
-

You may start it with:

-
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:6
+
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:6
 

The API will be available at http://localhost:3000.

+

The image uses a dedicated non-root user called gotenberg with uid and gid 1001.

+ +

If you wish to change those uid and gid, you will have to:

+ +
    +
  • clone the project
  • +
  • re-build the image
  • +
  • publish the image in your own Docker registry
  • +
+ +

For instance:

+ +
$ git clone https://github.com/thecodingmachine/gotenberg.git
+$ make publish GOTENBERG_USER_GID=your_custom_gid GOTENBERG_USER_UID=your_custom_uid DOCKER_REGISTRY=your_registry DOCKER_USER=registry_user DOCKER_PASSWORD=registry_password VERSION=6.1.0 
+
+ +
+

master branch is always up-to-date with the latest version of the API.

+
+

Docker Compose

You may also add it in your Docker Compose stack:

-
version: '3'
+
version: '3'
 
-services:
+services:
 
   # your others services
 
-  gotenberg:
-    image: thecodingmachine/gotenberg:6
+  gotenberg:
+    image: thecodingmachine/gotenberg:6
 
@@ -177,13 +193,19 @@

The more resources are granted, the quicker will be the conversions.

-

In the deployment specification of the pod, also specify the uid 1001 of the user gotenberg:

+

In the deployment specification of the pod, also specify the uid of the user gotenberg:

securityContext:
   privileged: false
   runAsUser: 1001
 
+

Cloud Run (Google Cloud)

+ +

If you’re looking for cost savings, you might be interested by Cloud Run.

+

In the following examples, we will assume your Gotenberg API is available at http://localhost:3000.

@@ -199,9 +221,11 @@ Gotenberg API is available at http://localhost:3 Go client -
$ go get -u github.com/thecodingmachine/gotenberg-go-client/v6
+
$ go get -u github.com/thecodingmachine/gotenberg-go-client/v7
 
+

See also the example from the README.

+

PHP client

@@ -211,11 +235,22 @@ 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
 
+

See also the example from the README.

+ +

Community clients

+ + +
@@ -249,6 +284,22 @@ about what’s going on.

This environment variable accepts any string that can be turned into a port number.

+

Root path

+ +

By default, the API root path is /.

+ +

You may customize this value with the environment variable ROOT_PATH.

+ +

This environment variable accepts a string starting and ending with /.

+ +

For instance, /gotenberg/ is a valid value while gotenberg is not.

+ +
+

This is useful if you wish to do service discovery via URL paths.

+
+

Disable Google Chrome

@@ -263,6 +314,23 @@ for disabling Google Chrome.

HTML, URL and Markdown

+

Default Google Chrome rpcc buffer size

+ +

When performing a HTML, URL or Markdown conversion, the API might return +a 400 HTTP code with the message increase the Google Chrome rpcc buffer size.

+ +

If so, you may increase this buffer size with the environment variable DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE.

+ +

It takes a string representation of an int as value (e.g. "1048576" for 1 MB). +The hard limit is 100 MB and is defined by Google Chrome itself.

+ +
+

The default Google Chrome rpcc buffer size may also be overridden per request thanks to the form field googleChromeRpccBufferSize. +See the rpcc buffer size section.

+
+

Disable LibreOffice (unoconv)

@@ -389,14 +457,13 @@ which will be converted to PDF.

Go -
import "github.com/thecodingmachine/gotenberg-go-client/v6"
+
import "github.com/thecodingmachine/gotenberg-go-client/v7"
 
-func main() {
-    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-    req, _ := gotenberg.NewHTMLRequest("index.html")
-    dest := "result.pdf"
-    c.Store(req, dest)
-}
+c := &gotenberg.Client{Hostname: "http://localhost:3000"}
+index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
+req := gotenberg.NewHTMLRequest(index)
+dest := "result.pdf"
+c.Store(req, dest)
 

  • fonts section)
  • images only work using a base64 encoded source (<img src="data:image/png;base64, iVBORw0K... />)
  • +
  • background-color and color CSS properties require an additional -webkit-print-color-adjust: exact CSS property in order to work
  • Go

    -
    import "github.com/thecodingmachine/gotenberg-go-client/v6"
    +
    import "github.com/thecodingmachine/gotenberg-go-client/v7"
     
    -func main() {
    -    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    -    req, _ := gotenberg.NewHTMLRequest("index.html")
    -    req.Header("header.html")
    -    req.Footer("footer.html")
    -    dest := "result.pdf"
    -    c.Store(req, dest)
    -}
    +c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    +index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
    +header, _ := gotenberg.NewDocumentFromPath("header.html", "/path/to/file")
    +footer, _ := gotenberg.NewDocumentFromPath("footer.html", "/path/to/file")
    +req := gotenberg.NewHTMLRequest(index)
    +req.Header(header)
    +req.Footer(footer)
    +dest := "result.pdf"
    +c.Store(req, dest)
     

    fonts section.

    Go

    -
    import "github.com/thecodingmachine/gotenberg-go-client/v6"
    +
    import "github.com/thecodingmachine/gotenberg-go-client/v7"
     
    -func main() {
    -    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    -    req, _ := gotenberg.NewHTMLRequest("index.html")
    -    req.Assets("font.woff", "img.gif", "style.css")
    -    dest := "result.pdf"
    -    c.Store(req, dest)
    -}
    +c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    +index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
    +style, _ := gotenberg.NewDocumentFromPath("style.css", "/path/to/file")
    +img, _ := gotenberg.NewDocumentFromPath("img.png", "/path/to/file")
    +font, _ := gotenberg.NewDocumentFromPath("font.woff", "/path/to/file")
    +req := gotenberg.NewHTMLRequest(index)
    +req.Assets(style, img, font)
    +dest := "result.pdf"
    +c.Store(req, dest)
     

    Go -
    import "github.com/thecodingmachine/gotenberg-go-client/v6"
    +
    import "github.com/thecodingmachine/gotenberg-go-client/v7"
     
    -func main() {
    -    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    -    req, _ := gotenberg.NewHTMLRequest("index.html")
    -    req.PaperSize(gotenberg.A4)
    -    req.Margins(gotenberg.NoMargins)
    -    req.Landscape(true)
    -    dest := "result.pdf"
    -    c.Store(req, dest)
    -}
    +c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    +index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
    +req := gotenberg.NewHTMLRequest(index)
    +req.PaperSize(gotenberg.A4)
    +req.Margins(gotenberg.NoMargins)
    +req.Landscape(true)
    +dest := "result.pdf"
    +c.Store(req, dest)
     

    Wait delay

    - -

    In some cases, you may want to wait a certain amount of time to make sure the -page you’re trying to generate is fully rendered. For instance, if your page relies -a lot on JavaScript for rendering.

    - -
    -

    The wait delay is a duration in seconds (e.g 2.5 for 2.5 seconds).

    -
    - -

    cURL

    - -
    $ curl --request POST \
    -    --url http://localhost:3000/convert/html \
    -    --header 'Content-Type: multipart/form-data' \
    -    --form files=@index.html \
    -    --form waitDelay=5.5 \
    -    -o result.pdf
    -
    - -

    Go

    - -
    import "github.com/thecodingmachine/gotenberg-go-client/v6"
    -
    -func main() {
    -    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    -    req, _ := gotenberg.NewHTMLRequest("index.html")
    -    req.WaitDelay(5.5)
    -    dest := "result.pdf"
    -    c.Store(req, dest)
    -}
    -
    - -

    PHP

    - -
    use TheCodingMachine\Gotenberg\Client;
    -use TheCodingMachine\Gotenberg\DocumentFactory;
    -use TheCodingMachine\Gotenberg\HTMLRequest;
    -use TheCodingMachine\Gotenberg\Request;
    -
    -$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);
     
    @@ -751,15 +765,14 @@ of Google Chrome, e.g. 1-5,8,11-13.

    Go -
    import "github.com/thecodingmachine/gotenberg-go-client/v6"
    +
    import "github.com/thecodingmachine/gotenberg-go-client/v7"
     
    -func main() {
    -    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    -    req, _ := gotenberg.NewHTMLRequest("index.html")
    -    req.PageRanges("1-3,5")
    -    dest := "result.pdf"
    -    c.Store(req, dest)
    -}
    +c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    +index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
    +req := gotenberg.NewHTMLRequest(index)
    +req.PageRanges("1-3,5")
    +dest := "result.pdf"
    +c.Store(req, dest)
     

    Wait delay

    + +

    In some cases, you may want to wait a certain amount of time to make sure the +page you’re trying to generate is fully rendered. For instance, if your page relies +a lot on JavaScript for rendering.

    + +
    +

    The wait delay is a duration in seconds (e.g 2.5 for 2.5 seconds).

    +
    + +

    cURL

    + +
    $ curl --request POST \
    +    --url http://localhost:3000/convert/html \
    +    --header 'Content-Type: multipart/form-data' \
    +    --form files=@index.html \
    +    --form waitDelay=5.5 \
    +    -o result.pdf
    +
    + +

    Go

    + +
    import "github.com/thecodingmachine/gotenberg-go-client/v7"
    +
    +c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    +index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
    +req := gotenberg.NewHTMLRequest(index)
    +req.WaitDelay(5.5)
    +dest := "result.pdf"
    +c.Store(req, dest)
    +
    + +

    PHP

    + +
    use TheCodingMachine\Gotenberg\Client;
    +use TheCodingMachine\Gotenberg\DocumentFactory;
    +use TheCodingMachine\Gotenberg\HTMLRequest;
    +use TheCodingMachine\Gotenberg\Request;
    +
    +$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
    +$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
    +$request = new HTMLRequest($index);
    +$request->setWaitDelay(5.5);
    +$dest = 'result.pdf';
    +$client->store($request, $dest);
    +
    + +

    Rpcc buffer size

    + +

    The API might return a 400 HTTP code with the message increase the Google Chrome rpcc buffer size.

    + +

    If so, you may increase this buffer size with a form field named googleChromeRpccBufferSize.

    + +

    It takes an int as value (e.g. 1048576 for 1 MB). +The hard limit is 100 MB and is defined by Google Chrome itself.

    + +
    +

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

    +
    + +

    cURL

    + +
    $ curl --request POST \
    +    --url http://localhost:3000/convert/html \
    +    --header 'Content-Type: multipart/form-data' \
    +    --form files=@index.html \
    +    --form googleChromeRpccBufferSize=1048576 \
    +    -o result.pdf
    +
    + +

    Go

    + +
    import "github.com/thecodingmachine/gotenberg-go-client/v7"
    +
    +c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    +index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
    +req := gotenberg.NewHTMLRequest(index)
    +req.GoogleChromeRpccBufferSize(1048576)
    +dest := "result.pdf"
    +c.Store(req, dest)
    +
    + +

    PHP

    + +
    use TheCodingMachine\Gotenberg\Client;
    +use TheCodingMachine\Gotenberg\DocumentFactory;
    +use TheCodingMachine\Gotenberg\HTMLRequest;
    +use TheCodingMachine\Gotenberg\Request;
    +
    +$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
    +$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
    +$request = new HTMLRequest($index);
    +$request->setGoogleChromeRpccBufferSize(1048576);
    +$dest = 'result.pdf';
    +$client->store($request, $dest);
     
    @@ -820,15 +946,13 @@ If not, some of the content of the page might be hidden.

    Go -
    import "github.com/thecodingmachine/gotenberg-go-client/v6"
    +
    import "github.com/thecodingmachine/gotenberg-go-client/v7"
     
    -func main() {
    -    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    -    req := gotenberg.NewURLRequest("https://google.com")
    -    req.Margins(gotenberg.NoMargins)
    -    dest := "result.pdf"
    -    c.Store(req, dest)
    -}
    +c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    +req := gotenberg.NewURLRequest("https://google.com")
    +req.Margins(gotenberg.NoMargins)
    +dest := "result.pdf"
    +c.Store(req, 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/v7"
    +
    +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);
     
    @@ -894,14 +1074,14 @@ in the file index.html. This function will convert a given markdown Go -
    import "github.com/thecodingmachine/gotenberg-go-client/v6"
    +
    import "github.com/thecodingmachine/gotenberg-go-client/v7"
     
    -func main() {
    -    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    -    req, _ := gotenberg.NewMarkdownRequest("index.html", "file.md")
    -    dest := "result.pdf"
    -    c.Store(req, dest)
    -}
    +c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    +index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
    +markdown, _ := gotenberg.NewDocumentFromPath("file.md", "/path/to/file")
    +req := gotenberg.NewMarkdownRequest(index, markdown)
    +dest := "result.pdf"
    +c.Store(req, dest)
     

    Go -
    import "github.com/thecodingmachine/gotenberg-go-client/v6"
    +
    import "github.com/thecodingmachine/gotenberg-go-client/v7"
     
    -func main() {
    -    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    -    req, _ := gotenberg.NewOfficeRequest("document.docx", "document2.docx")
    -    dest := "result.pdf"
    -    c.Store(req, dest)
    -}
    +c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    +doc, _ := gotenberg.NewDocumentFromPath("document.docx", "/path/to/file")
    +doc2, _ := gotenberg.NewDocumentFromPath("document2.docx", "/path/to/file")
    +req := gotenberg.NewOfficeRequest(doc, doc2)
    +dest := "result.pdf"
    +c.Store(req, dest)
     

    Go -
    import "github.com/thecodingmachine/gotenberg-go-client/v6"
    +
    import "github.com/thecodingmachine/gotenberg-go-client/v7"
     
    -func main() {
    -    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    -    req, _ := gotenberg.NewOfficeRequest("document.docx")
    -    req.Landscape(true)
    -    dest := "result.pdf"
    -    c.Store(req, dest)
    -}
    +c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    +doc, _ := gotenberg.NewDocumentFromPath("document.docx", "/path/to/file")
    +req := gotenberg.NewOfficeRequest(doc)
    +req.Landscape(true)
    +dest := "result.pdf"
    +c.Store(req, dest)
     

    Go

    -
    import "github.com/thecodingmachine/gotenberg-go-client/v6"
    +
    import "github.com/thecodingmachine/gotenberg-go-client/v7"
     
    -func main() {
    -    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    -    req, _ := gotenberg.NewMergeRequest("file.pdf", "file2.pdf")
    -    dest := "result.pdf"
    -    c.Store(req, dest)
    -}
    +c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    +pdf, _ := gotenberg.NewDocumentFromPath("file.pdf", "/path/to/file")
    +pdf2, _ := gotenberg.NewDocumentFromPath("file2.pdf", "/path/to/file")
    +req := gotenberg.NewMergeRequest(pdf, pdf2)
    +dest := "result.pdf"
    +c.Store(req, dest)
     

    Go

    -
    import "github.com/thecodingmachine/gotenberg-go-client/v6"
    +
    import "github.com/thecodingmachine/gotenberg-go-client/v7"
     
    -func main() {
    -    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    -    req, _ := gotenberg.NewHTMLRequest("index.html")
    -    req.WaitTimeout(2.5)
    -    resp, _ := c.Post(req)
    -}
    +c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    +index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
    +req := gotenberg.NewHTMLRequest(index)
    +req.WaitTimeout(2.5)
    +resp, _ := c.Post(req)
     

    Go -
    import "github.com/thecodingmachine/gotenberg-go-client/v6"
    +
    import "github.com/thecodingmachine/gotenberg-go-client/v7"
     
    -func main() {
    -    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    -    req, _ := gotenberg.NewHTMLRequest("index.html")
    -    req.WebhookURL("http://myapp.com/webhook/")
    -    resp, _ := c.Post(req)
    -}
    +c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    +index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
    +req := gotenberg.NewHTMLRequest(index)
    +req.WebhookURL("http://myapp.com/webhook/")
    +resp, _ := c.Post(req)
     

    environment variables section.

    -

    Examples

    - -

    cURL

    +cURL
    $ curl --request POST \
         --url http://localhost:3000/convert/html \
         --header 'Content-Type: multipart/form-data' \
         --form files=@index.html \
         --form webhookURL='http://myapp.com/webhook/' \
    -    --form webhookURLTimeout=2.5
    +    --form webhookURLTimeout=2.5
     
    -

    Go

    +Go

    -
    import "github.com/thecodingmachine/gotenberg-go-client/v6"
    +
    import "github.com/thecodingmachine/gotenberg-go-client/v7"
     
    -func main() {
    -    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    -    req, _ := gotenberg.NewHTMLRequest("index.html")
    -    req.WebhookURL("http://myapp.com/webhook/")
    -    req.WebhookURLTimeout(2.5)
    -    resp, _ := c.Post(req)
    -}
    +c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    +index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
    +req := gotenberg.NewHTMLRequest(index)
    +req.WebhookURL("http://myapp.com/webhook/")
    +req.WebhookURLTimeout(2.5)
    +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');
    +$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
     $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/v7"
    +
    +c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    +index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
    +req := gotenberg.NewHTMLRequest(index)
    +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', '/path/to/file');
    +$request = new HTMLRequest($index);
    +$request->setWebhookURL('http://myapp.com/webhook/');
    +$request->addWebhookURLHTTPHeader('Your-Header', 'Foo');
    +$resp = $client->post($request);
     
    @@ -1395,14 +1625,13 @@ Otherwise a random filename is used.

    Go -
    import "github.com/thecodingmachine/gotenberg-go-client/v6"
    +
    import "github.com/thecodingmachine/gotenberg-go-client/v7"
     
    -func main() {
    -    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    -    req, _ := gotenberg.NewHTMLRequest("index.html")
    -    req.ResultFilename("foo.pdf")
    -    resp, _ := c.Post(req)
    -}
    +c := &gotenberg.Client{Hostname: "http://localhost:3000"}
    +index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
    +req := gotenberg.NewHTMLRequest(index)
    +req.ResultFilename("foo.pdf")
    +resp, _ := c.Post(req)
     

    Office and Merge endpoints will start respectively as many LibreOffice (unoconv) and PDTk -instances are there are requests. The limitation here is the available memory and CPU usage.

    +instances as there are requests. The limitation here is the available memory and CPU usage.

    On another hand, for the HTML, URL and Markdown endpoints, the API does only 6 conversions in parallel. Indeed, Google Chrome misbehaves if there are too many concurrent conversions.

    @@ -1467,14 +1696,14 @@ if the API is under heavy load.

    For instance, using the following Docker Compose file:

    -
    version: '3'
    +
    version: '3'
     
    -services:
    +services:
     
       # your others services
     
    -  gotenberg:
    -    image: thecodingmachine/gotenberg:6
    +  gotenberg:
    +    image: thecodingmachine/gotenberg:6
     

    You may now launch your services using:

    @@ -1525,17 +1754,20 @@ restart your Gotenberg instances from time to time to ensure a nominal behaviour

    Links

    -
      +

      + Gotenberg logo +

      + + -

      Psst: TheCodingMachine is always looking for talented coders.

      - - + diff --git a/docs/theme/gotenberg/views/index.html b/docs/theme/gotenberg/views/index.html index 0350c3ae..aa43f10f 100644 --- a/docs/theme/gotenberg/views/index.html +++ b/docs/theme/gotenberg/views/index.html @@ -34,7 +34,7 @@
      - Gotenberg logo + Gotenberg logo {{.Title}} {{.Subtitle}}
      @@ -58,7 +58,8 @@ {{.Content}}
      {{end}} - +
      diff --git a/internal/app/xhttp/handler.go b/internal/app/xhttp/handler.go index dda5d68e..92cf22ea 100644 --- a/internal/app/xhttp/handler.go +++ b/internal/app/xhttp/handler.go @@ -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 } diff --git a/internal/app/xhttp/handler_test.go b/internal/app/xhttp/handler_test.go index e61d0985..7e141d53 100644 --- a/internal/app/xhttp/handler_test.go +++ b/internal/app/xhttp/handler_test.go @@ -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)) } diff --git a/internal/app/xhttp/middleware.go b/internal/app/xhttp/middleware.go index 5e5ca0b3..4049b9ac 100644 --- a/internal/app/xhttp/middleware.go +++ b/internal/app/xhttp/middleware.go @@ -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) } } diff --git a/internal/app/xhttp/option.go b/internal/app/xhttp/option.go index a40d4ea4..5a9a3759 100644 --- a/internal/app/xhttp/option.go +++ b/internal/app/xhttp/option.go @@ -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() 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/arg.go b/internal/app/xhttp/pkg/resource/arg.go index 3372da46..80063925 100644 --- a/internal/app/xhttp/pkg/resource/arg.go +++ b/internal/app/xhttp/pkg/resource/arg.go @@ -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 +} diff --git a/internal/app/xhttp/pkg/resource/arg_test.go b/internal/app/xhttp/pkg/resource/arg_test.go index f42fd1d0..230add46 100644 --- a/internal/app/xhttp/pkg/resource/arg_test.go +++ b/internal/app/xhttp/pkg/resource/arg_test.go @@ -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) +} 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/app/xhttp/xhttp.go b/internal/app/xhttp/xhttp.go index 7324997e..02db10e1 100644 --- a/internal/app/xhttp/xhttp.go +++ b/internal/app/xhttp/xhttp.go @@ -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 } diff --git a/internal/app/xhttp/xhttp_test.go b/internal/app/xhttp/xhttp_test.go index 685fb0a1..f4fe3df0 100644 --- a/internal/app/xhttp/xhttp_test.go +++ b/internal/app/xhttp/xhttp_test.go @@ -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, "/") +} diff --git a/internal/pkg/conf/conf.go b/internal/pkg/conf/conf.go index d0aca24a..45d1bce1 100644 --- a/internal/pkg/conf/conf.go +++ b/internal/pkg/conf/conf.go @@ -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 +} diff --git a/internal/pkg/conf/conf_test.go b/internal/pkg/conf/conf_test.go index 262aa079..678ac2aa 100644 --- a/internal/pkg/conf/conf_test.go +++ b/internal/pkg/conf/conf_test.go @@ -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()) } diff --git a/internal/pkg/printer/chrome.go b/internal/pkg/printer/chrome.go index 41b4c99d..4fe79d33 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,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 = "" 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 { diff --git a/internal/pkg/printer/merge.go b/internal/pkg/printer/merge.go index b8d4fa5b..bc663b3d 100644 --- a/internal/pkg/printer/merge.go +++ b/internal/pkg/printer/merge.go @@ -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 diff --git a/internal/pkg/printer/office.go b/internal/pkg/printer/office.go index e0f4f20b..020e5688 100644 --- a/internal/pkg/printer/office.go +++ b/internal/pkg/printer/office.go @@ -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, diff --git a/internal/pkg/xassert/string.go b/internal/pkg/xassert/string.go index 540eee9b..d59bea8a 100644 --- a/internal/pkg/xassert/string.go +++ b/internal/pkg/xassert/string.go @@ -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)) ) diff --git a/internal/pkg/xassert/string_test.go b/internal/pkg/xassert/string_test.go index 87ad8065..fb1c0d4e 100644 --- a/internal/pkg/xassert/string_test.go +++ b/internal/pkg/xassert/string_test.go @@ -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) +} diff --git a/scripts/publish.sh b/scripts/publish.sh index 009fce2d..74e0073d 100755 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -4,7 +4,7 @@ set -e GOLANG_VERSION="$1" TINI_VERSION="$2" -DOCKER_REPOSITORY="$3" +DOCKER_REGISTRY="$3" VERSION="$4" DOCKER_USER="$5" DOCKER_PASSWORD="$6" @@ -23,13 +23,13 @@ fi docker build \ --build-arg VERSION=${VERSION} \ --build-arg TINI_VERSION=${TINI_VERSION} \ - -t ${DOCKER_REPOSITORY}/gotenberg:latest \ - -t ${DOCKER_REPOSITORY}/gotenberg:${SEMVER[0]} \ - -t ${DOCKER_REPOSITORY}/gotenberg:${SEMVER[0]}.${SEMVER[1]} \ - -t ${DOCKER_REPOSITORY}/gotenberg:${SEMVER[0]}.${SEMVER[1]}.${SEMVER[2]} \ + -t ${DOCKER_REGISTRY}/gotenberg:latest \ + -t ${DOCKER_REGISTRY}/gotenberg:${SEMVER[0]} \ + -t ${DOCKER_REGISTRY}/gotenberg:${SEMVER[0]}.${SEMVER[1]} \ + -t ${DOCKER_REGISTRY}/gotenberg:${SEMVER[0]}.${SEMVER[1]}.${SEMVER[2]} \ -f build/package/Dockerfile . -docker push "${DOCKER_REPOSITORY}/gotenberg:latest" -docker push "${DOCKER_REPOSITORY}/gotenberg:${SEMVER[0]}" -docker push "${DOCKER_REPOSITORY}/gotenberg:${SEMVER[0]}.${SEMVER[1]}" -docker push "${DOCKER_REPOSITORY}/gotenberg:${SEMVER[0]}.${SEMVER[1]}.${SEMVER[2]}" \ No newline at end of file +docker push "${DOCKER_REGISTRY}/gotenberg:latest" +docker push "${DOCKER_REGISTRY}/gotenberg:${SEMVER[0]}" +docker push "${DOCKER_REGISTRY}/gotenberg:${SEMVER[0]}.${SEMVER[1]}" +docker push "${DOCKER_REGISTRY}/gotenberg:${SEMVER[0]}.${SEMVER[1]}.${SEMVER[2]}" \ No newline at end of file diff --git a/scripts/tests.sh b/scripts/tests.sh index 3824cc6d..814d50f0 100755 --- a/scripts/tests.sh +++ b/scripts/tests.sh @@ -2,15 +2,15 @@ set -e -DOCKER_REPOSITORY="$1" +DOCKER_REGISTRY="$1" CODE_COVERAGE="$2" touch "$PWD/coverage.txt" chmod 777 "$PWD/coverage.txt" -docker build -t "$DOCKER_REPOSITORY/gotenberg:tests" -f build/tests/Dockerfile . +docker build -t "$DOCKER_REGISTRY/gotenberg:tests" -f build/tests/Dockerfile . if [ "$CODE_COVERAGE" = "1" ]; then - docker run --rm -e "CODE_COVERAGE=$CODE_COVERAGE" -v "$PWD/coverage.txt:/gotenberg/tests/coverage.txt" "$DOCKER_REPOSITORY/gotenberg:tests" + docker run --rm -e "CODE_COVERAGE=$CODE_COVERAGE" -v "$PWD/coverage.txt:/gotenberg/tests/coverage.txt" "$DOCKER_REGISTRY/gotenberg:tests" else - docker run --rm -e "CODE_COVERAGE=$CODE_COVERAGE" "$DOCKER_REPOSITORY/gotenberg:tests" + docker run --rm -e "CODE_COVERAGE=$CODE_COVERAGE" "$DOCKER_REGISTRY/gotenberg:tests" fi \ No newline at end of file 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