├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature-request.md └── workflows │ └── aws.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── Makefile ├── README.md ├── _ ├── amazon │ ├── code │ │ └── .gitignore │ ├── events │ │ └── example.com.json │ ├── handlers │ │ └── index.js │ └── template.yml └── ansible │ ├── Makefile │ ├── README.md │ ├── ansible.cfg │ ├── inventory.ini │ └── plays │ └── chromium.yml ├── bin ├── aws.tar.br ├── chromium.br └── swiftshader.tar.br ├── package.json ├── source ├── hooks │ ├── adblock.ts │ ├── agent.ts │ ├── chrome.ts │ ├── languages.ts │ ├── permissions.ts │ ├── timezone.ts │ ├── webdriver.ts │ └── window.ts ├── index.ts └── puppeteer │ └── lib │ ├── Browser.ts │ ├── BrowserContext.ts │ ├── ElementHandle.ts │ ├── FrameManager.ts │ └── Page.ts ├── tsconfig.json └── typings └── chrome-aws-lambda.d.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.json] 14 | indent_size = 2 15 | indent_style = space 16 | 17 | [Makefile] 18 | indent_size = 4 19 | indent_style = tab 20 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: alixaxel 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Standard Bug Report 4 | title: "[BUG]" 5 | labels: bug 6 | --- 7 | 8 | 12 | 13 | ## Environment 14 | * `chrome-aws-lambda` Version: 15 | * `puppeteer` / `puppeteer-core` Version: 16 | * OS: 17 | * Node.js Version: 18 | * Lambda / GCF Runtime: 19 | 20 | ## Expected Behavior 21 | 22 | 23 | 24 | ## Current Behavior 25 | 26 | 27 | 28 | ## Steps to Reproduce 29 | 30 | 31 | 32 | 66 | 67 | ## Possible Solution 68 | 69 | 70 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an Idea or Improvement 4 | title: "[REQUEST]" 5 | labels: enhancement 6 | --- 7 | 8 | ## What would you like to have implemented? 9 | 10 | 11 | 12 | ## Why would it be useful? 13 | 14 | 15 | -------------------------------------------------------------------------------- /.github/workflows/aws.yml: -------------------------------------------------------------------------------- 1 | name: AWS Lambda CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | name: Build Lambda Layer 12 | runs-on: ubuntu-20.04 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: 14.x 21 | 22 | - name: Install Packages 23 | run: npm install 24 | 25 | - name: Create Lambda Layer 26 | run: make chrome_aws_lambda.zip 27 | 28 | - name: Upload Layer Artifact 29 | uses: actions/upload-artifact@v2 30 | with: 31 | name: chrome_aws_lambda 32 | path: chrome_aws_lambda.zip 33 | 34 | execute: 35 | name: Lambda (Node ${{ matrix.version }}.x) 36 | needs: build 37 | runs-on: ubuntu-20.04 38 | strategy: 39 | matrix: 40 | event: 41 | - example.com 42 | version: 43 | - 10 44 | - 12 45 | - 14 46 | steps: 47 | - name: Checkout 48 | uses: actions/checkout@v2 49 | 50 | - name: Setup AWS SAM CLI 51 | uses: aws-actions/setup-sam@v0 52 | 53 | - name: Download Layer Artifact 54 | uses: actions/download-artifact@v2 55 | with: 56 | name: chrome_aws_lambda 57 | 58 | - name: Provision Layer 59 | run: unzip chrome_aws_lambda.zip -d _/amazon/code 60 | 61 | - name: Invoke Lambda on SAM 62 | run: sam local invoke --template _/amazon/template.yml --event _/amazon/events/${{ matrix.event }}.json node${{ matrix.version }} 2>&1 | (grep 'Error' && exit 1 || exit 0) 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .fonts 2 | .idea 3 | *.log 4 | *.pem 5 | *.pem.pub 6 | *.zip 7 | bin/chromium-*.br 8 | build 9 | node_modules 10 | nodejs 11 | package-lock.json 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | _ 2 | .fonts 3 | .idea 4 | *.zip 5 | Dockerfile 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Alix Axel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean 2 | 3 | clean: 4 | rm -f $(lastword $(MAKECMDGOALS)) 5 | 6 | .fonts.zip: 7 | zip -9 --filesync --move --recurse-paths .fonts.zip .fonts/ 8 | 9 | %.zip: 10 | npm install --no-fund --no-package-lock --no-shrinkwrap 11 | mkdir -p nodejs/ 12 | npm install --prefix nodejs/ lambdafs@~2.0.3 puppeteer-core@~10.1.0 --no-bin-links --no-fund --no-optional --no-package-lock --no-save --no-shrinkwrap 13 | npm pack 14 | mkdir -p nodejs/node_modules/chrome-aws-lambda/ 15 | tar --directory nodejs/node_modules/chrome-aws-lambda/ --extract --file chrome-aws-lambda-*.tgz --strip-components=1 16 | rm chrome-aws-lambda-*.tgz 17 | mkdir -p $(dir $@) 18 | zip -9 --filesync --move --recurse-paths $@ nodejs/ 19 | 20 | .DEFAULT_GOAL := chrome_aws_lambda.zip 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chrome-aws-lambda 2 | 3 | [![chrome-aws-lambda](https://img.shields.io/npm/v/chrome-aws-lambda.svg?style=for-the-badge)](https://www.npmjs.com/package/chrome-aws-lambda) 4 | [![TypeScript](https://img.shields.io/npm/types/chrome-aws-lambda?style=for-the-badge)](https://www.typescriptlang.org/dt/search?search=chrome-aws-lambda) 5 | [![Chromium](https://img.shields.io/badge/chromium-44_MB-brightgreen.svg?style=for-the-badge)](bin/) 6 | [![Donate](https://img.shields.io/badge/donate-paypal-orange.svg?style=for-the-badge)](https://paypal.me/alixaxel) 7 | 8 | Chromium Binary for AWS Lambda and Google Cloud Functions 9 | 10 | ## Install 11 | 12 | ```shell 13 | npm install chrome-aws-lambda --save-prod 14 | ``` 15 | 16 | This will ship with appropriate binary for the latest stable release of [`puppeteer`](https://github.com/GoogleChrome/puppeteer) (usually updated within a few days). 17 | 18 | You also need to install the corresponding version of `puppeteer-core` (or `puppeteer`): 19 | 20 | ```shell 21 | npm install puppeteer-core --save-prod 22 | ``` 23 | 24 | If you wish to install an older version of Chromium, take a look at [Versioning](https://github.com/alixaxel/chrome-aws-lambda#versioning). 25 | 26 | ## Usage 27 | 28 | This package works with all the currently supported AWS Lambda Node.js runtimes out of the box. 29 | 30 | ```javascript 31 | const chromium = require('chrome-aws-lambda'); 32 | 33 | exports.handler = async (event, context, callback) => { 34 | let result = null; 35 | let browser = null; 36 | 37 | try { 38 | browser = await chromium.puppeteer.launch({ 39 | args: chromium.args, 40 | defaultViewport: chromium.defaultViewport, 41 | executablePath: await chromium.executablePath, 42 | headless: chromium.headless, 43 | ignoreHTTPSErrors: true, 44 | }); 45 | 46 | let page = await browser.newPage(); 47 | 48 | await page.goto(event.url || 'https://example.com'); 49 | 50 | result = await page.title(); 51 | } catch (error) { 52 | return callback(error); 53 | } finally { 54 | if (browser !== null) { 55 | await browser.close(); 56 | } 57 | } 58 | 59 | return callback(null, result); 60 | }; 61 | ``` 62 | 63 | ### Usage with Playwright 64 | 65 | ```javascript 66 | const chromium = require('chrome-aws-lambda'); 67 | const playwright = require('playwright-core'); 68 | 69 | (async () => { 70 | const browser = await playwright.chromium.launch({ 71 | args: chromium.args, 72 | executablePath: await chromium.executablePath, 73 | headless: chromium.headless, 74 | }); 75 | 76 | // ... 77 | 78 | await browser.close(); 79 | })(); 80 | ``` 81 | 82 | You should allocate at least 512 MB of RAM to your Lambda, however 1600 MB (or more) is recommended. 83 | 84 | ### Running Locally 85 | 86 | Please refer to the [Local Development Wiki page](https://github.com/alixaxel/chrome-aws-lambda/wiki/HOWTO:-Local-Development) for instructions and troubleshooting. 87 | 88 | ## API 89 | 90 | | Method / Property | Returns | Description | 91 | | ----------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | 92 | | `font(url)` | `{?Promise}` | Provisions a custom font and returns its basename. | 93 | | `args` | `{!Array}` | Provides a list of recommended additional [Chromium flags](https://github.com/GoogleChrome/chrome-launcher/blob/master/docs/chrome-flags-for-tools.md). | 94 | | `defaultViewport` | `{!Object}` | Returns more sensible default viewport settings. | 95 | | `executablePath` | `{?Promise}` | Returns the path the Chromium binary was extracted to. | 96 | | `headless` | `{!boolean}` | Returns `true` if we are running on AWS Lambda or GCF. | 97 | | `puppeteer` | `{!Object}` | Overloads `puppeteer` and returns the resolved package. | 98 | 99 | ## Fonts 100 | 101 | The Amazon Linux 2 AWS Lambda runtime is no longer provisioned with any font faces. 102 | 103 | Because of this, this package ships with [Open Sans](https://fonts.google.com/specimen/Open+Sans), which supports the following scripts: 104 | 105 | * Latin 106 | * Greek 107 | * Cyrillic 108 | 109 | To provision additional fonts, simply call the `font()` method with an absolute path or URL: 110 | 111 | ```typescript 112 | await chromium.font('/var/task/fonts/NotoColorEmoji.ttf'); 113 | // or 114 | await chromium.font('https://raw.githack.com/googlei18n/noto-emoji/master/fonts/NotoColorEmoji.ttf'); 115 | ``` 116 | 117 | > `Noto Color Emoji` (or similar) is needed if you want to [render emojis](https://getemoji.com/). 118 | 119 | > For URLs, it's recommended that you use a CDN, like [raw.githack.com](https://raw.githack.com/) or [gitcdn.xyz](https://gitcdn.xyz/). 120 | 121 | This method should be invoked _before_ launching Chromium. 122 | 123 | > On non-serverless environments, the `font()` method is a no-op to avoid polluting the user space. 124 | 125 | --- 126 | 127 | Alternatively, it's also possible to provision fonts via AWS Lambda Layers. 128 | 129 | Simply create a directory named `.fonts` and place any font faces you want there: 130 | 131 | ``` 132 | .fonts 133 | ├── NotoColorEmoji.ttf 134 | └── Roboto.ttf 135 | ``` 136 | 137 | Afterwards, you just need to ZIP the directory and upload it as a AWS Lambda Layer: 138 | 139 | ```shell 140 | zip -9 --filesync --move --recurse-paths .fonts.zip .fonts/ 141 | ``` 142 | 143 | ## Overloading 144 | 145 | Since version `8.0.0`, it's possible to [overload puppeteer](/typings/chrome-aws-lambda.d.ts) with the following convenient API: 146 | 147 | ```typescript 148 | interface Browser { 149 | defaultPage(...hooks: ((page: Page) => Promise)[]) 150 | newPage(...hooks: ((page: Page) => Promise)[]) 151 | } 152 | 153 | interface BrowserContext { 154 | defaultPage(...hooks: ((page: Page) => Promise)[]) 155 | newPage(...hooks: ((page: Page) => Promise)[]) 156 | } 157 | 158 | interface Page { 159 | block(patterns: string[]) 160 | clear(selector: string) 161 | clickAndWaitForNavigation(selector: string, options?: WaitForOptions) 162 | clickAndWaitForRequest(selector: string, predicate: string | RegExp, options?: WaitTimeoutOptions) 163 | clickAndWaitForRequest(selector: string, predicate: ((request: HTTPRequest) => boolean | Promise), options?: WaitTimeoutOptions) 164 | clickAndWaitForResponse(selector: string, predicate: string | RegExp, options?: WaitTimeoutOptions) 165 | clickAndWaitForResponse(selector: string, predicate: ((request: HTTPResponse) => boolean | Promise), options?: WaitTimeoutOptions) 166 | count(selector: string) 167 | exists(selector: string) 168 | fillFormByLabel(selector: string, data: Record) 169 | fillFormByName(selector: string, data: Record) 170 | fillFormBySelector(selector: string, data: Record) 171 | fillFormByXPath(selector: string, data: Record) 172 | number(selector: string, decimal?: string, property?: string) 173 | selectByLabel(selector: string, ...values: string[]) 174 | string(selector: string, property?: string) 175 | waitForInflightRequests(requests?: number, alpha: number, omega: number, options?: WaitTimeoutOptions) 176 | waitForText(predicate: string, options?: WaitTimeoutOptions) 177 | waitUntilVisible(selector: string, options?: WaitTimeoutOptions) 178 | waitWhileVisible(selector: string, options?: WaitTimeoutOptions) 179 | withTracing(options: TracingOptions, callback: (page: Page) => Promise) 180 | } 181 | 182 | interface Frame { 183 | clear(selector: string) 184 | clickAndWaitForNavigation(selector: string, options?: WaitForOptions) 185 | clickAndWaitForRequest(selector: string, predicate: string | RegExp, options?: WaitTimeoutOptions) 186 | clickAndWaitForRequest(selector: string, predicate: ((request: HTTPRequest) => boolean | Promise), options?: WaitTimeoutOptions) 187 | clickAndWaitForResponse(selector: string, predicate: string | RegExp, options?: WaitTimeoutOptions) 188 | clickAndWaitForResponse(selector: string, predicate: ((request: HTTPResponse) => boolean | Promise), options?: WaitTimeoutOptions) 189 | count(selector: string) 190 | exists(selector: string) 191 | fillFormByLabel(selector: string, data: Record) 192 | fillFormByName(selector: string, data: Record) 193 | fillFormBySelector(selector: string, data: Record) 194 | fillFormByXPath(selector: string, data: Record) 195 | number(selector: string, decimal?: string, property?: string) 196 | selectByLabel(selector: string, ...values: string[]) 197 | string(selector: string, property?: string) 198 | waitForText(predicate: string, options?: WaitTimeoutOptions) 199 | waitUntilVisible(selector: string, options?: WaitTimeoutOptions) 200 | waitWhileVisible(selector: string, options?: WaitTimeoutOptions) 201 | } 202 | 203 | interface ElementHandle { 204 | clear() 205 | clickAndWaitForNavigation(options?: WaitForOptions) 206 | clickAndWaitForRequest(predicate: string | RegExp, options?: WaitTimeoutOptions) 207 | clickAndWaitForRequest(predicate: ((request: HTTPRequest) => boolean | Promise), options?: WaitTimeoutOptions) 208 | clickAndWaitForResponse(predicate: string | RegExp, options?: WaitTimeoutOptions) 209 | clickAndWaitForResponse(predicate: ((request: HTTPResponse) => boolean | Promise), options?: WaitTimeoutOptions) 210 | fillFormByLabel(data: Record) 211 | fillFormByName(data: Record) 212 | fillFormBySelector(data: Record) 213 | fillFormByXPath(data: Record) 214 | getInnerHTML() 215 | getInnerText() 216 | number(decimal?: string, property?: string) 217 | selectByLabel(...values: string[]) 218 | string(property?: string) 219 | } 220 | ``` 221 | 222 | To enable this behavior, simply call the `puppeteer` property exposed by this package. 223 | 224 | > Refer to the [TypeScript typings](/typings/chrome-aws-lambda.d.ts) for general documentation. 225 | 226 | ## Page Hooks 227 | 228 | When overloaded, you can specify a list of hooks to automatically apply to pages. 229 | 230 | For instance, to remove the `Headless` substring from the user agent: 231 | 232 | ```typescript 233 | async function replaceUserAgent(page: Page): Promise { 234 | let value = await page.browser().userAgent(); 235 | 236 | if (value.includes('Headless') === true) { 237 | await page.setUserAgent(value.replace('Headless', '')); 238 | } 239 | 240 | return page; 241 | } 242 | ``` 243 | 244 | And then simply pass that page hook to `defaultPage()` or `newPage()`: 245 | 246 | ```typescript 247 | let page = await browser.defaultPage(replaceUserAgent); 248 | ``` 249 | 250 | > Additional bundled page hooks can be found on [`/build/hooks`](/source/hooks). 251 | 252 | ## Versioning 253 | 254 | This package is versioned based on the underlying `puppeteer` minor version: 255 | 256 | | `puppeteer` Version | `chrome-aws-lambda` Version | Chromium Revision | 257 | | ------------------- | --------------------------------- | ---------------------------------------------------- | 258 | | `10.1.*` | `npm i chrome-aws-lambda@~10.1.0` | [`884014`](https://crrev.com/884014) (`92.0.4512.0`) | 259 | | `10.0.*` | `npm i chrome-aws-lambda@~10.0.0` | [`884014`](https://crrev.com/884014) (`92.0.4512.0`) | 260 | | `9.1.*` | `npm i chrome-aws-lambda@~9.1.0` | [`869685`](https://crrev.com/869685) (`91.0.4469.0`) | 261 | | `9.0.*` | `npm i chrome-aws-lambda@~9.0.0` | [`869685`](https://crrev.com/869685) (`91.0.4469.0`) | 262 | | `8.0.*` | `npm i chrome-aws-lambda@~8.0.2` | [`856583`](https://crrev.com/856583) (`90.0.4427.0`) | 263 | | `7.0.*` | `npm i chrome-aws-lambda@~7.0.0` | [`848005`](https://crrev.com/848005) (`90.0.4403.0`) | 264 | | `6.0.*` | `npm i chrome-aws-lambda@~6.0.0` | [`843427`](https://crrev.com/843427) (`89.0.4389.0`) | 265 | | `5.5.*` | `npm i chrome-aws-lambda@~5.5.0` | [`818858`](https://crrev.com/818858) (`88.0.4298.0`) | 266 | | `5.4.*` | `npm i chrome-aws-lambda@~5.4.0` | [`809590`](https://crrev.com/809590) (`87.0.4272.0`) | 267 | | `5.3.*` | `npm i chrome-aws-lambda@~5.3.1` | [`800071`](https://crrev.com/800071) (`86.0.4240.0`) | 268 | | `5.2.*` | `npm i chrome-aws-lambda@~5.2.1` | [`782078`](https://crrev.com/782078) (`85.0.4182.0`) | 269 | | `5.1.*` | `npm i chrome-aws-lambda@~5.1.0` | [`768783`](https://crrev.com/768783) (`84.0.4147.0`) | 270 | | `5.0.*` | `npm i chrome-aws-lambda@~5.0.0` | [`756035`](https://crrev.com/756035) (`83.0.4103.0`) | 271 | | `3.1.*` | `npm i chrome-aws-lambda@~3.1.1` | [`756035`](https://crrev.com/756035) (`83.0.4103.0`) | 272 | | `3.0.*` | `npm i chrome-aws-lambda@~3.0.4` | [`737027`](https://crrev.com/737027) (`81.0.4044.0`) | 273 | | `2.1.*` | `npm i chrome-aws-lambda@~2.1.1` | [`722234`](https://crrev.com/722234) (`80.0.3987.0`) | 274 | | `2.0.*` | `npm i chrome-aws-lambda@~2.0.2` | [`705776`](https://crrev.com/705776) (`79.0.3945.0`) | 275 | | `1.20.*` | `npm i chrome-aws-lambda@~1.20.4` | [`686378`](https://crrev.com/686378) (`78.0.3882.0`) | 276 | | `1.19.*` | `npm i chrome-aws-lambda@~1.19.0` | [`674921`](https://crrev.com/674921) (`77.0.3844.0`) | 277 | | `1.18.*` | `npm i chrome-aws-lambda@~1.18.1` | [`672088`](https://crrev.com/672088) (`77.0.3835.0`) | 278 | | `1.18.*` | `npm i chrome-aws-lambda@~1.18.0` | [`669486`](https://crrev.com/669486) (`77.0.3827.0`) | 279 | | `1.17.*` | `npm i chrome-aws-lambda@~1.17.1` | [`662092`](https://crrev.com/662092) (`76.0.3803.0`) | 280 | | `1.16.*` | `npm i chrome-aws-lambda@~1.16.1` | [`656675`](https://crrev.com/656675) (`76.0.3786.0`) | 281 | | `1.15.*` | `npm i chrome-aws-lambda@~1.15.1` | [`650583`](https://crrev.com/650583) (`75.0.3765.0`) | 282 | | `1.14.*` | `npm i chrome-aws-lambda@~1.14.0` | [`641577`](https://crrev.com/641577) (`75.0.3738.0`) | 283 | | `1.13.*` | `npm i chrome-aws-lambda@~1.13.0` | [`637110`](https://crrev.com/637110) (`74.0.3723.0`) | 284 | | `1.12.*` | `npm i chrome-aws-lambda@~1.12.2` | [`624492`](https://crrev.com/624492) (`73.0.3679.0`) | 285 | | `1.11.*` | `npm i chrome-aws-lambda@~1.11.2` | [`609904`](https://crrev.com/609904) (`72.0.3618.0`) | 286 | | `1.10.*` | `npm i chrome-aws-lambda@~1.10.1` | [`604907`](https://crrev.com/604907) (`72.0.3582.0`) | 287 | | `1.9.*` | `npm i chrome-aws-lambda@~1.9.1` | [`594312`](https://crrev.com/594312) (`71.0.3563.0`) | 288 | | `1.8.*` | `npm i chrome-aws-lambda@~1.8.0` | [`588429`](https://crrev.com/588429) (`71.0.3542.0`) | 289 | | `1.7.*` | `npm i chrome-aws-lambda@~1.7.0` | [`579032`](https://crrev.com/579032) (`70.0.3508.0`) | 290 | | `1.6.*` | `npm i chrome-aws-lambda@~1.6.3` | [`575458`](https://crrev.com/575458) (`69.0.3494.0`) | 291 | | `1.5.*` | `npm i chrome-aws-lambda@~1.5.0` | [`564778`](https://crrev.com/564778) (`69.0.3452.0`) | 292 | | `1.4.*` | `npm i chrome-aws-lambda@~1.4.0` | [`555668`](https://crrev.com/555668) (`68.0.3419.0`) | 293 | | `1.3.*` | `npm i chrome-aws-lambda@~1.3.0` | [`549031`](https://crrev.com/549031) (`67.0.3391.0`) | 294 | | `1.2.*` | `npm i chrome-aws-lambda@~1.2.0` | [`543305`](https://crrev.com/543305) (`67.0.3372.0`) | 295 | | `1.1.*` | `npm i chrome-aws-lambda@~1.1.0` | [`536395`](https://crrev.com/536395) (`66.0.3347.0`) | 296 | | `1.0.*` | `npm i chrome-aws-lambda@~1.0.0` | [`526987`](https://crrev.com/526987) (`65.0.3312.0`) | 297 | | `0.13.*` | `npm i chrome-aws-lambda@~0.13.0` | [`515411`](https://crrev.com/515411) (`64.0.3264.0`) | 298 | 299 | Patch versions are reserved for bug fixes in `chrome-aws-lambda` and general maintenance. 300 | 301 | ## Compiling 302 | 303 | To compile your own version of Chromium check the [Ansible playbook instructions](_/ansible). 304 | 305 | ## AWS Lambda Layer 306 | 307 | [Lambda Layers](https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html) is a new convenient way to manage common dependencies between different Lambda Functions. 308 | 309 | The following set of (Linux) commands will create a layer of this package alongside `puppeteer-core`: 310 | 311 | ```shell 312 | git clone --depth=1 https://github.com/alixaxel/chrome-aws-lambda.git && \ 313 | cd chrome-aws-lambda && \ 314 | make chrome_aws_lambda.zip 315 | ``` 316 | 317 | The above will create a `chrome-aws-lambda.zip` file, which can be uploaded to your Layers console. 318 | 319 | Alternatively, you can also download the layer artifact from one of our [CI workflow runs](https://github.com/alixaxel/chrome-aws-lambda/actions/workflows/aws.yml?query=is%3Asuccess+branch%3Amaster). 320 | 321 | ## Google Cloud Functions 322 | 323 | Since version `1.11.2`, it's also possible to use this package on Google/Firebase Cloud Functions. 324 | 325 | According to our benchmarks, it's 40% to 50% faster than using the off-the-shelf `puppeteer` bundle. 326 | 327 | ## Compression 328 | 329 | The Chromium binary is compressed using the Brotli algorithm. 330 | 331 | This allows us to get the best compression ratio and faster decompression times. 332 | 333 | | File | Algorithm | Level | Bytes | MiB | % | Inflation | 334 | | ------------- | --------- | ----- | --------- | --------- | ---------- | ---------- | 335 | | `chromium` | - | - | 136964856 | 130.62 | - | - | 336 | | `chromium.gz` | Gzip | 1 | 51662087 | 49.27 | 62.28% | 1.035s | 337 | | `chromium.gz` | Gzip | 2 | 50438352 | 48.10 | 63.17% | 1.016s | 338 | | `chromium.gz` | Gzip | 3 | 49428459 | 47.14 | 63.91% | 0.968s | 339 | | `chromium.gz` | Gzip | 4 | 47873978 | 45.66 | 65.05% | 0.950s | 340 | | `chromium.gz` | Gzip | 5 | 46929422 | 44.76 | 65.74% | 0.938s | 341 | | `chromium.gz` | Gzip | 6 | 46522529 | 44.37 | 66.03% | 0.919s | 342 | | `chromium.gz` | Gzip | 7 | 46406406 | 44.26 | 66.12% | 0.917s | 343 | | `chromium.gz` | Gzip | 8 | 46297917 | 44.15 | 66.20% | 0.916s | 344 | | `chromium.gz` | Gzip | 9 | 46270972 | 44.13 | 66.22% | 0.968s | 345 | | `chromium.gz` | Zopfli | 10 | 45089161 | 43.00 | 67.08% | 0.919s | 346 | | `chromium.gz` | Zopfli | 20 | 45085868 | 43.00 | 67.08% | 0.919s | 347 | | `chromium.gz` | Zopfli | 30 | 45085003 | 43.00 | 67.08% | 0.925s | 348 | | `chromium.gz` | Zopfli | 40 | 45084328 | 43.00 | 67.08% | 0.921s | 349 | | `chromium.gz` | Zopfli | 50 | 45084098 | 43.00 | 67.08% | 0.935s | 350 | | `chromium.br` | Brotli | 0 | 55401211 | 52.83 | 59.55% | 0.778s | 351 | | `chromium.br` | Brotli | 1 | 54429523 | 51.91 | 60.26% | 0.757s | 352 | | `chromium.br` | Brotli | 2 | 46436126 | 44.28 | 66.10% | 0.659s | 353 | | `chromium.br` | Brotli | 3 | 46122033 | 43.99 | 66.33% | 0.616s | 354 | | `chromium.br` | Brotli | 4 | 45050239 | 42.96 | 67.11% | 0.692s | 355 | | `chromium.br` | Brotli | 5 | 40813510 | 38.92 | 70.20% | **0.598s** | 356 | | `chromium.br` | Brotli | 6 | 40116951 | 38.26 | 70.71% | 0.601s | 357 | | `chromium.br` | Brotli | 7 | 39302281 | 37.48 | 71.30% | 0.615s | 358 | | `chromium.br` | Brotli | 8 | 39038303 | 37.23 | 71.50% | 0.668s | 359 | | `chromium.br` | Brotli | 9 | 38853994 | 37.05 | 71.63% | 0.673s | 360 | | `chromium.br` | Brotli | 10 | 36090087 | 34.42 | 73.65% | 0.765s | 361 | | `chromium.br` | Brotli | 11 | 34820408 | **33.21** | **74.58%** | 0.712s | 362 | 363 | ## License 364 | 365 | MIT 366 | -------------------------------------------------------------------------------- /_/amazon/code/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alixaxel/chrome-aws-lambda/f9d5a9ff0282ef8e172a29d6d077efc468ca3c76/_/amazon/code/.gitignore -------------------------------------------------------------------------------- /_/amazon/events/example.com.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "url": "https://example.com", 4 | "expected": { 5 | "title": "Example Domain", 6 | "screenshot": "aabec363d69b6ae44bd17d4724bab665767d56f2" 7 | } 8 | }, 9 | { 10 | "url": "https://example.com", 11 | "expected": { 12 | "title": "Example Domain", 13 | "screenshot": "aabec363d69b6ae44bd17d4724bab665767d56f2" 14 | } 15 | } 16 | ] 17 | -------------------------------------------------------------------------------- /_/amazon/handlers/index.js: -------------------------------------------------------------------------------- 1 | const { ok } = require('assert'); 2 | const { createHash } = require('crypto'); 3 | const chromium = require('chrome-aws-lambda'); 4 | 5 | exports.handler = async (event, context) => { 6 | let browser = null; 7 | 8 | try { 9 | const browser = await chromium.puppeteer.launch({ 10 | args: chromium.args, 11 | defaultViewport: chromium.defaultViewport, 12 | executablePath: await chromium.executablePath, 13 | headless: chromium.headless, 14 | ignoreHTTPSErrors: true, 15 | }); 16 | 17 | const contexts = [ 18 | browser.defaultBrowserContext(), 19 | ]; 20 | 21 | while (contexts.length < event.length) { 22 | contexts.push(await browser.createIncognitoBrowserContext()); 23 | } 24 | 25 | for (let context of contexts) { 26 | const job = event.shift(); 27 | const page = await context.defaultPage(); 28 | 29 | if (job.hasOwnProperty('url') === true) { 30 | await page.goto(job.url, { waitUntil: ['domcontentloaded', 'load'] }); 31 | 32 | if (job.hasOwnProperty('expected') === true) { 33 | if (job.expected.hasOwnProperty('title') === true) { 34 | ok(await page.title() === job.expected.title, `Title assertion failed.`); 35 | } 36 | 37 | if (job.expected.hasOwnProperty('screenshot') === true) { 38 | ok(createHash('sha1').update((await page.screenshot()).toString('base64')).digest('hex') === job.expected.screenshot, `Screenshot assertion failed.`); 39 | } 40 | } 41 | } 42 | } 43 | } catch (error) { 44 | throw error.message; 45 | } finally { 46 | if (browser !== null) { 47 | await browser.close(); 48 | } 49 | } 50 | 51 | return true; 52 | }; 53 | -------------------------------------------------------------------------------- /_/amazon/template.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Transform: AWS::Serverless-2016-10-31 3 | Globals: 4 | Function: 5 | MemorySize: 512 6 | Timeout: 30 7 | 8 | Resources: 9 | layer: 10 | Type: AWS::Serverless::LayerVersion 11 | Properties: 12 | LayerName: chrome-aws-lambda 13 | ContentUri: code/ 14 | CompatibleRuntimes: 15 | - nodejs10.x 16 | - nodejs12.x 17 | - nodejs14.x 18 | 19 | node10: 20 | Type: AWS::Serverless::Function 21 | Properties: 22 | Layers: 23 | - !Ref layer 24 | Handler: handlers/index.handler 25 | Runtime: nodejs10.x 26 | Policies: 27 | - AWSLambdaBasicExecutionRole 28 | 29 | node12: 30 | Type: AWS::Serverless::Function 31 | Properties: 32 | Layers: 33 | - !Ref layer 34 | Handler: handlers/index.handler 35 | Runtime: nodejs12.x 36 | Policies: 37 | - AWSLambdaBasicExecutionRole 38 | 39 | node14: 40 | Type: AWS::Serverless::Function 41 | Properties: 42 | Layers: 43 | - !Ref layer 44 | Handler: handlers/index.handler 45 | Runtime: nodejs14.x 46 | Policies: 47 | - AWSLambdaBasicExecutionRole 48 | -------------------------------------------------------------------------------- /_/ansible/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: ansible chromium 2 | 3 | ansible: 4 | sudo pip3 install ansible boto boto3 5 | 6 | chromium: 7 | ansible-playbook plays/chromium.yml -i inventory.ini 8 | -------------------------------------------------------------------------------- /_/ansible/README.md: -------------------------------------------------------------------------------- 1 | # Chromium Playbook 2 | 3 | This Ansible playbook will launch an EC2 `c5.12xlarge` Spot Instance and compile Chromium statically. 4 | 5 | Once the compilation finishes, the binary will be compressed with Brotli and downloaded. 6 | 7 | The whole process usually takes around 1 hour to on a `c5.12xlarge` instance. 8 | 9 | ## Chromium Version 10 | 11 | To compile a specific version of Chromium, update the `puppeteer_version` variable in the Ansible inventory, i.e.: 12 | 13 | ```shell 14 | puppeteer_version=v1.9.0 15 | ``` 16 | 17 | If not specified, the current `main` will be used. 18 | 19 | ## Usage 20 | 21 | ```shell 22 | AWS_REGION=us-east-1 \ 23 | AWS_ACCESS_KEY=XXXXXXXXXXXXXXXXXXXX \ 24 | AWS_SECRET_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX \ 25 | make chromium 26 | ``` 27 | 28 | ## Requirements 29 | 30 | - [Ansible](http://docs.ansible.com/ansible/latest/intro_installation.html#latest-releases-via-apt-ubuntu) 31 | - AWS SDK for Python (`boto` and `boto3`) 32 | -------------------------------------------------------------------------------- /_/ansible/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | hash_behaviour = merge 3 | host_key_checking = false 4 | retry_files_enabled = false 5 | 6 | [ssh_connection] 7 | ssh_args = -C -o ControlMaster=auto -o ControlPersist=60 -o ServerAliveInterval=30 8 | pipelining = true 9 | -------------------------------------------------------------------------------- /_/ansible/inventory.ini: -------------------------------------------------------------------------------- 1 | [localhost] 2 | 127.0.0.1 3 | 4 | [localhost:vars] 5 | ansible_connection=local 6 | ansible_python_interpreter=python3 7 | image=ami-0de53d8956e8dcf80 8 | region=us-east-1 9 | 10 | [aws] 11 | 12 | [aws:vars] 13 | ansible_connection=ssh 14 | ansible_python_interpreter=auto_silent 15 | ansible_ssh_private_key_file=ansible.pem 16 | puppeteer_version=v10.1.0 17 | -------------------------------------------------------------------------------- /_/ansible/plays/chromium.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Bootstrap AWS 3 | hosts: localhost 4 | gather_facts: false 5 | 6 | tasks: 7 | - name: Creating SSH Key 8 | shell: | 9 | ssh-keygen -b 2048 -t rsa -f ansible.pem -q -N '' && \ 10 | chmod 0600 ansible.pem.pub 11 | args: 12 | chdir: .. 13 | creates: ansible.pem 14 | 15 | - name: Creating EC2 Key Pair 16 | ec2_key: 17 | name: ansible 18 | state: present 19 | region: "{{ region }}" 20 | key_material: "{{ item }}" 21 | with_file: ../ansible.pem.pub 22 | 23 | - name: Creating Security Group 24 | ec2_group: 25 | name: Chromium 26 | description: SSH Access 27 | state: present 28 | region: "{{ region }}" 29 | rules: 30 | - proto: tcp 31 | to_port: 22 32 | from_port: 22 33 | cidr_ip: 0.0.0.0/0 34 | rules_egress: 35 | - proto: all 36 | cidr_ip: 0.0.0.0/0 37 | 38 | - name: Launching EC2 Instance 39 | ec2: 40 | group: Chromium 41 | image: "{{ image }}" 42 | instance_type: c5.18xlarge 43 | instance_initiated_shutdown_behavior: terminate 44 | key_name: ansible 45 | wait: yes 46 | zone: "{{ region }}a" 47 | spot_type: one-time 48 | spot_price: "1.25" 49 | spot_wait_timeout: 300 50 | spot_launch_group: chromium 51 | region: "{{ region }}" 52 | state: present 53 | volumes: 54 | - device_name: /dev/xvda 55 | delete_on_termination: true 56 | volume_size: 128 57 | volume_type: gp2 58 | instance_tags: 59 | Name: Chromium 60 | register: ec2 61 | 62 | - name: Registering Host 63 | add_host: 64 | hostname: "{{ item.public_ip }}" 65 | groupname: aws 66 | with_items: "{{ ec2.instances }}" 67 | 68 | - name: Waiting for SSH 69 | wait_for: 70 | host: "{{ item.public_ip }}" 71 | port: 22 72 | timeout: 120 73 | state: started 74 | with_items: "{{ ec2.instances }}" 75 | 76 | - name: AWS 77 | user: ec2-user 78 | hosts: aws 79 | gather_facts: true 80 | environment: 81 | LANG: en_US.UTF-8 82 | LC_ALL: en_US.UTF-8 83 | PATH: "{{ ansible_env.PATH }}:/srv/source/depot_tools" 84 | 85 | tasks: 86 | - name: Installing Packages 87 | become: true 88 | become_user: root 89 | yum: 90 | name: 91 | - "@Development Tools" 92 | - binutils 93 | - bison 94 | - bzip2 95 | - cmake 96 | - curl 97 | - dbus-x11 98 | - flex 99 | - git-core 100 | - gperf 101 | - patch 102 | - perl 103 | - python-setuptools 104 | - python3 105 | - rpm 106 | - ruby 107 | - subversion 108 | - zip 109 | state: latest 110 | update_cache: true 111 | 112 | - name: Checking for NVMe SSD Device 113 | become: true 114 | become_user: root 115 | stat: 116 | path: /dev/nvme1n1 117 | register: nvme 118 | 119 | - name: Creating NVMe SSD Filesystem 120 | become: true 121 | become_user: root 122 | filesystem: 123 | dev: /dev/nvme1n1 124 | fstype: xfs 125 | when: nvme.stat.exists 126 | 127 | - name: Mounting NVMe SSD Filesystem 128 | become: true 129 | become_user: root 130 | shell: | 131 | mount /dev/nvme1n1 /srv 132 | args: 133 | warn: false 134 | when: nvme.stat.exists 135 | 136 | - name: Checking for Directory Structure 137 | stat: 138 | path: /srv/source/chromium 139 | register: 140 | structure 141 | 142 | - name: Creating Directory Structure 143 | become: true 144 | become_user: root 145 | file: 146 | path: /srv/{{ item }}/chromium 147 | state: directory 148 | group: ec2-user 149 | owner: ec2-user 150 | recurse: true 151 | with_items: 152 | - build 153 | - source 154 | when: structure.stat.exists != true 155 | 156 | - name: Checking for Brotli 157 | stat: 158 | path: /usr/local/bin/brotli 159 | register: brotli 160 | 161 | - name: Cloning Brotli 162 | git: 163 | repo: https://github.com/google/brotli.git 164 | dest: /srv/source/brotli 165 | force: yes 166 | update: yes 167 | when: brotli.stat.exists != true 168 | 169 | - name: Compiling Brotli 170 | become: true 171 | become_user: root 172 | shell: | 173 | ./configure-cmake && \ 174 | make && \ 175 | make install 176 | args: 177 | chdir: /srv/source/brotli 178 | creates: /usr/local/bin/brotli 179 | when: brotli.stat.exists != true 180 | 181 | - name: Cloning Depot Tools 182 | git: 183 | repo: https://chromium.googlesource.com/chromium/tools/depot_tools.git 184 | dest: /srv/source/depot_tools 185 | force: yes 186 | update: yes 187 | 188 | - name: Checking for Chromium 189 | stat: 190 | path: /srv/source/chromium/.gclient 191 | register: gclient 192 | 193 | - name: Fetching Chromium 194 | shell: | 195 | fetch chromium 196 | args: 197 | chdir: /srv/source/chromium 198 | when: gclient.stat.exists != true 199 | 200 | - name: Resolving Puppeteer Version 201 | uri: 202 | url: "https://raw.githubusercontent.com/GoogleChrome/puppeteer/{{ puppeteer_version | default('main') }}/src/revisions.ts" 203 | return_content: yes 204 | register: puppeteer_revisions 205 | 206 | - name: Resolving Chromium Revision from Puppeteer Version 207 | set_fact: 208 | chromium_revision: > 209 | {{ puppeteer_revisions.content | regex_search("chromium: [']([0-9]+)[']", '\1') | first }} 210 | 211 | - name: Resolving Git Commit from Chromium Revision 212 | uri: 213 | url: "https://cr-rev.appspot.com/_ah/api/crrev/v1/redirect/{{ chromium_revision }}" 214 | return_content: yes 215 | register: revision 216 | 217 | - name: Checking Out Git Commit 218 | shell: | 219 | git checkout {{ revision.json.git_sha }} 220 | args: 221 | chdir: /srv/source/chromium/src 222 | 223 | - name: Synchronizing Chromium 224 | shell: | 225 | gclient sync --with_branch_heads 226 | args: 227 | chdir: /srv/source/chromium 228 | 229 | - name: Patching Chromium 230 | lineinfile: 231 | path: "/srv/source/chromium/src/content/browser/{{ item.path }}" 232 | line: "{{ item.line }}" 233 | regexp: "{{ item.regexp }}" 234 | state: present 235 | backrefs: yes 236 | with_items: 237 | - { 238 | path: 'sandbox_ipc_linux.cc', 239 | line: '\1PLOG(WARNING) << "poll"; failed_polls = 0;', 240 | regexp: '^(\s+)PLOG[(]WARNING[)] << "poll";$', 241 | } 242 | - { 243 | path: 'renderer_host/render_process_host_impl.cc', 244 | line: '\1// \2\3', 245 | regexp: '^( )(\s*)(CHECK[(]render_process_host->InSameStoragePartition[(])$', 246 | } 247 | - { 248 | path: 'renderer_host/render_process_host_impl.cc', 249 | line: '\1// \2\3', 250 | regexp: '^( )(\s*)(browser_context->GetStoragePartition[(]site_instance,)$', 251 | } 252 | - { 253 | path: 'renderer_host/render_process_host_impl.cc', 254 | line: '\1// \2\3', 255 | regexp: '^( )(\s*)(false /[*] can_create [*]/[)][)][)];)$', 256 | } 257 | 258 | - name: Creating Build Configuration Directory 259 | file: 260 | mode: 0755 261 | path: /srv/source/chromium/src/out/Headless 262 | state: directory 263 | 264 | - name: Mounting Build Directory in Memory 265 | become: true 266 | become_user: root 267 | shell: | 268 | mount --types tmpfs --options size=48G,nr_inodes=128k,mode=1777 tmpfs /srv/source/chromium/src/out/Headless 269 | args: 270 | warn: false 271 | 272 | - name: Creating Headless Chromium Configuration 273 | copy: 274 | content: | 275 | import("//build/args/headless.gn") 276 | blink_symbol_level = 0 277 | disable_ftp_support = true 278 | disable_histogram_support = false 279 | enable_basic_print_dialog = false 280 | enable_basic_printing = true 281 | enable_keystone_registration_framework = false 282 | enable_linux_installer = false 283 | enable_media_remoting = false 284 | enable_media_remoting_rpc = false 285 | enable_nacl = false 286 | enable_one_click_signin = false 287 | ffmpeg_branding = "Chrome" 288 | headless_use_embedded_resources = true 289 | icu_use_data_file = false 290 | is_component_build = false 291 | is_debug = false 292 | proprietary_codecs = true 293 | symbol_level = 0 294 | target_cpu = "x64" 295 | target_os = "linux" 296 | use_bundled_fontconfig = true 297 | use_cups = false 298 | use_pulseaudio = false 299 | use_sysroot = true 300 | v8_target_cpu = "x64" 301 | dest: /srv/source/chromium/src/out/Headless/args.gn 302 | 303 | - name: Generating Headless Chromium Configuration 304 | shell: | 305 | gn gen out/Headless 306 | args: 307 | chdir: /srv/source/chromium/src 308 | 309 | - name: Compiling Headless Chromium 310 | shell: | 311 | autoninja -C out/Headless headless_shell 312 | args: 313 | chdir: /srv/source/chromium/src 314 | 315 | - name: Getting Chromium Version 316 | shell: | 317 | sed --regexp-extended 's~[^0-9]+~~g' chrome/VERSION | tr '\n' '.' | sed 's~[.]$~~' 318 | args: 319 | chdir: /srv/source/chromium/src 320 | warn: false 321 | register: version 322 | 323 | - name: Striping Symbols from Chromium Binary 324 | shell: | 325 | strip -o /srv/build/chromium/chromium-{{ version.stdout | quote }} out/Headless/headless_shell 326 | args: 327 | chdir: /srv/source/chromium/src 328 | 329 | - name: Compressing Chromium 330 | shell: | 331 | brotli --best --force {{ item }} 332 | args: 333 | chdir: /srv/build/chromium 334 | with_items: 335 | - "chromium-{{ version.stdout }}" 336 | 337 | - name: Downloading Chromium 338 | fetch: 339 | src: "/srv/build/chromium/{{ item }}" 340 | dest: ../../../bin/ 341 | flat: yes 342 | fail_on_missing: true 343 | with_items: 344 | - "chromium-{{ version.stdout }}.br" 345 | 346 | - name: Archiving SwiftShader 347 | shell: | 348 | tar --directory /srv/source/chromium/src/out/Headless/swiftshader --create --file swiftshader.tar libEGL.so libGLESv2.so 349 | args: 350 | chdir: /srv/build/chromium 351 | creates: /srv/build/chromium/swiftshader.tar 352 | warn: false 353 | 354 | - name: Compressing SwiftShader 355 | shell: | 356 | brotli --best --force swiftshader.tar 357 | args: 358 | chdir: /srv/build/chromium 359 | creates: /srv/build/chromium/swiftshader.tar.br 360 | 361 | - name: Downloading SwiftShader 362 | fetch: 363 | src: /srv/build/chromium/swiftshader.tar.br 364 | dest: ../../../bin/ 365 | flat: yes 366 | fail_on_missing: true 367 | 368 | - name: Teardown AWS 369 | hosts: localhost 370 | gather_facts: false 371 | 372 | tasks: 373 | - name: Terminating EC2 Instance 374 | ec2: 375 | wait: yes 376 | state: absent 377 | instance_ids: '{{ ec2.instance_ids }}' 378 | region: "{{ region }}" 379 | 380 | - name: Deleting Security Group 381 | ec2_group: 382 | name: Chromium 383 | state: absent 384 | region: "{{ region }}" 385 | 386 | - name: Deleting EC2 Key Pair 387 | ec2_key: 388 | name: ansible 389 | state: absent 390 | region: "{{ region }}" 391 | 392 | - name: Deleting SSH Key 393 | file: 394 | path: "../{{ item }}" 395 | state: absent 396 | with_items: 397 | - ansible.pem 398 | - ansible.pem.pub 399 | -------------------------------------------------------------------------------- /bin/aws.tar.br: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alixaxel/chrome-aws-lambda/f9d5a9ff0282ef8e172a29d6d077efc468ca3c76/bin/aws.tar.br -------------------------------------------------------------------------------- /bin/chromium.br: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alixaxel/chrome-aws-lambda/f9d5a9ff0282ef8e172a29d6d077efc468ca3c76/bin/chromium.br -------------------------------------------------------------------------------- /bin/swiftshader.tar.br: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alixaxel/chrome-aws-lambda/f9d5a9ff0282ef8e172a29d6d077efc468ca3c76/bin/swiftshader.tar.br -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chrome-aws-lambda", 3 | "private": false, 4 | "version": "10.1.0", 5 | "author": { 6 | "name": "Alix Axel" 7 | }, 8 | "license": "MIT", 9 | "description": "Chromium Binary for AWS Lambda and Google Cloud Functions", 10 | "main": "build/index.js", 11 | "types": "build/index.d.ts", 12 | "files": [ 13 | "bin", 14 | "build", 15 | "typings" 16 | ], 17 | "engines": { 18 | "node": ">= 10.16" 19 | }, 20 | "scripts": { 21 | "build": "rimraf build && tsc -p tsconfig.json", 22 | "postversion": "git push && git push --tags && npm publish", 23 | "prepack": "npm run build", 24 | "preversion": "npm run build" 25 | }, 26 | "dependencies": { 27 | "lambdafs": "^2.0.3" 28 | }, 29 | "devDependencies": { 30 | "@types/node": "^10.17.55", 31 | "puppeteer-core": "^10.1.0", 32 | "rimraf": "^3.0.2", 33 | "typescript": "4.3.2" 34 | }, 35 | "peerDependencies": { 36 | "puppeteer-core": "^10.1.0" 37 | }, 38 | "bugs": { 39 | "url": "https://github.com/alixaxel/chrome-aws-lambda/issues" 40 | }, 41 | "homepage": "https://github.com/alixaxel/chrome-aws-lambda", 42 | "repository": { 43 | "type": "git", 44 | "url": "git://github.com/alixaxel/chrome-aws-lambda.git" 45 | }, 46 | "keywords": [ 47 | "aws", 48 | "browser", 49 | "chrome", 50 | "chromium", 51 | "lambda", 52 | "puppeteer", 53 | "serverless" 54 | ], 55 | "prettier": { 56 | "arrowParens": "always", 57 | "bracketSpacing": true, 58 | "jsxBracketSameLine": false, 59 | "printWidth": 140, 60 | "semi": true, 61 | "singleQuote": true, 62 | "tabWidth": 2, 63 | "trailingComma": "es5", 64 | "useTabs": false 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /source/hooks/adblock.ts: -------------------------------------------------------------------------------- 1 | import { promises } from 'fs'; 2 | import { get } from 'https'; 3 | import { Page } from 'puppeteer-core'; 4 | 5 | let adblocker: any = null; 6 | 7 | /** 8 | * Enables ad blocking in page. 9 | * Requires `@cliqz/adblocker-puppeteer` package. 10 | * 11 | * @param page - Page to hook to. 12 | */ 13 | export = async function (page: Page): Promise { 14 | if (adblocker == null) { 15 | const { fullLists, PuppeteerBlocker } = require('@cliqz/adblocker-puppeteer'); 16 | 17 | adblocker = await PuppeteerBlocker.fromLists( 18 | (url: string) => { 19 | return new Promise((resolve, reject) => { 20 | return get(url, (response) => { 21 | if (response.statusCode !== 200) { 22 | return reject(`Unexpected status code: ${response.statusCode}.`); 23 | } 24 | 25 | let result = ''; 26 | 27 | response.on('data', (chunk) => { 28 | result += chunk; 29 | }); 30 | 31 | response.on('end', () => { 32 | return resolve({ text: () => result }); 33 | }); 34 | }); 35 | }); 36 | }, 37 | fullLists, 38 | { enableCompression: false }, 39 | { 40 | path: '/tmp/adblock.bin', 41 | read: promises.readFile, 42 | write: promises.writeFile, 43 | } 44 | ); 45 | } 46 | 47 | return await adblocker.enableBlockingInPage(page).then(() => page); 48 | } 49 | -------------------------------------------------------------------------------- /source/hooks/agent.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer-core'; 2 | 3 | /** 4 | * Removes `Headless` from the User Agent string, if present. 5 | * 6 | * @param page - Page to hook to. 7 | */ 8 | export = async function (page: Page): Promise { 9 | let result = await page.browser().userAgent(); 10 | 11 | if (result.includes('Headless') === true) { 12 | await page.setUserAgent(result.replace('Headless', '')); 13 | } 14 | 15 | return page; 16 | }; 17 | -------------------------------------------------------------------------------- /source/hooks/chrome.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer-core'; 2 | import { Writeable } from '../../typings/chrome-aws-lambda'; 3 | 4 | /** 5 | * Mocks the global `chrome` property to mimic headful Chrome. 6 | * 7 | * @param page - Page to hook to. 8 | */ 9 | export = async function (page: Page): Promise { 10 | const handler = () => { 11 | let alpha = Date.now(); 12 | let delta = Math.floor(500 * Math.random()); 13 | 14 | if ((window as any).chrome === undefined) { 15 | Object.defineProperty(window, 'chrome', { 16 | configurable: false, 17 | enumerable: true, 18 | value: {}, 19 | writable: true, 20 | }); 21 | } 22 | 23 | /** 24 | * https://github.com/berstend/puppeteer-extra/blob/master/packages/puppeteer-extra-plugin-stealth/evasions/chrome.app/index.js 25 | */ 26 | if ((window as any).chrome.app === undefined) { 27 | const InvocationError = (callback: string) => { 28 | /** 29 | * Truncates every line of the stack trace (with the exception of the first), until `search` is found. 30 | */ 31 | const truncateStackTrace = (error: Error, search: string) => { 32 | const stack = error.stack.split('\n'); 33 | const index = stack.findIndex((value: string) => value.trim().startsWith(search)); 34 | 35 | if (index > 0) { 36 | error.stack = [stack[0], ...stack.slice(index + 1)].join('\n'); 37 | } 38 | 39 | return error; 40 | }; 41 | 42 | return truncateStackTrace(new TypeError(`Error in invocation of app.${callback}()`), `at ${callback} (eval at `); 43 | }; 44 | 45 | Object.defineProperty((window as any).chrome, 'app', { 46 | value: { 47 | InstallState: { 48 | DISABLED: 'disabled', 49 | INSTALLED: 'installed', 50 | NOT_INSTALLED: 'not_installed', 51 | }, 52 | RunningState: { 53 | CANNOT_RUN: 'cannot_run', 54 | READY_TO_RUN: 'ready_to_run', 55 | RUNNING: 'running', 56 | }, 57 | get isInstalled() { 58 | return false; 59 | }, 60 | getDetails: function getDetails(): null { 61 | if (arguments.length > 0) { 62 | throw InvocationError('getDetails'); 63 | } 64 | 65 | return null; 66 | }, 67 | getIsInstalled: function getIsInstalled() { 68 | if (arguments.length > 0) { 69 | throw InvocationError('getIsInstalled'); 70 | } 71 | 72 | return false; 73 | }, 74 | runningState: function runningState() { 75 | if (arguments.length > 0) { 76 | throw InvocationError('runningState'); 77 | } 78 | 79 | return 'cannot_run'; 80 | }, 81 | }, 82 | }); 83 | } 84 | 85 | let timing: Partial = { 86 | navigationStart: alpha + 1 * delta, 87 | domContentLoadedEventEnd: alpha + 4 * delta, 88 | responseStart: alpha + 2 * delta, 89 | loadEventEnd: alpha + 5 * delta, 90 | }; 91 | 92 | if (window.performance?.timing !== undefined) { 93 | timing = window.performance.timing; 94 | } 95 | 96 | /** 97 | * https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth/evasions/chrome.csi 98 | */ 99 | if ((window as any).chrome.csi === undefined) { 100 | Object.defineProperty((window as any).chrome, 'csi', { 101 | value: function csi() { 102 | return { 103 | startE: timing.navigationStart, 104 | onloadT: timing.domContentLoadedEventEnd, 105 | pageT: Date.now() - timing.navigationStart + Math.random().toFixed(3), 106 | tran: 15, 107 | }; 108 | }, 109 | }); 110 | } 111 | 112 | /** 113 | * https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth/evasions/chrome.loadTimes 114 | */ 115 | if ((window as any).chrome.loadTimes === undefined) { 116 | let navigation: Writeable> = { 117 | nextHopProtocol: 'h2', 118 | startTime: 3 * delta, 119 | type: 'other' as any, 120 | }; 121 | 122 | if (typeof window.performance?.getEntriesByType === 'function') { 123 | let entries = { 124 | navigation: window.performance.getEntriesByType('navigation') as PerformanceNavigationTiming[], 125 | paint: window.performance.getEntriesByType('paint') as PerformanceNavigationTiming[], 126 | }; 127 | 128 | if (entries.navigation.length > 0) { 129 | navigation = entries.navigation.shift(); 130 | } 131 | 132 | if (entries.paint.length > 0) { 133 | navigation.startTime = entries.paint.shift().startTime; 134 | } 135 | } 136 | 137 | Object.defineProperty((window as any).chrome, 'loadTimes', { 138 | value: function loadTimes() { 139 | return { 140 | get commitLoadTime() { 141 | return timing.responseStart / 1000; 142 | }, 143 | get connectionInfo() { 144 | return navigation.nextHopProtocol; 145 | }, 146 | get finishDocumentLoadTime() { 147 | return timing.domContentLoadedEventEnd / 1000; 148 | }, 149 | get finishLoadTime() { 150 | return timing.loadEventEnd / 1000; 151 | }, 152 | get firstPaintAfterLoadTime() { 153 | return 0; 154 | }, 155 | get firstPaintTime() { 156 | return parseFloat(((navigation.startTime + (window.performance?.timeOrigin ?? timing.navigationStart)) / 1000).toFixed(3)); 157 | }, 158 | get navigationType() { 159 | return navigation.type; 160 | }, 161 | get npnNegotiatedProtocol() { 162 | return ['h2', 'hq'].includes(navigation.nextHopProtocol) ? navigation.nextHopProtocol : 'unknown'; 163 | }, 164 | get requestTime() { 165 | return timing.navigationStart / 1000; 166 | }, 167 | get startLoadTime() { 168 | return timing.navigationStart / 1000; 169 | }, 170 | get wasAlternateProtocolAvailable() { 171 | return false; 172 | }, 173 | get wasFetchedViaSpdy() { 174 | return ['h2', 'hq'].includes(navigation.nextHopProtocol); 175 | }, 176 | get wasNpnNegotiated() { 177 | return ['h2', 'hq'].includes(navigation.nextHopProtocol); 178 | }, 179 | }; 180 | }, 181 | }); 182 | }; 183 | } 184 | 185 | await page.evaluate(handler); 186 | await page.evaluateOnNewDocument(handler); 187 | 188 | return page; 189 | } 190 | -------------------------------------------------------------------------------- /source/hooks/languages.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer-core'; 2 | 3 | /** 4 | * Emulates `en-US` language. 5 | * 6 | * @param page - Page to hook to. 7 | */ 8 | export = async function (page: Page): Promise { 9 | const handler = () => { 10 | Object.defineProperty(Object.getPrototypeOf(navigator), 'language', { 11 | get: () => 'en-US', 12 | }); 13 | 14 | Object.defineProperty(Object.getPrototypeOf(navigator), 'languages', { 15 | get: () => ['en-US', 'en'], 16 | }); 17 | }; 18 | 19 | await page.evaluate(handler); 20 | await page.evaluateOnNewDocument(handler); 21 | 22 | return page; 23 | } 24 | -------------------------------------------------------------------------------- /source/hooks/permissions.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer-core'; 2 | 3 | /** 4 | * Emulates `denied` state for all permission queries. 5 | * 6 | * @param page - Page to hook to. 7 | */ 8 | export = async function (page: Page): Promise { 9 | const handler = () => { 10 | let query = window.navigator.permissions.query; 11 | 12 | (Permissions as any).prototype.query = function (parameters: DevicePermissionDescriptor | MidiPermissionDescriptor | PermissionDescriptor | PushPermissionDescriptor) { 13 | if (parameters?.name?.length > 0) { 14 | return Promise.resolve({ 15 | onchange: null, 16 | state: 'denied', 17 | }); 18 | } 19 | 20 | return query(parameters); 21 | }; 22 | }; 23 | 24 | await page.evaluate(handler); 25 | await page.evaluateOnNewDocument(handler); 26 | 27 | return page; 28 | } 29 | -------------------------------------------------------------------------------- /source/hooks/timezone.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer-core'; 2 | 3 | /** 4 | * Emulates UTC timezone. 5 | * 6 | * @param page - Page to hook to. 7 | */ 8 | export = function (page: Page): Promise { 9 | return page.emulateTimezone('UTC').then(() => page); 10 | } 11 | -------------------------------------------------------------------------------- /source/hooks/webdriver.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer-core'; 2 | 3 | /** 4 | * Removes global `webdriver` property to mimic headful Chrome. 5 | * 6 | * @param page - Page to hook to. 7 | */ 8 | export = async function (page: Page): Promise { 9 | const handler = () => { 10 | Object.defineProperty(Object.getPrototypeOf(navigator), 'webdriver', { 11 | get: () => false, 12 | }); 13 | }; 14 | 15 | await page.evaluate(handler); 16 | await page.evaluateOnNewDocument(handler); 17 | 18 | return page; 19 | } 20 | -------------------------------------------------------------------------------- /source/hooks/window.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer-core'; 2 | 3 | /** 4 | * Patches window outer dimentions to mimic headful Chrome. 5 | * 6 | * @param page - Page to hook to. 7 | */ 8 | export = async function (page: Page): Promise { 9 | const handler = () => { 10 | if (window.outerWidth === 0) { 11 | Object.defineProperty(window, 'outerWidth', { 12 | get: () => screen.availWidth, 13 | }); 14 | } 15 | 16 | if (window.outerHeight === 0) { 17 | Object.defineProperty(window, 'outerHeight', { 18 | get: () => screen.availHeight, 19 | }); 20 | } 21 | 22 | if (window.screenX === 0) { 23 | Object.defineProperty(window, 'screenX', { 24 | get: () => screen.width - screen.availWidth, 25 | }); 26 | 27 | Object.defineProperty(window, 'screenLeft', { 28 | get: () => screenX, 29 | }); 30 | } 31 | 32 | if (window.screenY === 0) { 33 | Object.defineProperty(window, 'screenY', { 34 | get: () => screen.height - screen.availHeight, 35 | }); 36 | 37 | Object.defineProperty(window, 'screenTop', { 38 | get: () => screenY, 39 | }); 40 | } 41 | }; 42 | 43 | await page.evaluate(handler); 44 | await page.evaluateOnNewDocument(handler); 45 | 46 | return page; 47 | } 48 | -------------------------------------------------------------------------------- /source/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { access, createWriteStream, existsSync, mkdirSync, readdirSync, symlink, unlinkSync } from 'fs'; 4 | import { IncomingMessage } from 'http'; 5 | import LambdaFS from 'lambdafs'; 6 | import { join } from 'path'; 7 | import { PuppeteerNode, Viewport } from 'puppeteer-core'; 8 | import { URL } from 'url'; 9 | 10 | if (/^AWS_Lambda_nodejs(?:10|12|14)[.]x$/.test(process.env.AWS_EXECUTION_ENV) === true) { 11 | if (process.env.FONTCONFIG_PATH === undefined) { 12 | process.env.FONTCONFIG_PATH = '/tmp/aws'; 13 | } 14 | 15 | if (process.env.LD_LIBRARY_PATH === undefined) { 16 | process.env.LD_LIBRARY_PATH = '/tmp/aws/lib'; 17 | } else if (process.env.LD_LIBRARY_PATH.startsWith('/tmp/aws/lib') !== true) { 18 | process.env.LD_LIBRARY_PATH = [...new Set(['/tmp/aws/lib', ...process.env.LD_LIBRARY_PATH.split(':')])].join(':'); 19 | } 20 | } 21 | 22 | class Chromium { 23 | /** 24 | * Downloads or symlinks a custom font and returns its basename, patching the environment so that Chromium can find it. 25 | * If not running on AWS Lambda nor Google Cloud Functions, `null` is returned instead. 26 | */ 27 | static font(input: string): Promise { 28 | if (Chromium.headless !== true) { 29 | return null; 30 | } 31 | 32 | if (process.env.HOME === undefined) { 33 | process.env.HOME = '/tmp'; 34 | } 35 | 36 | if (existsSync(`${process.env.HOME}/.fonts`) !== true) { 37 | mkdirSync(`${process.env.HOME}/.fonts`); 38 | } 39 | 40 | return new Promise((resolve, reject) => { 41 | if (/^https?:[/][/]/i.test(input) !== true) { 42 | input = `file://${input}`; 43 | } 44 | 45 | const url = new URL(input); 46 | const output = `${process.env.HOME}/.fonts/${url.pathname.split('/').pop()}`; 47 | 48 | if (existsSync(output) === true) { 49 | return resolve(output.split('/').pop()); 50 | } 51 | 52 | if (url.protocol === 'file:') { 53 | access(url.pathname, (error) => { 54 | if (error != null) { 55 | return reject(error); 56 | } 57 | 58 | symlink(url.pathname, output, (error) => { 59 | return error != null ? reject(error) : resolve(url.pathname.split('/').pop()); 60 | }); 61 | }); 62 | } else { 63 | let handler = url.protocol === 'http:' ? require('http').get : require('https').get; 64 | 65 | handler(input, (response: IncomingMessage) => { 66 | if (response.statusCode !== 200) { 67 | return reject(`Unexpected status code: ${response.statusCode}.`); 68 | } 69 | 70 | const stream = createWriteStream(output); 71 | 72 | stream.once('error', (error) => { 73 | return reject(error); 74 | }); 75 | 76 | response.on('data', (chunk) => { 77 | stream.write(chunk); 78 | }); 79 | 80 | response.once('end', () => { 81 | stream.end(() => { 82 | return resolve(url.pathname.split('/').pop()); 83 | }); 84 | }); 85 | }); 86 | } 87 | }); 88 | } 89 | 90 | /** 91 | * Returns a list of additional Chromium flags recommended for serverless environments. 92 | * The canonical list of flags can be found on https://peter.sh/experiments/chromium-command-line-switches/. 93 | */ 94 | static get args(): string[] { 95 | const result = [ 96 | '--allow-running-insecure-content', // https://source.chromium.org/search?q=lang:cpp+symbol:kAllowRunningInsecureContent&ss=chromium 97 | '--autoplay-policy=user-gesture-required', // https://source.chromium.org/search?q=lang:cpp+symbol:kAutoplayPolicy&ss=chromium 98 | '--disable-component-update', // https://source.chromium.org/search?q=lang:cpp+symbol:kDisableComponentUpdate&ss=chromium 99 | '--disable-domain-reliability', // https://source.chromium.org/search?q=lang:cpp+symbol:kDisableDomainReliability&ss=chromium 100 | '--disable-features=AudioServiceOutOfProcess,IsolateOrigins,site-per-process', // https://source.chromium.org/search?q=file:content_features.cc&ss=chromium 101 | '--disable-print-preview', // https://source.chromium.org/search?q=lang:cpp+symbol:kDisablePrintPreview&ss=chromium 102 | '--disable-setuid-sandbox', // https://source.chromium.org/search?q=lang:cpp+symbol:kDisableSetuidSandbox&ss=chromium 103 | '--disable-site-isolation-trials', // https://source.chromium.org/search?q=lang:cpp+symbol:kDisableSiteIsolation&ss=chromium 104 | '--disable-speech-api', // https://source.chromium.org/search?q=lang:cpp+symbol:kDisableSpeechAPI&ss=chromium 105 | '--disable-web-security', // https://source.chromium.org/search?q=lang:cpp+symbol:kDisableWebSecurity&ss=chromium 106 | '--disk-cache-size=33554432', // https://source.chromium.org/search?q=lang:cpp+symbol:kDiskCacheSize&ss=chromium 107 | '--enable-features=SharedArrayBuffer', // https://source.chromium.org/search?q=file:content_features.cc&ss=chromium 108 | '--hide-scrollbars', // https://source.chromium.org/search?q=lang:cpp+symbol:kHideScrollbars&ss=chromium 109 | '--ignore-gpu-blocklist', // https://source.chromium.org/search?q=lang:cpp+symbol:kIgnoreGpuBlocklist&ss=chromium 110 | '--in-process-gpu', // https://source.chromium.org/search?q=lang:cpp+symbol:kInProcessGPU&ss=chromium 111 | '--mute-audio', // https://source.chromium.org/search?q=lang:cpp+symbol:kMuteAudio&ss=chromium 112 | '--no-default-browser-check', // https://source.chromium.org/search?q=lang:cpp+symbol:kNoDefaultBrowserCheck&ss=chromium 113 | '--no-pings', // https://source.chromium.org/search?q=lang:cpp+symbol:kNoPings&ss=chromium 114 | '--no-sandbox', // https://source.chromium.org/search?q=lang:cpp+symbol:kNoSandbox&ss=chromium 115 | '--no-zygote', // https://source.chromium.org/search?q=lang:cpp+symbol:kNoZygote&ss=chromium 116 | '--use-gl=swiftshader', // https://source.chromium.org/search?q=lang:cpp+symbol:kUseGl&ss=chromium 117 | '--window-size=1920,1080', // https://source.chromium.org/search?q=lang:cpp+symbol:kWindowSize&ss=chromium 118 | ]; 119 | 120 | if (Chromium.headless === true) { 121 | result.push('--single-process'); // https://source.chromium.org/search?q=lang:cpp+symbol:kSingleProcess&ss=chromium 122 | } else { 123 | result.push('--start-maximized'); // https://source.chromium.org/search?q=lang:cpp+symbol:kStartMaximized&ss=chromium 124 | } 125 | 126 | return result; 127 | } 128 | 129 | /** 130 | * Returns sensible default viewport settings. 131 | */ 132 | static get defaultViewport(): Required { 133 | return { 134 | deviceScaleFactor: 1, 135 | hasTouch: false, 136 | height: 1080, 137 | isLandscape: true, 138 | isMobile: false, 139 | width: 1920, 140 | }; 141 | } 142 | 143 | /** 144 | * Inflates the current version of Chromium and returns the path to the binary. 145 | * If not running on AWS Lambda nor Google Cloud Functions, `null` is returned instead. 146 | */ 147 | static get executablePath(): Promise { 148 | if (Chromium.headless !== true) { 149 | return Promise.resolve(null); 150 | } 151 | 152 | if (existsSync('/tmp/chromium') === true) { 153 | for (const file of readdirSync('/tmp')) { 154 | if (file.startsWith('core.chromium') === true) { 155 | unlinkSync(`/tmp/${file}`); 156 | } 157 | } 158 | 159 | return Promise.resolve('/tmp/chromium'); 160 | } 161 | 162 | const input = join(__dirname, '..', 'bin'); 163 | const promises = [ 164 | LambdaFS.inflate(`${input}/chromium.br`), 165 | LambdaFS.inflate(`${input}/swiftshader.tar.br`), 166 | ]; 167 | 168 | if (/^AWS_Lambda_nodejs(?:10|12|14)[.]x$/.test(process.env.AWS_EXECUTION_ENV) === true) { 169 | promises.push(LambdaFS.inflate(`${input}/aws.tar.br`)); 170 | } 171 | 172 | return Promise.all(promises).then((result) => result.shift()); 173 | } 174 | 175 | /** 176 | * Returns a boolean indicating if we are running on AWS Lambda or Google Cloud Functions. 177 | * False is returned if Serverless environment variables `IS_LOCAL` or `IS_OFFLINE` are set. 178 | */ 179 | static get headless() { 180 | if (process.env.IS_LOCAL !== undefined || process.env.IS_OFFLINE !== undefined) { 181 | return false; 182 | } 183 | 184 | const environments = [ 185 | 'AWS_LAMBDA_FUNCTION_NAME', 186 | 'FUNCTION_NAME', 187 | 'FUNCTION_TARGET', 188 | 'FUNCTIONS_EMULATOR', 189 | ]; 190 | 191 | return environments.some((key) => process.env[key] !== undefined); 192 | } 193 | 194 | /** 195 | * Overloads puppeteer with useful methods and returns the resolved package. 196 | */ 197 | static get puppeteer(): PuppeteerNode { 198 | for (const overload of ['Browser', 'BrowserContext', 'ElementHandle', 'FrameManager', 'Page']) { 199 | require(`${__dirname}/puppeteer/lib/${overload}`); 200 | } 201 | 202 | try { 203 | return require('puppeteer'); 204 | } catch (error) { 205 | if (error.code !== 'MODULE_NOT_FOUND') { 206 | throw error; 207 | } 208 | 209 | return require('puppeteer-core'); 210 | } 211 | } 212 | } 213 | 214 | export = Chromium; 215 | -------------------------------------------------------------------------------- /source/puppeteer/lib/Browser.ts: -------------------------------------------------------------------------------- 1 | import { Browser, Page } from 'puppeteer-core'; 2 | import { Hook, Prototype } from '../../../typings/chrome-aws-lambda'; 3 | 4 | let Super: Prototype = null; 5 | 6 | try { 7 | Super = require('puppeteer/lib/cjs/puppeteer/common/Browser').Browser; 8 | } catch (error) { 9 | Super = require('puppeteer-core/lib/cjs/puppeteer/common/Browser').Browser; 10 | } 11 | 12 | Super.prototype.defaultPage = async function (...hooks: Hook[]) { 13 | let page: Page = null; 14 | let pages: Page[] = await this.pages(); 15 | 16 | if (pages.length === 0) { 17 | pages = [await this.newPage()]; 18 | } 19 | 20 | page = pages.shift(); 21 | 22 | if (hooks != null && Array.isArray(hooks) === true) { 23 | for (let hook of hooks) { 24 | page = await hook(page); 25 | } 26 | } 27 | 28 | return page; 29 | }; 30 | 31 | let newPage: any = Super.prototype.newPage; 32 | 33 | Super.prototype.newPage = async function (...hooks: Hook[]) { 34 | let page: Page = await newPage.apply(this, arguments); 35 | 36 | if (hooks != null && Array.isArray(hooks) === true) { 37 | for (let hook of hooks) { 38 | page = await hook(page); 39 | } 40 | } 41 | 42 | return page; 43 | }; 44 | -------------------------------------------------------------------------------- /source/puppeteer/lib/BrowserContext.ts: -------------------------------------------------------------------------------- 1 | import { BrowserContext, Page } from 'puppeteer-core'; 2 | import { Hook, Prototype } from '../../../typings/chrome-aws-lambda'; 3 | 4 | let Super: Prototype = null; 5 | 6 | try { 7 | Super = require('puppeteer/lib/cjs/puppeteer/common/Browser').BrowserContext; 8 | } catch (error) { 9 | Super = require('puppeteer-core/lib/cjs/puppeteer/common/Browser').BrowserContext; 10 | } 11 | 12 | Super.prototype.defaultPage = async function (...hooks: Hook[]) { 13 | let page: Page = null; 14 | let pages: Page[] = await this.pages(); 15 | 16 | if (pages.length === 0) { 17 | pages = [await this.newPage()]; 18 | } 19 | 20 | page = pages.shift(); 21 | 22 | if (hooks != null && Array.isArray(hooks) === true) { 23 | for (let hook of hooks) { 24 | page = await hook(page); 25 | } 26 | } 27 | 28 | return page; 29 | }; 30 | 31 | let newPage: any = Super.prototype.newPage; 32 | 33 | Super.prototype.newPage = async function (...hooks: Hook[]) { 34 | let page: Page = await newPage.apply(this, arguments); 35 | 36 | if (hooks != null && Array.isArray(hooks) === true) { 37 | for (let hook of hooks) { 38 | page = await hook(page); 39 | } 40 | } 41 | 42 | return page; 43 | }; 44 | -------------------------------------------------------------------------------- /source/puppeteer/lib/ElementHandle.ts: -------------------------------------------------------------------------------- 1 | import { ElementHandle, EvaluateFn, HTTPRequest, HTTPResponse, Page, WaitForOptions, WaitTimeoutOptions } from 'puppeteer-core'; 2 | import { KeysOfType, Prototype } from '../../../typings/chrome-aws-lambda'; 3 | 4 | let Super: Prototype = null; 5 | 6 | try { 7 | Super = require('puppeteer/lib/cjs/puppeteer/common/JSHandle').ElementHandle; 8 | } catch (error) { 9 | Super = require('puppeteer-core/lib/cjs/puppeteer/common/JSHandle').ElementHandle; 10 | } 11 | 12 | Super.prototype.clear = function () { 13 | return this.click({ clickCount: 3 }).then(() => this.press('Backspace')); 14 | }; 15 | 16 | Super.prototype.clickAndWaitForNavigation = function (options?: WaitForOptions) { 17 | options = options ?? { 18 | waitUntil: [ 19 | 'load', 20 | ], 21 | }; 22 | 23 | let promises: [Promise, Promise] = [ 24 | ((this as any)._page as Page).waitForNavigation(options), 25 | this.click(), 26 | ]; 27 | 28 | return Promise.all(promises).then((value) => value.shift() as HTTPResponse); 29 | }; 30 | 31 | Super.prototype.clickAndWaitForRequest = function (predicate: string | RegExp | ((request: HTTPRequest) => boolean | Promise), options?: WaitTimeoutOptions) { 32 | let callback = (request: HTTPRequest) => { 33 | let url = request.url(); 34 | 35 | if (typeof predicate === 'string' && predicate.includes('*') === true) { 36 | predicate = new RegExp(predicate.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&').replace(/[*]+/g, '.*?'), 'g'); 37 | } 38 | 39 | if (predicate instanceof RegExp) { 40 | return predicate.test(url); 41 | } 42 | 43 | return predicate === url; 44 | }; 45 | 46 | let promises: [Promise, Promise] = [ 47 | ((this as any)._page as Page).waitForRequest((typeof predicate === 'function') ? predicate : callback, options), 48 | this.click(), 49 | ]; 50 | 51 | return Promise.all(promises).then((value) => value.shift() as HTTPRequest); 52 | }; 53 | 54 | Super.prototype.clickAndWaitForResponse = function (predicate: string | RegExp | ((request: HTTPResponse) => boolean | Promise), options?: WaitTimeoutOptions) { 55 | let callback = (request: HTTPResponse) => { 56 | let url = request.url(); 57 | 58 | if (typeof predicate === 'string' && predicate.includes('*') === true) { 59 | predicate = new RegExp(predicate.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&').replace(/[*]+/g, '.*?'), 'g'); 60 | } 61 | 62 | if (predicate instanceof RegExp) { 63 | return predicate.test(url); 64 | } 65 | 66 | return predicate === url; 67 | }; 68 | 69 | let promises: [Promise, Promise] = [ 70 | ((this as any)._page as Page).waitForResponse((typeof predicate === 'function') ? predicate : callback, options), 71 | this.click(), 72 | ]; 73 | 74 | return Promise.all(promises).then((value) => value.shift() as HTTPResponse); 75 | }; 76 | 77 | Super.prototype.fillFormByLabel = function >(data: T) { 78 | let callback = (node: HTMLFormElement, data: T) => { 79 | if (node.nodeName.toLowerCase() !== 'form') { 80 | throw new Error('Element is not a
element.'); 81 | } 82 | 83 | let result: Record = {}; 84 | 85 | for (let [key, value] of Object.entries(data)) { 86 | let selector = [ 87 | `id(string(//label[normalize-space(.) = "${key}"]/@for))`, 88 | `//label[normalize-space(.) = "${key}"]//*[self::input or self::select or self::textarea]`, 89 | ].join(' | '); 90 | 91 | if (result.hasOwnProperty(key) !== true) { 92 | result[key] = []; 93 | } 94 | 95 | let element: Node = null; 96 | let elements: HTMLInputElement[] = []; 97 | let iterator = document.evaluate(selector, node, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); 98 | 99 | while ((element = iterator.iterateNext()) != null) { 100 | elements.push(element as HTMLInputElement); 101 | } 102 | 103 | if (elements.length === 0) { 104 | throw new Error(`No elements match the selector '${selector}' for '${key}'.`); 105 | } 106 | 107 | let type = (elements[0].getAttribute('type') || elements[0].nodeName).toLowerCase(); 108 | let values: (boolean | string)[] = (Array.isArray(value) === true) ? value as (boolean | string)[] : [value] as (boolean | string)[]; 109 | 110 | if (type === 'file') { 111 | throw new Error(`Input element of type 'file' is not supported.`); 112 | } 113 | 114 | for (let element of elements) { 115 | try { 116 | element.focus(); 117 | element.dispatchEvent(new Event('focus')); 118 | } catch (error) { 119 | } 120 | 121 | if (type === 'select') { 122 | element.value = undefined; 123 | 124 | for (let index of ['value', 'label'] as ['value', 'label']) { 125 | if (result[key].length > 0) { 126 | break; 127 | } 128 | 129 | for (let option of Array.from((element as unknown as HTMLSelectElement).options)) { 130 | option.selected = values.includes(option[index]); 131 | 132 | if (option.selected === true) { 133 | result[key].push(option.value); 134 | 135 | if (element.multiple !== true) { 136 | break; 137 | } 138 | } 139 | } 140 | } 141 | } else if (type === 'checkbox' || type === 'radio') { 142 | element.checked = (value === true) || values.includes(element.value); 143 | 144 | if (element.checked === true) { 145 | result[key].push(element.value); 146 | } 147 | } else if (typeof value === 'string') { 148 | if (element.isContentEditable === true) { 149 | result[key].push(element.textContent = value); 150 | } else { 151 | result[key].push(element.value = value); 152 | } 153 | } 154 | 155 | for (let trigger of ['input', 'change']) { 156 | element.dispatchEvent(new Event(trigger, { 'bubbles': true })); 157 | } 158 | 159 | try { 160 | element.blur(); 161 | element.dispatchEvent(new Event('blur')); 162 | } catch (error) { 163 | } 164 | 165 | if (type === 'checkbox' || type === 'radio') { 166 | break; 167 | } 168 | } 169 | } 170 | 171 | return result; 172 | }; 173 | 174 | return this.evaluate(callback as unknown as EvaluateFn, data) as any; 175 | }; 176 | 177 | Super.prototype.fillFormByName = function >(data: T) { 178 | let callback = (node: HTMLFormElement, data: T, heuristic: 'css' | 'label' | 'name' | 'xpath' = 'css') => { 179 | if (node.nodeName.toLowerCase() !== 'form') { 180 | throw new Error('Element is not a element.'); 181 | } 182 | 183 | let result: Record = {}; 184 | 185 | for (let [key, value] of Object.entries(data)) { 186 | let selector = `[name="${key}"]`; 187 | 188 | if (result.hasOwnProperty(key) !== true) { 189 | result[key] = []; 190 | } 191 | 192 | let elements: HTMLInputElement[] = Array.from(node.querySelectorAll(selector)); 193 | 194 | if (elements.length === 0) { 195 | throw new Error(`No elements match the selector '${selector}' for '${key}'.`); 196 | } 197 | 198 | let type = (elements[0].getAttribute('type') || elements[0].nodeName).toLowerCase(); 199 | let values: (boolean | string)[] = (Array.isArray(value) === true) ? value as (boolean | string)[] : [value] as (boolean | string)[]; 200 | 201 | if (type === 'file') { 202 | throw new Error(`Input element of type 'file' is not supported.`); 203 | } 204 | 205 | for (let element of elements) { 206 | try { 207 | element.focus(); 208 | element.dispatchEvent(new Event('focus')); 209 | } catch (error) { 210 | } 211 | 212 | if (type === 'select') { 213 | element.value = undefined; 214 | 215 | for (let index of ['value', 'label'] as ['value', 'label']) { 216 | if (result[key].length > 0) { 217 | break; 218 | } 219 | 220 | for (let option of Array.from((element as unknown as HTMLSelectElement).options)) { 221 | option.selected = values.includes(option[index]); 222 | 223 | if (option.selected === true) { 224 | result[key].push(option.value); 225 | 226 | if (element.multiple !== true) { 227 | break; 228 | } 229 | } 230 | } 231 | } 232 | } else if (type === 'checkbox' || type === 'radio') { 233 | element.checked = (value === true) || values.includes(element.value); 234 | 235 | if (element.checked === true) { 236 | result[key].push(element.value); 237 | } 238 | } else if (typeof value === 'string') { 239 | if (element.isContentEditable === true) { 240 | result[key].push(element.textContent = value); 241 | } else { 242 | result[key].push(element.value = value); 243 | } 244 | } 245 | 246 | for (let trigger of ['input', 'change']) { 247 | element.dispatchEvent(new Event(trigger, { 'bubbles': true })); 248 | } 249 | 250 | try { 251 | element.blur(); 252 | element.dispatchEvent(new Event('blur')); 253 | } catch (error) { 254 | } 255 | 256 | if (type === 'checkbox' || type === 'radio') { 257 | break; 258 | } 259 | } 260 | } 261 | 262 | return result; 263 | }; 264 | 265 | return this.evaluate(callback as unknown as EvaluateFn, data) as any; 266 | }; 267 | 268 | Super.prototype.fillFormBySelector = function >(data: T) { 269 | let callback = (node: HTMLFormElement, data: T, heuristic: 'css' | 'label' | 'name' | 'xpath' = 'css') => { 270 | if (node.nodeName.toLowerCase() !== 'form') { 271 | throw new Error('Element is not a element.'); 272 | } 273 | 274 | let result: Record = {}; 275 | 276 | for (let [key, value] of Object.entries(data)) { 277 | let selector = key; 278 | 279 | if (result.hasOwnProperty(key) !== true) { 280 | result[key] = []; 281 | } 282 | 283 | let elements: HTMLInputElement[] = Array.from(node.querySelectorAll(selector)); 284 | 285 | if (elements.length === 0) { 286 | throw new Error(`No elements match the selector '${selector}' for '${key}'.`); 287 | } 288 | 289 | let type = (elements[0].getAttribute('type') || elements[0].nodeName).toLowerCase(); 290 | let values: (boolean | string)[] = (Array.isArray(value) === true) ? value as (boolean | string)[] : [value] as (boolean | string)[]; 291 | 292 | if (type === 'file') { 293 | throw new Error(`Input element of type 'file' is not supported.`); 294 | } 295 | 296 | for (let element of elements) { 297 | try { 298 | element.focus(); 299 | element.dispatchEvent(new Event('focus')); 300 | } catch (error) { 301 | } 302 | 303 | if (type === 'select') { 304 | element.value = undefined; 305 | 306 | for (let index of ['value', 'label'] as ['value', 'label']) { 307 | if (result[key].length > 0) { 308 | break; 309 | } 310 | 311 | for (let option of Array.from((element as unknown as HTMLSelectElement).options)) { 312 | option.selected = values.includes(option[index]); 313 | 314 | if (option.selected === true) { 315 | result[key].push(option.value); 316 | 317 | if (element.multiple !== true) { 318 | break; 319 | } 320 | } 321 | } 322 | } 323 | } else if (type === 'checkbox' || type === 'radio') { 324 | element.checked = (value === true) || values.includes(element.value); 325 | 326 | if (element.checked === true) { 327 | result[key].push(element.value); 328 | } 329 | } else if (typeof value === 'string') { 330 | if (element.isContentEditable === true) { 331 | result[key].push(element.textContent = value); 332 | } else { 333 | result[key].push(element.value = value); 334 | } 335 | } 336 | 337 | for (let trigger of ['input', 'change']) { 338 | element.dispatchEvent(new Event(trigger, { 'bubbles': true })); 339 | } 340 | 341 | try { 342 | element.blur(); 343 | element.dispatchEvent(new Event('blur')); 344 | } catch (error) { 345 | } 346 | 347 | if (type === 'checkbox' || type === 'radio') { 348 | break; 349 | } 350 | } 351 | } 352 | 353 | return result; 354 | }; 355 | 356 | return this.evaluate(callback as unknown as EvaluateFn, data) as any; 357 | }; 358 | 359 | Super.prototype.fillFormByXPath = function >(data: T) { 360 | let callback = (node: HTMLFormElement, data: T) => { 361 | if (node.nodeName.toLowerCase() !== 'form') { 362 | throw new Error('Element is not a element.'); 363 | } 364 | 365 | let result: Record = {}; 366 | 367 | for (let [key, value] of Object.entries(data)) { 368 | let selector = key; 369 | 370 | if (result.hasOwnProperty(key) !== true) { 371 | result[key] = []; 372 | } 373 | 374 | let element: Node = null; 375 | let elements: HTMLInputElement[] = []; 376 | let iterator = document.evaluate(selector, node, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); 377 | 378 | while ((element = iterator.iterateNext()) != null) { 379 | elements.push(element as HTMLInputElement); 380 | } 381 | 382 | if (elements.length === 0) { 383 | throw new Error(`No elements match the selector '${selector}' for '${key}'.`); 384 | } 385 | 386 | let type = (elements[0].getAttribute('type') || elements[0].nodeName).toLowerCase(); 387 | let values: (boolean | string)[] = (Array.isArray(value) === true) ? value as (boolean | string)[] : [value] as (boolean | string)[]; 388 | 389 | if (type === 'file') { 390 | throw new Error(`Input element of type 'file' is not supported.`); 391 | } 392 | 393 | for (let element of elements) { 394 | try { 395 | element.focus(); 396 | element.dispatchEvent(new Event('focus')); 397 | } catch (error) { 398 | } 399 | 400 | if (type === 'select') { 401 | element.value = undefined; 402 | 403 | for (let index of ['value', 'label'] as ['value', 'label']) { 404 | if (result[key].length > 0) { 405 | break; 406 | } 407 | 408 | for (let option of Array.from((element as unknown as HTMLSelectElement).options)) { 409 | option.selected = values.includes(option[index]); 410 | 411 | if (option.selected === true) { 412 | result[key].push(option.value); 413 | 414 | if (element.multiple !== true) { 415 | break; 416 | } 417 | } 418 | } 419 | } 420 | } else if (type === 'checkbox' || type === 'radio') { 421 | element.checked = (value === true) || values.includes(element.value); 422 | 423 | if (element.checked === true) { 424 | result[key].push(element.value); 425 | } 426 | } else if (typeof value === 'string') { 427 | if (element.isContentEditable === true) { 428 | result[key].push(element.textContent = value); 429 | } else { 430 | result[key].push(element.value = value); 431 | } 432 | } 433 | 434 | for (let trigger of ['input', 'change']) { 435 | element.dispatchEvent(new Event(trigger, { 'bubbles': true })); 436 | } 437 | 438 | try { 439 | element.blur(); 440 | element.dispatchEvent(new Event('blur')); 441 | } catch (error) { 442 | } 443 | 444 | if (type === 'checkbox' || type === 'radio') { 445 | break; 446 | } 447 | } 448 | } 449 | 450 | return result; 451 | }; 452 | 453 | return this.evaluate(callback as unknown as EvaluateFn, data) as any; 454 | }; 455 | 456 | Super.prototype.getInnerHTML = function () { 457 | return this.evaluate((node: Element) => { 458 | return (node as HTMLElement).innerHTML; 459 | }); 460 | }; 461 | 462 | Super.prototype.getInnerText = function () { 463 | return this.evaluate((node: Element) => { 464 | return (node as HTMLElement).innerText; 465 | }); 466 | }; 467 | 468 | Super.prototype.number = function (decimal: string = '.', property: KeysOfType = 'textContent' as any) { 469 | let callback = (node: T, decimal: string, property: KeysOfType) => { 470 | let data = (node[property] as unknown) as string; 471 | 472 | if (typeof data === 'string') { 473 | decimal = decimal ?? '.'; 474 | 475 | if (typeof decimal === 'string') { 476 | decimal = decimal.replace(/[.]/g, '\\$&'); 477 | } 478 | 479 | let matches = data.match(/((?:[-+]|\b)[0-9]+(?:[ ,.'`´]*[0-9]+)*)\b/g); 480 | 481 | if (matches != null) { 482 | return matches.map((value) => parseFloat(value.replace(new RegExp(`[^-+0-9${decimal}]+`, 'g'), '').replace(decimal, '.'))); 483 | } 484 | } 485 | 486 | return null; 487 | }; 488 | 489 | return this.evaluate(callback as unknown as EvaluateFn, decimal, property as any); 490 | }; 491 | 492 | Super.prototype.selectByLabel = function (...values: string[]) { 493 | for (let value of values) { 494 | console.assert(typeof value === 'string', `Values must be strings. Found value '${value}' of type '${typeof value}'.`); 495 | } 496 | 497 | let callback = (node: HTMLSelectElement, values: string[]) => { 498 | if (node.nodeName.toLowerCase() !== 'select') { 499 | throw new Error('Element is not a