[
  {
    "path": ".dockerignore",
    "content": "node_modules\nyarn-error.log\nnpm-debug.log\nDockerfile\n.dockerignore\n.git\n.gitignore\ntestdir/privatefile\n"
  },
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"extends\": \"eslint-config-populist\",\n  \"rules\": {\n    \"strict\": \"warn\",\n    \"indent\": [\"warn\", 2],\n    \"valid-jsdoc\": \"warn\",\n    \"no-undefined\": \"warn\",\n    \"comma-dangle\": \"warn\",\n    \"callback-return\": [\"warn\", [\"next\"]]\n  }\n}\n"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "# Contributing to http-server\n\n> Please read these guidelines before submitting an issue, filing a feature request, or contributing code.\n\n## :bug: I Found a Bug\n\nSorry! It happens to the best of us. If you've found a bug in http-server, **please [search](https://github.com/http-party/http-server/issues/) to see if it's already been reported**. Otherwise, create a [new issue](https://github.com/http-party/http-server/issues/new). If you can fix the bug yourself, feel free to create a [pull request](#propose-a-change) thereafter.\n\nPlease include _as much detail as possible_ to help us reproduce and diagnose the bug. Most importantly:\n\n- Make use of the issue template!\n- Let us know _how_ you're running http-server (options, flags, environment, etc.)\n- Include your test code or file(s). If large, please provide a link to a repository or [gist](https://gist.github.com).\n- Please show code in JavaScript only (any version)\n\nIf we need more information from you, we'll let you know. If you don't within a reasonable time frame (TBD), your issue will be automatically tagged as stale and eventually closed for inactivity.\n\n## :exclamation: Propose a Change\n\nBefore you get your hands dirty, please [search](https://github.com/http-party/http-server/issues/) for a related issue, or [create a new one](https://github.com/http-party/http-server/issues/new). If you wish to contribute a new feature, this is doubly important! Let's discuss your proposed changes first; we don't want you to waste time implementing a change that is at odds with the project's direction. That said, we'll happily consider any contribution, no matter how great or small.\n\n### :shoe: Contributing Code: Step-by-Step\n\nFollow these steps to get going.\n\n1. [Install the latest version of Node.js](https://nodejs.org/en/download).\n    - If you're new to installing Node, a tool like [nvm](https://github.com/creationix/nvm#install-script) can help you manage multiple version installations.\n1. Follow [Github's documentation](https://help.github.com/articles/fork-a-repo/) on setting up Git, forking and cloning.\n1. Create a new branch in your fork, giving it a descriptive name\n1. Execute `npm install` to install the prod and dev dependencies\n   - Do not use `yarn install` for development, as it may not get the same package versions as other developers.\n1. Make your changes and add them via `git add`.\n   - **Tests are required** for any non-trivial code change. If you're having trouble making tests, go ahead and open the pull request and we can help\n   - Keep your PR focused. Don't fix multiple things at once, and don't upgrade dependencies unless necessary.\n1. Before committing, run `npm test`\n   - Tests will also run on your PR, but running them locally will let you catch problems ahead-of-time.\n1. Commit your changes.\n   - See [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/).\n   - **Please do not use \"Conventional Commits\" style**\n1. Push your changes to your fork.\n1. Now on [http-party/http-server](https://github.com/http-party/http-server), you should see a notification about your recent changes in your fork's branch, with a green button to create a pull request. Click the button.\n1. Describe your changes in detail here, following the template. Once you're satisfied, submit the form.\n1. Be patient while your PR is reviewed. This can take a while. We may request changes, but don't be afraid to question them.\n1. Your PR might become conflicted with the code in `master`. If this is the case, you will need to [update your PR](#up-to-date) and resolve your conflicts.\n1. You don't need to make a new PR to any needed changes. Instead, commit on top of your changes, and push these to your fork's branch. The PR will be updated, and CI will re-run.\n   - **Please do not rebase and force-push**, it ruins the git history\n\n## :angel: I Just Want To Help\n\n_Excellent._ Here's how:\n\n- **Handy with JavaScript?** Please check out the issues labeled [`help-wanted`](https://github.com/http-party/http-server/issues?q=is%3Aopen+is%3Aissue+label%3A%22help-wanted%22) or [`good first issue`](https://github.com/http-party/http-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Agood+first+issue). \n- **Wait--you write unit tests for _fun_?** A PR which increases coverage is unlikely to ever be turned down.\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: thornjad\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\n---\n\n<!-- Describe the issue briefly here. -->\n\n#### Environment Versions\n\n1. OS Type\n1. Node version: `$ node --version`\n1. http-server version: `$ http-server --version`\n\n#### Steps to reproduce\n\n<!-- Include the actual command -->\n\n1. ...\n2. ...\n3. ...\n\n#### Expected result\n\n...\n\n#### Actual result\n\n<!-- Include full output and/or stack trace -->\n\n...\n\n#### Other information\n\n<!-- Include related issues, suggestions for a fix or further debug information, etc. -->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\n---\n\n#### What's the problem this feature will solve?\n\n<!-- What are you trying to do, that you are unable to achieve with http-server as it currently stands? -->\n\n#### Describe the solution you'd like\n\n<!-- A clear and concise description of what you want to happen. -->\n\n<!-- Provide examples of real-world use cases that this would enable and how it solves the problem described above. -->\n\n#### Alternative Solutions\n\n<!-- Have you tried to workaround the problem using http-server or other tools? Or a different approach to solving this issue? Please elaborate here. -->\n\n#### Additional context\n\n<!-- Add any other context, links, etc. about the feature here. -->\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "<!--- Describe the changes here. --->\n\n##### Relevant issues\n\n<!--\n    Link to the issue(s) this pull request fixes here, if applicable: \"Fixes #xxx\" or \"Resolves #xxx\"\n    \n    If your PR fixes multiple issues, list them individually like \"Fixes #xx1, fixes #xx2, fixes #xx3\". This is a quirk of how GitHub links issues.\n-->\n\n##### Contributor checklist\n\n- [ ] Provide tests for the changes (unless documentation-only)\n- [ ] Documented any new features, CLI switches, etc. (if applicable)\n    - [ ] Server `--help` output\n    - [ ] README.md\n    - [ ] doc/http-server.1 (use the same format as other entries)\n- [ ] The pull request is being made against the `master` branch\n\n##### Maintainer checklist\n\n- [ ] Assign a version triage tag\n- [ ] Approve tests if applicable\n"
  },
  {
    "path": ".github/release-drafter.yml",
    "content": "name-template: 'v$RESOLVED_VERSION'\ntag-template: 'v$RESOLVED_VERSION'\n\nchange-template: '- $TITLE @$AUTHOR (#$NUMBER)'\nchange-title-escapes: '\\<*_&'\n\ncategories:\n  - title: 'Breaking changes'\n    labels:\n      - 'major version'\n  - title: 'Features and enhancements'\n    labels:\n      - 'feature'\n      - 'enhancement'\n  - title: 'Bug Fixes'\n    labels:\n      - 'fix'\n      - 'bug'\n  - title: 'Other changes'\n    labels:\n      - 'dependencies'\n      - 'documentation'\n\nexclude-labels:\n  - \"skip-changelog\"\n  - \"maintenance\"\n  - \"trivial\"\n\nversion-resolver:\n  major:\n    labels:\n      - 'major version'\n  minor:\n    labels:\n      - 'minor version'\n  patch:\n    labels:\n      - 'patch version'\n  default: patch\n\ntemplate: |\n  $CHANGES\n"
  },
  {
    "path": ".github/workflows/node.js.yml",
    "content": "# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node\n# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions\n\nname: Node.js CI\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  build:\n    name: Test\n    runs-on: ${{ matrix.os }}\n\n    strategy:\n      fail-fast: false\n      matrix:\n        node-version: [20.x, 22.x, 24.x]\n        os: [ubuntu-latest, macOS-latest, windows-latest]\n\n    steps:\n    - uses: actions/checkout@v4.2.2\n    - name: Use Node.js ${{ matrix.node-version }} on ${{ matrix.os }}\n      uses: actions/setup-node@v4.1.0\n      with:\n        node-version: ${{ matrix.node-version }}\n        cache: 'npm'\n    - run: npx npm@7 ci\n    - run: npm test\n"
  },
  {
    "path": ".github/workflows/release-drafter.yml",
    "content": "name: release-drafter\n\non:\n  push:\n    # branches to consider in the event; optional, defaults to all\n    branches:\n      - master\n\njobs:\n  update_release_draft:\n    permissions:\n      contents: write\n      pull-requests: read\n    if: github.repository == 'http-party/http-server'\n    runs-on: ubuntu-latest\n    steps:\n      # Drafts your next release notes as pull requests are merged into master\n      - uses: release-drafter/release-drafter@v5\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/stale.yml",
    "content": "name: Mark stale issues and pull requests\n\non:\n  schedule:\n  - cron: '25 12 * * *'\n\njobs:\n  stale:\n\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n      pull-requests: write\n\n    steps:\n    - uses: actions/stale@v4.0.0\n      with:\n        repo-token: ${{ secrets.GITHUB_TOKEN }}\n        days-before-stale: 360\n        days-before-issue-stale: 180\n        days-before-pr-stale: 360\n        stale-issue-message: 'This issue has been inactive for 180 days'\n        stale-pr-message: 'This pull request has been inactive for 360 days'\n        stale-issue-label: 'stale'\n        stale-pr-label: 'stale'\n        exempt-issue-labels: 'no-stale'\n        exempt-pr-labels: 'no-stale'\n        exempt-all-milestones: true\n        days-before-close: -1\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules/\nnpm-debug.log*\n.nyc_*/\n.dir-locals.el\n.DS_Store\n.httpserver*\n.tap\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\njademichael @ jmthornton.net.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM node:16-alpine\nVOLUME /public\nWORKDIR /srv/http-server\nCOPY package.json package-lock.json ./\nRUN npm install --production\nCOPY . .\nEXPOSE 8080\nENTRYPOINT [\"node\", \"./bin/http-server\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2011-2026 Charlie Robbins, Marak Squires, Jade Michael Thornton and the Contributors.\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "[![GitHub Workflow Status (master)](https://img.shields.io/github/actions/workflow/status/http-party/http-server/node.js.yml?style=flat-square&branch=master)](https://github.com/http-party/http-server/actions)\n[![npm](https://img.shields.io/npm/v/http-server.svg?style=flat-square)](https://www.npmjs.com/package/http-server) [![homebrew](https://img.shields.io/homebrew/v/http-server?style=flat-square)](https://formulae.brew.sh/formula/http-server) [![npm downloads](https://img.shields.io/npm/dm/http-server?color=blue&label=npm%20downloads&style=flat-square)](https://www.npmjs.com/package/http-server)\n[![license](https://img.shields.io/github/license/http-party/http-server.svg?style=flat-square)](https://github.com/http-party/http-server/blob/master/LICENSE)\n\n# http-server: a simple static HTTP server\n\n`http-server` is a simple, zero-configuration command-line static HTTP server.  It is powerful enough for production usage, but it's simple and hackable enough to be used for testing, local development and learning.\n\n![Example of running http-server](https://github.com/http-party/http-server/raw/master/screenshots/public.png)\n\n## Installation:\n\n#### Running on-demand:\n\nUsing `npx` you can run the script without installing it first:\n\n    npx http-server [path] [options]\n\n#### Globally via `npm`\n\n    npm install --global http-server\n\nThis will install `http-server` globally so that it may be run from the command line anywhere.\n\n#### Globally via Homebrew\n\n    brew install http-server\n     \n#### As a dependency in your `npm` package:\n\n    npm install http-server\n\n#### Using Docker\n\nNote: a public image is not provided currently, but you can build one yourself\nwith the provided Dockerfile.\n\n1. Create an image\n   ```\n   docker build -t my-image .\n   ```\n2. Run a container\n   ```\n   docker run -p 8080:8080 -v \"${pwd}:/public\" my-image\n   ```\n   In the example above we're serving the directory `./` (working directory).\n   If you wanted to serve `./test` you'd replace `${pwd}` with `${pwd}/test`.\n\n## Usage:\n\n     http-server [path] [options]\n\n`[path]` defaults to `./public` if the folder exists, and `./` otherwise.\n\n*Now you can visit http://localhost:8080 to view your server*\n\n**Note:** Caching is on by default. Add `-c-1` as an option to disable caching.\n\n## Available Options:\n\n| Command         | \tDescription         | Defaults  |\n| -------------  |-------------|-------------|\n|`-p` or `--port` |Port to use. Use `-p 0` to look for an open port, starting at 8080. It will also read from `process.env.PORT`. |8080 |\n|`-a`   |Address to use |0.0.0.0|\n|`--base-dir` | Base path to serve files from | `/` |\n|`-d`     |Show directory listings |`true` |\n|`-dir-overrides-404` | Whether `-d` should override magic `404.html` | `false`\n|`-i`   | Display autoIndex | `true` |\n|`-g` or `--gzip` |When enabled it will serve `./public/some-file.js.gz` in place of `./public/some-file.js` when a gzipped version of the file exists and the request accepts gzip encoding. If brotli is also enabled, it will try to serve brotli first.|`false`|\n|`-b` or `--brotli`|When enabled it will serve `./public/some-file.js.br` in place of `./public/some-file.js` when a brotli compressed version of the file exists and the request accepts `br` encoding. If gzip is also enabled, it will try to serve brotli first. |`false`|\n|`-e` or `--ext`  |Default file extension if none supplied |`html` | \n|`-s` or `--silent` |Suppress log messages from output  | |\n|`--coop` |Enable COOP via the `Cross-Origin-Opener-Policy` header  | |\n|`--cors` |Enable CORS via the `Access-Control-Allow-Origin` header  | |\n|`--private-network-access` |Enable Private Network Access via the `Access-Control-Allow-Private-Network` header  | |\n|`--cors` | Enable CORS via the `Access-Control-Allow-Origin: *` header. Optionally provide comma-separated values to add to `Access-Control-Allow-Headers`  | |\n|`-H` or `--header` |Add an extra response header (can be used several times)  | |\n|`-o [path]` |Open browser window after starting the server. Optionally provide a URL path to open. e.g.: -o /other/dir/ | |\n|`-c` |Set cache time (in seconds) for cache-control max-age header, e.g. `-c10` for 10 seconds. To disable caching, use `-c-1`.|`3600` |\n|`-t` |Connection timeout in seconds, e.g. `-t60` for 1 minute. To disable timeout, use `-t0`.|`120` |\n|`-T` or `--title` |Custom title suffix for the terminal window. The title will be \"http-server PORT [TITLE]\".| |\n|`-U` or `--utc` |Use UTC time format in log messages.| |\n|`--log-ip` |Enable logging of the client's IP address |`false` |\n|`-P` or `--proxy` |Proxies all requests which can't be resolved locally to the given url. e.g.: -P http://someurl.com | |\n|`--proxy-options` |Pass proxy [options](https://github.com/http-party/node-http-proxy#options) using nested dotted objects. e.g.: --proxy-options.secure false | |\n|`--proxy-config` |Pass in `.json` configuration file or stringified JSON. e.g.: `./path/to/config.json` | |\n|`--proxy-all` |Forward every request to the proxy target instead of serving local files|`false`|\n|`--proxy-options` |Pass proxy [options](https://github.com/http-party/node-http-proxy#options) using nested dotted objects. e.g.: --proxy-options.secure false |\n|`--user` or `--username` |Username for basic authentication | |\n|`--password` |Password for basic authentication | |\n|`-S`, `--tls` or `--ssl` |Enable secure request serving with TLS/SSL (HTTPS)|`false`|\n|`-C` or `--cert` |Path to ssl cert file |`cert.pem` |\n|`-K` or `--key` |Path to ssl key file |`key.pem` |\n|`-r` or `--robots` | Automatically provide a /robots.txt (The content of which defaults to `User-agent: *\\nDisallow: /`)  | `false` |\n|`--no-dotfiles` |Do not show dotfiles| |\n|`--mimetypes` |Path to a .types file for custom mimetype definition| |\n|`--hide-permissions` |Do not show file permissions| |\n|`--allowed-hosts` |Comma-separated list of hosts allowed to access the server. e.g.: `--allowed-hosts localhost,example.com`| |\n|`-h` or `--help` |Print this list and exit. |   |\n|`-v` or `--version`|Print the version and exit. | |\n| `--no-panic` | Don't print error stack in the console, put it in a log file | `false`|\n\n## Magic Files\n\n- `index.html` will be served as the default file to any directory requests.\n- `404.html` will be served if a file is not found. This can be used for Single-Page App (SPA) hosting to serve the entry page.\n\n## Catch-all redirect\n\nTo implement a catch-all redirect, use the index page itself as the proxy with:\n\n```\nhttp-server --proxy http://localhost:8080?\n```\n\nNote the `?` at the end of the proxy URL. Thanks to [@houston3](https://github.com/houston3) for this clever hack!\n\n## TLS/SSL\n\nFirst, you need to make sure that [openssl](https://github.com/openssl/openssl) is installed correctly, and you have `key.pem` and `cert.pem` files. You can generate them using this command:\n\n``` sh\nopenssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out cert.pem\n```\n\nYou will be prompted with a few questions after entering the command. Use `127.0.0.1` as value for `Common name` if you want to be able to install the certificate in your OS's root certificate store or browser so that it is trusted.\n\nThis generates a cert-key pair and it will be valid for 3650 days (about 10 years).\n\nThen you need to run the server with `-S` for enabling SSL and `-C` for your certificate file.\n\n``` sh\nhttp-server -S -C cert.pem\n```\n\nIf you wish to use a passphrase with your private key you can include one in the openssl command via the -passout parameter (using password of foobar)\n\n\ne.g.\n`openssl req -newkey rsa:2048 -passout pass:foobar -keyout key.pem -x509 -days 365 -out cert.pem`\n\nFor security reasons, the passphrase will only be read from the `NODE_HTTP_SERVER_SSL_PASSPHRASE` environment variable.\n\n\nThis is what should be output if successful:\n\n``` sh\nStarting up http-server, serving ./ through https\n\nhttp-server settings:\nCOOP: disabled\nCORS: disabled\nCache: 3600 seconds\nConnection Timeout: 120 seconds\nDirectory Listings: visible\nAutoIndex: visible\nServe GZIP Files: false\nServe Brotli Files: false\nDefault File Extension: none\n\nAvailable on:\n  https://127.0.0.1:8080\n  https://192.168.1.101:8080\n  https://192.168.1.104:8080\nHit CTRL-C to stop the server\n```\n\n# Development\n\nCheckout this repository locally, then:\n\n```sh\n$ npm i\n$ npm start\n```\n\n*Now you can visit http://localhost:8080 to view your server*\n\nYou should see the turtle image in the screenshot above hosted at that URL. See\nthe `./public` folder for demo content.\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\n| Version | Supported |\n|---------|------------------------|\n| 14.x  | ✔️ Yes |\n| 13.x  | 🔐 Security updates until April 2022 |\n| <= 0.13.x  | ❌ No  |\n\n## Reporting a Vulnerability\n\nIn general, vulnerabilities can be reported as an issue, pull requests are very welcome. If you'd like to report privately, please email jademichael+http-server@jmthornton.net.\n"
  },
  {
    "path": "bin/http-server",
    "content": "#!/usr/bin/env node\n\n'use strict';\n\nvar chalk     = require('chalk'),\n  os         = require('os'),\n  httpServer = require('../lib/http-server'),\n  portfinder = require('portfinder'),\n  opener     = require('opener'),\n  fs         = require('fs'),\n  url        = require('url');\nvar argv = require('minimist')(process.argv.slice(2), {\n  alias: {\n    tls: 'ssl',\n    header: 'H',\n    user: 'username',\n  },\n  boolean: ['proxy-all']\n});\nvar ifaces = os.networkInterfaces();\n\nif (argv.h || argv.help) {\n  console.log([\n    'usage: http-server [path] [options]',\n    '',\n    'options:',\n    '  -p --port    Port to use. If 0, look for open port. [8080]',\n    '  -a           Address to use [0.0.0.0] or [::]',\n    '  -d           Show directory listings [true]',\n    '  --dir-overrides-404   Whether -d should override magic 404.html [false]',\n    '  --base-dir   Base directory to serve files from [/]',\n    '  -i           Display autoIndex [true]',\n    '  -g --gzip    Serve gzip files when possible [false]',\n    '  -b --brotli  Serve brotli files when possible [false]',\n    '               If both brotli and gzip are enabled, brotli takes precedence',\n    '',\n    '  --force-content-encoding',\n    '               When using --gzip or --brotli, includes the content encoding',\n    '               header even when the extension for the compressed file is',\n    '               specified in the URL. \"test.png.br\" will be served the same',\n    '               way as \"test.png\".',\n    '',\n    '  -e --ext     Default file extension if none supplied [none]',\n    '  -s --silent  Suppress log messages from output',\n    '  --coop[=mode]   Enable COOP via the \"Cross-Origin-Opener-Policy\" header',\n    '                  Optionally provide COOP mode.',\n    '  --content-type     Default content type for unknown file types [application/octet-stream]',\n    '  --cors[=headers]   Enable CORS via the \"Access-Control-Allow-Origin\" header',\n    '                     Optionally provide CORS headers list separated by commas',\n    '  --private-network-access Enable Private Network Access via the',\n    '                           \"Access-Control-Allow-Private-Network\" header',\n    '                     When enabled, sets Access-Control-Allow-Origin to \"*\"',\n    '                     Optional value adds to Access-Control-Allow-Headers',\n    '  -H',\n    '  --header',\n    '               Add an extra response header (can be used several times)',\n    '  -o [path]    Open browser window after starting the server.',\n    '               Optionally provide a URL path to open the browser window to.',\n    '  -c           Cache time (max-age) in seconds [3600], e.g. -c10 for 10 seconds.',\n    '               To disable caching, use -c-1.',\n    '  -t           Connection timeout in seconds [120], e.g. -t60 for 1 minute.',\n    '               To disable timeout, use -t0.',\n    '  -T --title   Custom title suffix for the terminal window [none]',\n    '               The terminal title will be \"http-server PORT [TITLE]\"',\n    '  -U --utc     Use UTC time format in log messages.',\n    '  --log-ip     Enable logging of the client\\'s IP address',\n    '',\n    '  -P --proxy       Fallback proxy if the request cannot be resolved. e.g.: http://someurl.com',\n    '  --proxy-all      Send every request to the proxy target instead of serving local files',\n    '  --proxy-options  Pass options to proxy using nested dotted objects. e.g.: --proxy-options.secure false',\n    '  --proxy-config   Pass in .json configuration file. e.g.: ./path/to/config.json',\n    '  --websocket      Enable websocket proxy',\n    '',\n    '  --user --username   Username for basic authentication [none]',\n    '                      Can also be specified with the env variable NODE_HTTP_SERVER_USERNAME',\n    '  --password   Password for basic authentication [none]',\n    '               Can also be specified with the env variable NODE_HTTP_SERVER_PASSWORD',\n    '',\n    '  -S --tls --ssl   Enable secure request serving with TLS/SSL (HTTPS)',\n    '  -C --cert    Path to TLS cert file (default: cert.pem)',\n    '  -K --key     Path to TLS key file (default: key.pem)',\n    '',\n    '  -r --robots        Respond to /robots.txt [User-agent: *\\\\nDisallow: /]',\n    '  --no-dotfiles      Do not show dotfiles',\n    '  --hide-permissions Do not show file permissions',\n    '  --mimetypes        Path to a .types file for custom mimetype definition',\n    '  -h --help          Print this list and exit.',\n    '  -v --version       Print the version and exit.',\n    '  --no-panic         If error occurs, gracefully shut down and create log file',\n    '                     Can also be specified with the env variable NODE_HTTP_SERVER_NO_PANIC',\n    '  --allowed-hosts    Comma-separated list of hosts allowed to access the server. e.g.: --allowed-hosts localhost,example.com',\n  ].join('\\n'));\n  process.exit();\n}\n\nvar port = argv.p || argv.port || parseInt(process.env.PORT, 10),\n    nopanic = !argv['panic'] || argv.n || process.env.NODE_HTTP_SERVER_NO_PANIC,\n    host = argv.a || '::',\n    tls = argv.S || argv.tls,\n    title = argv.T || argv.title,\n    sslPassphrase = process.env.NODE_HTTP_SERVER_SSL_PASSPHRASE,\n    proxy = argv.P || argv.proxy,\n    proxyOptions = argv['proxy-options'],\n    proxyConfig = argv['proxy-config'],\n    websocket = argv.websocket,\n    proxyAll = Boolean(argv['proxy-all']),\n    utc = argv.U || argv.utc,\n    version = argv.v || argv.version,\n    baseDir = argv['base-dir'],\n    logger,\n    allowedHosts = argv['allowed-hosts'];\n\nif (nopanic){\n  process.on('error', (e)=> {\n    // Results in a string like \"2021-12-27 14:56:31\"\n    const etime = new Date().toISOString().replace(/T/, ' ').replace(/\\..+/, '');\n    console.log(chalk.green(etime));\n    console.log(`${chalk.red('Fatal error: ')}${e.code}: ${e.message}`);\n    const filename = `httpserver-${etime.split(' ').join('_')}.log`;\n    console.log(chalk.bold(`Check ${filename} file in this folder.`));\n    fs.writeFileSync(filename, JSON.stringify(e));\n    process.exit(1);\n  });\n}\n\nvar proxyOptionsBooleanProps = [\n  'ws', 'xfwd', 'secure', 'toProxy', 'prependPath', 'ignorePath', 'changeOrigin',\n  'preserveHeaderKeyCase', 'followRedirects', 'selfHandleResponse'\n];\n\nif (proxyOptions) {\n  Object.keys(proxyOptions).forEach(function (key) {\n    if (proxyOptionsBooleanProps.indexOf(key) > -1) {\n      proxyOptions[key] = proxyOptions[key].toLowerCase() === 'true';\n    }\n  });\n}\n\nif (!argv.s && !argv.silent) {\n  logger = {\n    info: console.log,\n    warning: console.warn,\n    request: function (req, res, error) {\n      var date = utc ? new Date().toUTCString() : new Date();\n      var ip = argv['log-ip']\n        ? req.headers['x-forwarded-for'] || '' +  req.connection.remoteAddress\n        : '';\n      if (error) {\n        logger.info(\n          '[%s] %s \"%s %s\" Error (%s): \"%s\"',\n          date, ip, chalk.red(req.method), chalk.red(req.url),\n          chalk.red(error.status.toString()), chalk.red(error.message)\n        );\n      }\n      else if (req.proxy) {\n        logger.info(\n          '[%s] %s \"%s\" (%s)-> \"%s\"',\n          date, ip, chalk.cyan(req.url), chalk.magenta('Proxy'), chalk.cyan(req.proxy.target)\n        );\n      } else {\n        logger.info(\n          '[%s] %s \"%s %s\" \"%s\"',\n          date, ip, chalk.cyan(req.method), chalk.cyan(req.url),\n          req.headers['user-agent']\n        );\n      }\n    }\n  };\n} else if (chalk) {\n  logger = {\n    info: function () {},\n    request: function () {}\n  };\n}\n\nif (version) {\n  logger.info('v' + require('../package.json').version);\n  process.exit();\n}\n\nif (!port) {\n  portfinder.basePort = 8080;\n  portfinder.getPort(function (err, port) {\n    if (err) { throw err; }\n    listen(port);\n  });\n} else {\n  listen(port);\n}\n\nif (allowedHosts && typeof allowedHosts === 'string') {\n  allowedHosts = allowedHosts.split(',').map((host) => host.trim().toLowerCase());\n} else {\n  allowedHosts = undefined;\n}\n\nfunction listen(port) {\n  var options = {\n    root: argv._[0],\n    cache: argv.c,\n    timeout: argv.t,\n    showDir: argv.d,\n    dirOverrides404: argv['dir-overrides-404'],\n    baseDir: baseDir,\n    autoIndex: argv.i,\n    gzip: argv.g || argv.gzip,\n    brotli: argv.b || argv.brotli,\n    robots: argv.r || argv.robots,\n    ext: argv.e || argv.ext,\n    logFn: logger.request,\n    proxy: proxy,\n    proxyOptions: proxyOptions,\n    proxyConfig: proxyConfig,\n    proxyAll: proxyAll,\n    showDotfiles: argv.dotfiles,\n    hidePermissions: argv['hide-permissions'],\n    mimetypes: argv.mimetypes,\n    contentType: argv['content-type'],\n    username: argv.username || process.env.NODE_HTTP_SERVER_USERNAME,\n    password: argv.password || process.env.NODE_HTTP_SERVER_PASSWORD,\n    headers: {},\n    allowedHosts,\n  };\n\n  function setHeader(str) {\n    const m = /^(.+?)\\s*(:\\s*(.*))$/.exec(str);\n    if (!m || m.length < 4) {\n      options.headers[str] = '';\n    } else {\n      options.headers[m[1]] = m[3];\n    }\n  }\n\n  if (argv.coop) {\n    options.coop = true;\n    if (typeof argv.coop === 'string') {\n      options.coopHeader = argv.coop;\n    }\n  }\n\n  if (websocket) {\n    if (!proxy) {\n      logger.warning(chalk.yellow('WebSocket proxy will not be enabled because proxy is not enabled'));\n    } else {\n      options.websocket = true;\n    }\n  }\n\n  if (argv.cors) {\n    options.cors = true;\n    if (typeof argv.cors === 'string') {\n      options.corsHeaders = argv.cors;\n    }\n  }\n  \n  if ( argv['force-content-encoding'] ) {\n    options.forceContentEncoding = true;\n  }\n\n  if (argv.header) {\n    if (Array.isArray(argv.header)) {\n      argv.header.forEach(h => setHeader(h));\n    }\n    else {\n      setHeader(argv.header);\n    }\n  }\n\n  if (argv['private-network-access']) {\n    options.privateNetworkAccess = true;\n  }\n\n  if (proxy) {\n    try {\n      new url.URL(proxy);\n    } catch (err) {\n      logger.info(chalk.red('Error: Invalid proxy url'));\n      process.exit(1);\n    }\n  }\n\n  if (proxyConfig) {\n    try {\n      if (fs.existsSync(proxyConfig)) {\n        proxyConfig = fs.readFileSync(proxyConfig, 'utf8');\n      }\n      if (typeof proxyConfig === 'string') {\n        proxyConfig = JSON.parse(proxyConfig);\n      }\n      if (typeof proxyConfig !== 'object') {\n        throw new Error('Invalid proxy config');\n      }\n    }\n    catch (err) {\n      logger.info(chalk.red('Error: Invalid proxy config or file'));\n      process.exit(1);\n    }\n    // Proxy file overrides cli config\n    proxy = undefined;\n    proxyOptions = undefined;\n  }\n\n  if (proxyAll && proxyConfig) {\n    logger.info(chalk.red('Error: --proxy-all cannot be used with --proxy-config'));\n    logger.info(\n      '%s\\n%s\\n%s',\n      chalk.yellow('Hint: Use'),\n      chalk.cyan('\"/**\": {\\n  \"target\": \"your-proxy\"\\n}'),\n      chalk.yellow('in the proxy config to achieve the same effect.')\n    );\n    process.exit(1);\n  }\n\n  if (proxyAll && !proxy) {\n    logger.info(chalk.red('Error: --proxy-all requires --proxy to be set'));\n    process.exit(1);\n  }\n\n  if (tls) {\n    options.https = {\n      cert: argv.C || argv.cert || 'cert.pem',\n      key: argv.K || argv.key || 'key.pem',\n      passphrase: sslPassphrase\n    };\n    try {\n      fs.lstatSync(options.https.cert);\n    } catch (err) {\n      logger.info(chalk.red('Error: Could not find certificate ' + options.https.cert));\n      process.exit(1);\n    }\n    try {\n      fs.lstatSync(options.https.key);\n    } catch (err) {\n      logger.info(chalk.red('Error: Could not find private key ' + options.https.key));\n      process.exit(1);\n    }\n  }\n\n  var server = httpServer.createServer(options);\n  server.listen(port, host, function () {\n    // Set process title with port and optional custom suffix\n    process.title = 'http-server ' + port + (title ? ' ' + title : '');\n\n    var protocol = tls ? 'https://' : 'http://',\n      path = baseDir ? '/' + baseDir.replace(/^\\//, '') : '';\n\n    logger.info([\n      chalk.yellow('Starting up http-server, serving '),\n      chalk.cyan(server.root),\n      tls ? (chalk.yellow(' through') + chalk.cyan(' https')) : ''\n    ].join(''));\n\n    logger.info([chalk.yellow('\\nhttp-server version: '), chalk.cyan(require('../package.json').version)].join(''));\n\n    logger.info([\n      chalk.yellow('\\nhttp-server settings: '),\n      ([chalk.yellow('COOP: '), argv.coop ? chalk.cyan(argv.coop) : chalk.red('disabled')].join('')),\n      ([chalk.yellow('CORS: '), argv.cors ? chalk.cyan(argv.cors) : chalk.red('disabled')].join('')),\n      ([chalk.yellow('Private Network Access: '), argv['private-network-access'] ? chalk.cyan(argv['private-network-access']) : chalk.red('disabled')].join('')),\n      ([chalk.yellow('Cache: '), argv.c ? (argv.c === '-1' ? chalk.red('disabled') : chalk.cyan(argv.c + ' seconds')) : chalk.cyan('3600 seconds')].join('')),\n      ([chalk.yellow('Connection Timeout: '), Math.max(0, argv.t) === 0 ? chalk.red('disabled') :\n        ((!isNaN(argv.t) && !isNaN(parseFloat(argv.t))) ?\n          chalk.cyan(Number(argv.t) + ' seconds') : chalk.cyan('120 seconds'))].join('')),\n      ([chalk.yellow('Directory Listings: '), argv.d ? chalk.red('not visible') : chalk.cyan('visible')].join('')),\n      ([chalk.yellow('AutoIndex: '), argv.i ? chalk.red('not visible') : chalk.cyan('visible')].join('')),\n      ([chalk.yellow('Serve GZIP Files: '), argv.g || argv.gzip ? chalk.cyan('true') : chalk.red('false')].join('')),\n      ([chalk.yellow('Serve Brotli Files: '), argv.b || argv.brotli ? chalk.cyan('true') : chalk.red('false')].join('')),\n      ([chalk.yellow('Default File Extension: '), argv.e ? chalk.cyan(argv.e) : (argv.ext ? chalk.cyan(argv.ext) : chalk.red('none'))].join('')),\n      ([chalk.yellow('Base directory: '), baseDir ? chalk.cyan(baseDir) : chalk.cyan('/')].join(''))\n    ].join('\\n'));\n\n    if (options.headers) {\n      logger.info(chalk.yellow('Additional Headers:'));\n      for (let k in options.headers) {\n        let v = options.headers[k];\n        logger.info(chalk.yellow(`\\t${k}:`) + chalk.cyan(` ${v}`));\n      }\n    }\n\n    logger.info(chalk.yellow('\\nAvailable on:'));\n\n    if (allowedHosts) {\n      for (const host of allowedHosts) {\n        logger.info(`  ${protocol}${host}:${chalk.green(port.toString())}${path}`);\n      }\n    } else if (argv.a && (host !== '::' || host !== '0.0.0.0')) {\n      logger.info(`  ${protocol}${host}:${chalk.green(port.toString())}${path}`);\n    } else {\n      Object.keys(ifaces).forEach(function (dev) {\n        ifaces[dev].forEach(function (details) {\n          if (details.family === 'IPv4' || details.family === 4) {\n            logger.info(('  ' + protocol + details.address + ':' + chalk.green(port.toString()) + path));\n          }\n          if (details.family === 'IPv6' && !details.address.startsWith(\"fe80\") ) { // Ignoring Ipv6-Link Local addresses\n            logger.info(('  ' + protocol + details.address + ':' + chalk.green(port.toString())));\n          }\n        });\n      });\n    }\n\n    if (typeof proxy === 'string') {\n      if (proxyOptions) {\n        logger.info('Unhandled requests will be served from: ' + proxy + '. Options: ' + JSON.stringify(proxyOptions));\n      } else {\n        logger.info('Unhandled requests will be served from: ' + proxy);\n      }\n    }\n\n    // Set up \"CTRL-C\" hook, before printing out \"Hit CTRL-C to stop the server\"\n    function stopServer() {\n      server.close();\n      logger.info(chalk.red('http-server stopped.'));\n      process.exit();\n    }\n\n    process.on('SIGINT', stopServer);\n    process.on('SIGTERM', stopServer);\n\n    if (process.platform === 'win32') {\n      require('readline').createInterface({\n        input: process.stdin,\n        output: process.stdout\n      }).on('SIGINT', function () {\n        process.emit('SIGINT');\n      });\n    }\n\n    logger.info('Hit CTRL-C to stop the server');\n\n    if (argv.o) {\n      let openHost = host\n      if ('::' === host || '0.0.0.0'===host){\n        openHost = '127.0.0.1'\n      }\n      let openUrl = `${protocol}${openHost}:${port}`;\n      if (typeof argv.o === 'string') {\n        openUrl += argv.o[0] === '/' ? argv.o : '/' + argv.o;\n      }\n      logger.info('Open: ' + openUrl);\n      opener(openUrl);\n    }\n\n    // Spacing before logs\n    if (!argv.s) logger.info();\n  });\n}\n"
  },
  {
    "path": "doc/http-server.1",
    "content": ".TH http-server 1 \"April 2020\" GNU \"http-server man page\"\n\n.SH NAME\nhttp-server \\- a simple zero-configuration command-line http server\n\n.SH SYNOPSIS\n.B http-server\n[\\fIPATH\\fR]\n[\\fIOPTIONS\\fR]\n\n.SH DESCRIPTION\n\\fBhttp-server\\fR is a simple, zero-configuration command-line http server. It is powerful enough for production usage, but it's simple and hackable enough to be used for testing, local development, and learning.\n\n.SH OPTIONS\n\n.TP\n.BI [\\fIPATH\\fR]\nThe directory to serve.\nDefaults to ./public if it exists, and ./ otherwise.\n\n.TP\n.BI \\-p \", \" \\-\\-port \" \" \\fIPORT\\fR\nPort to use. If 0, look for the first available port, starting at 8080.\nDefault is 8080.\n\n.TP\n.BI \\-a \" \" \\fIADDRESS\\fR\nAddress to use.\nDefault is 0.0.0.0.\n\n.TP\n.BI \\-d\nShow directory listings.\nDefault is true.\n\n.TP\n.BI \\-d\nWhether -d should override magic 404.html\nDefault is false.\n\n.TP\n.BI \\-i\nDisplay autoIndex.\nDefault is true.\n\n.TP\n.BI \\-g \", \" \\-\\-gzip\nServe gzip files when possible.\nDefault is false.\n\n.TP\n.BI \\-b \", \" \\-\\-brotli\nServe brotli files when possible.\nIf both brotli and gzip are enabled, brotli takes precedence.\nDefault is false.\n\n.TP\n.BI \\-\\-force\\-content\\-encoding\nWhen using --gzip or --brotli, includes the content encoding\nheader even when the extension for the compressed file is\nspecified in the URL. \"test.png.br\" will be served the same\nway as \"test.png\".\n\n.TP\n.BI \\-e \", \" \\-\\-ext \" \" \\fIEXTENSION\\fR\nDefault file extension is none is provided.\n\n.TP\n.BI \\-s \", \" \\-\\-silent\nSuppress log messages from output.\n\n.TP\n.BI \\-n \", \" \\-\\-no-panic\nGracefully shut down whenever a fatal error occurs, sending stack to log file, not console.\n\n.TP\n.BI \\-\\-coop \" \" [\\fIMODE\\fR]\nEnable COOP via the \"Cross-Origin-Opener-Policy\" header and sets\nthe \"Cross-Origin-Embedder-Policy\" header to \"require-corp\".\nOptionally provide COOP mode which defaults to \"same-origin\".\n\n.TP\n.BI \\-\\-cors \" \" [\\fIHEADERS\\fR]\nEnable CORS by setting \"Access-Control-Allow-Origin\" to \"*\".\nOptional comma-separated headers list adds to \"Access-Control-Allow-Headers\".\nDefault Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Range.\n\n.TP\n.BI \\-H \", \" \\-\\-header \" \" \\fIHEADER\\fR\nAdd an extra response header (can be used several times)\n\n.TP\n.BI \\-\\-private-network-access\nEnable Private Network Access via the \"Access-Control-Allow-Private-Network\" header.\n\n.TP\n.BI \\-o \" \" [\\fIPATH\\fR]\nOpen default browser window after starting the server.\nOptionally provide a URL path to open the browser window to.\n\n.TP\n.BI \\-c \" \" \\fITIME\\fR\nCache time (max-age) in seconds.\nTo disable caching, use \\-c \\-1.\nDefault is 3600.\n\n.TP\n.BI \\-t \" \" \\fITIMEOUT\\fR\nConnection timeout in seconds, e.g. -t60 for 1 minute.\nTo disable timeout, use \\-t0.\nDefault is 120.\n\n.TP\n.BI \\-T \", \" \\-\\-title \" \" \\fITITLE\\fR\nCustom title suffix for the terminal window.\nThe terminal title will be \"http-server PORT [TITLE]\".\n\n.TP\n.BI \\-U \", \" \\-\\-utc\nUse UTC time format in log messages.\n\n.TP\n.BI \\-\\-log\\-ip\nEnable logging of the client IP address.\n\n.TP\n.BI \\-P \", \" \\-\\-proxy\nFallback proxy if the request cannot be resolved.\n\n.TP\n.BI \\-\\-proxy\\-all\nForward every request to the proxy target and disable local file serving.\nRequires \\-\\-proxy.\n\n.TP\n.BI \\-\\-proxy\\-options\nPass proxy options using nested dotted objects.\n\n.TP\n.BI \\-\\-proxy\\-config\nPass in .json configuration file.\n\n.TP\n.BI \\-\\-user \", \" \\-\\-username \" \" \\fIUSERNAME\\fR\nUsername for basic authentication.\nCan also be specified with the environment variable NODE_HTTP_SERVER_USERNAME.\nDefaults to none.\n\n.TP\n.BI \\-\\-password \" \" \\fIPASSWORD\\fR\nPassword for basic authentication.\nCan also be specified with the environment variable NODE_HTTP_SERVER_PASSWORD.\nDefaults to none.\n\n.TP\n.BI \\-S \", \" \\-\\-tls \", \" \\-\\-ssl\nEnable https.\n\n.TP\n.BI \\-C \", \" \\-\\-cert \" \" [\\fIFILE\\fR]\nPath to SSL certificate file.\nIf not specified, uses cert.pem.\n\n.TP\n.BI \\-K \", \" \\-\\-key \" \" [\\fIFILE\\fR]\nPath to SSL key file.\nIf not specified, uses key.pem.\nPassphrase will be read from NODE_HTTP_SERVER_SSL_PASSPHRASE (if set)\n\n.TP\n.BI \\-r \", \" \\-\\-robots \" \" [\\fIUSER\\-AGENT\\fR]\nRespond to /robots.txt request.\nIf not specified, uses \"User-agent: *\\\\nDisallow: /]\"\n\n.TP\n.BI \\-\\-no\\-dotfiles\nDo not show dotfiles.\n\n.TP\n.BI \\-\\-hide\\-permissions\nDo not show file permissions.\n\n.TP\n.BI \\-\\-allowed\\-hosts\nComma-separated list of hosts allowed to access the server. e.g.: \\-\\-allowed\\-hosts localhost,example.com\n\n.TP\n.BI \\-h \", \" \\-\\-help\nPrint usage and exit.\n\n.TP\n.BI \\-v \", \" \\-\\-version\nPrint version and exit.\n\n.SH FILES\n.B index.html\nwill be served as the default file to any directory requests.\n\n.B 404.html\nwill be served if a file is not found. This can be used for SPA hosting to serve the entry page.\n\n.SH COPYING\nCopyright (c) 2011-2022 Charlie Robbins, Marak Squires, and the Contributors.\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n.SH VERSION\nVersion 0.12.2\n"
  },
  {
    "path": "lib/core/aliases.json",
    "content": "{\n  \"autoIndex\": [ \"autoIndex\", \"autoindex\" ],\n  \"showDir\": [ \"showDir\", \"showdir\" ],\n  \"dirOverrides404\": [\n    \"dirOverrides404\", \n    \"diroverrides404\", \n    \"dir-overrides-404\",\n    \"listingsOverride404\",\n    \"listings-override-404\"\n  ],\n  \"showDotfiles\": [\"showDotfiles\", \"showdotfiles\"],\n  \"humanReadable\": [ \"humanReadable\", \"humanreadable\", \"human-readable\" ],\n  \"hidePermissions\": [\"hidePermissions\", \"hidepermissions\", \"hide-permissions\"],\n  \"si\": [ \"si\", \"index\" ],\n  \"handleError\": [ \"handleError\", \"handleerror\" ],\n  \"coop\": [ \"coop\", \"COOP\" ],\n  \"cors\": [ \"cors\", \"CORS\" ],\n  \"privateNetworkAccess\": [ \"privateNetworkAccess\", \"privatenetworkaccess\", \"private-network-access\" ],\n  \"headers\": [ \"H\", \"header\", \"headers\" ],\n  \"contentType\": [ \"contentType\", \"contenttype\", \"content-type\" ],\n  \"mimeType\": [\n    \"mimetype\",\n    \"mimetypes\",\n    \"mimeType\",\n    \"mimeTypes\",\n    \"mime-type\",\n    \"mime-types\",\n    \"mime-Type\",\n    \"mime-Types\"\n  ],\n  \"weakEtags\": [ \"weakEtags\", \"weaketags\", \"weak-etags\" ],\n  \"weakCompare\": [\n    \"weakcompare\",\n    \"weakCompare\",\n    \"weak-compare\",\n    \"weak-Compare\"\n  ],\n  \"handleOptionsMethod\": [\n    \"handleOptionsMethod\",\n    \"handleoptionsmethod\",\n    \"handle-options-method\"\n  ]\n}\n"
  },
  {
    "path": "lib/core/etag.js",
    "content": "'use strict';\n\nmodule.exports = (stat, weakEtag) => {\n  let etag = `\"${[stat.ino, stat.size, stat.mtime.toISOString()].join('-')}\"`;\n  if (weakEtag) {\n    etag = `W/${etag}`;\n  }\n  return etag;\n};\n"
  },
  {
    "path": "lib/core/index.js",
    "content": "#! /usr/bin/env node\n\n'use strict';\n\nconst path = require('path');\nconst fs = require('fs');\nconst url = require('url');\nconst { Readable } = require('stream');\nconst buffer = require('buffer');\nconst mime = require('mime');\nconst urlJoin = require('url-join');\nconst showDir = require('./show-dir');\nconst version = require('../../package.json').version;\nconst status = require('./status-handlers');\nconst generateEtag = require('./etag');\nconst optsParser = require('./opts');\nconst htmlEncodingSniffer = require('html-encoding-sniffer');\n\nlet httpServerCore = null;\n\nfunction decodePathname(pathname) {\n  const pieces = pathname.replace(/\\\\/g, '/').split('/');\n\n  const normalized = path.normalize(pieces.map((rawPiece) => {\n    const piece = decodeURIComponent(rawPiece);\n\n    if (process.platform === 'win32' && /\\\\/.test(piece)) {\n      throw new Error('Invalid forward slash character');\n    }\n\n    return piece;\n  }).join('/'));\n  return process.platform === 'win32'\n    ? normalized.replace(/\\\\/g, '/') : normalized;\n}\n\nconst nonUrlSafeCharsRgx = /[\\x00-\\x1F\\x20\\x7F-\\uFFFF]+/g;\nfunction ensureUriEncoded(text) {\n  return text\n  return String(text).replace(nonUrlSafeCharsRgx, encodeURIComponent);\n}\n\n// Check to see if we should try to compress a file with gzip.\nfunction shouldCompressGzip(req) {\n  const headers = req.headers;\n\n  return headers && headers['accept-encoding'] &&\n    headers['accept-encoding']\n    .split(',')\n    .some(el => ['*', 'compress', 'gzip', 'deflate'].indexOf(el.trim()) !== -1)\n  ;\n}\n\nfunction shouldCompressBrotli(req) {\n  const headers = req.headers;\n\n  return headers && headers['accept-encoding'] &&\n    headers['accept-encoding']\n    .split(',')\n    .some(el => ['*', 'br'].indexOf(el.trim()) !== -1)\n  ;\n}\n\nfunction hasGzipId12(gzipped, cb) {\n  const stream = fs.createReadStream(gzipped, { start: 0, end: 1 });\n  let buffer = Buffer.from('');\n  let hasBeenCalled = false;\n\n  stream.on('data', (chunk) => {\n    buffer = Buffer.concat([buffer, chunk], 2);\n  });\n\n  stream.on('error', (err) => {\n    if (hasBeenCalled) {\n      throw err;\n    }\n\n    hasBeenCalled = true;\n    cb(err);\n  });\n\n  stream.on('close', () => {\n    if (hasBeenCalled) {\n      return;\n    }\n\n    hasBeenCalled = true;\n    cb(null, buffer[0] === 31 && buffer[1] === 139);\n  });\n}\n\n\nmodule.exports = function createMiddleware(_dir, _options) {\n  let dir;\n  let options;\n  if (typeof _dir === 'string') {\n    dir = _dir;\n    options = _options;\n  } else {\n    options = _dir;\n    dir = options.root;\n  }\n\n  const root = path.join(path.resolve(dir), '/');\n  const opts = optsParser(options);\n\n  opts.root = dir;\n  \n\n  // Support hashes and .types files in mimeTypes @since 0.8\n  if (opts.mimeTypes) {\n    try {\n      // You can pass a JSON blob here---useful for CLI use\n      opts.mimeTypes = JSON.parse(opts.mimeTypes);\n    } catch (e) {\n      // swallow parse errors, treat this as a string mimetype input\n    }\n    if (typeof opts.mimeTypes === 'string') {\n      mime.load(opts.mimeTypes);\n    } else if (typeof opts.mimeTypes === 'object') {\n      mime.define(opts.mimeTypes);\n    }\n  }\n\n  function shouldReturn304(req, serverLastModified, serverEtag) {\n    if (!req || !req.headers) {\n      return false;\n    }\n\n    const clientModifiedSince = req.headers['if-modified-since'];\n    const clientEtag = req.headers['if-none-match'];\n    let clientModifiedDate;\n\n    if (!clientModifiedSince && !clientEtag) {\n      // Client did not provide any conditional caching headers\n      return false;\n    }\n\n    if (clientModifiedSince) {\n      // Catch \"illegal access\" dates that will crash v8\n      try {\n        clientModifiedDate = new Date(Date.parse(clientModifiedSince));\n      } catch (err) {\n        return false;\n      }\n\n      if (clientModifiedDate.toString() === 'Invalid Date') {\n        return false;\n      }\n      // If the client's copy is older than the server's, don't return 304\n      if (clientModifiedDate < new Date(serverLastModified)) {\n        return false;\n      }\n    }\n\n    if (clientEtag) {\n      // Do a strong or weak etag comparison based on setting\n      // https://www.ietf.org/rfc/rfc2616.txt Section 13.3.3\n      if (opts.weakCompare && clientEtag !== serverEtag\n          && clientEtag !== `W/${serverEtag}` && `W/${clientEtag}` !== serverEtag) {\n        return false;\n      }\n      if (!opts.weakCompare && (clientEtag !== serverEtag || clientEtag.indexOf('W/') === 0)) {\n        return false;\n      }\n    }\n\n    return true;\n  }\n\n  return function middleware(req, res, next) {\n    // Figure out the path for the file from the given url\n    const parsed = url.parse(req.url);\n    let pathname = null;\n    let file = null;\n    let gzippedFile = null;\n    let brotliFile = null;\n\n    try {\n      decodeURIComponent(req.url); // check validity of url\n      pathname = decodePathname(parsed.pathname);\n    } catch (err) {\n      status[400](res, next, { error: err });\n      return;\n    }\n\n    file = path.normalize(\n      path.join(\n        root,\n        path.relative(path.join('/', opts.baseDir), pathname)\n      )\n    );\n    // determine compressed forms if they were to exist, make sure to handle pre-compressed files, i.e. files with .br/.gz extension. we will serve them \"as-is\"\n    gzippedFile = `${file}.gz`;\n    brotliFile = `${file}.br`;\n    \n    if ( opts.forceContentEncoding ) {\n      if ( file.endsWith('.gz') ) gzippedFile = file;\n      if ( file.endsWith('.br') ) brotliFile = file;\n    }\n\n    Object.keys(opts.headers).forEach((key) => {\n      res.setHeader(key, opts.headers[key]);\n    });\n\n    if (req.method === 'OPTIONS' && opts.handleOptionsMethod) {\n      res.end();\n      return;\n    }\n\n    // TODO: This check is broken, which causes the 403 on the\n    // expected 404.\n    if (file.slice(0, root.length) !== root) {\n      status[403](res, next);\n      return;\n    }\n\n    if (req.method && (req.method !== 'GET' && req.method !== 'HEAD')) {\n      status[405](res, next);\n      return;\n    }\n\n\n    function serve(stat) {\n      // Do a MIME lookup, fall back to octet-stream and handle gzip\n      // and brotli special case.\n      const defaultType = opts.contentType || 'application/octet-stream';\n      let contentType = mime.lookup(file, defaultType);\n      const range = (req.headers && req.headers.range);\n      const lastModified = (new Date(stat.mtime)).toUTCString();\n      const etag = generateEtag(stat, opts.weakEtags);\n      let cacheControl = opts.cache;\n      let stream = null;\n      if (contentType && isTextFile(contentType)) {\n        if (stat.size < buffer.constants.MAX_LENGTH) {\n          const bytes = fs.readFileSync(file);\n          const sniffedEncoding = htmlEncodingSniffer(bytes, {\n            defaultEncoding: 'UTF-8'\n          });\n          contentType += `; charset=${sniffedEncoding}`;\n          stream = Readable.from(bytes)\n        } else {\n          // Assume text types are utf8\n          contentType += '; charset=UTF-8';\n        }\n      }\n\n      if (file === gzippedFile) { // is .gz picked up\n        res.setHeader('Content-Encoding', 'gzip');\n        // strip gz ending and lookup mime type\n        contentType = mime.lookup(path.basename(file, '.gz'), defaultType);\n      } else if (file === brotliFile) { // is .br picked up\n        res.setHeader('Content-Encoding', 'br');\n        // strip br ending and lookup mime type\n        contentType = mime.lookup(path.basename(file, '.br'), defaultType);\n      }\n\n      if (typeof cacheControl === 'function') {\n        cacheControl = opts.cache(pathname);\n      }\n      if (typeof cacheControl === 'number') {\n        cacheControl = `max-age=${cacheControl}`;\n      }\n\n      if (range) {\n        const total = stat.size;\n        const parts = range.trim().replace(/bytes=/, '').split('-');\n        const partialstart = parts[0];\n        const partialend = parts[1];\n        const start = parseInt(partialstart, 10);\n        const end = Math.min(\n          total - 1,\n          partialend ? parseInt(partialend, 10) : total - 1\n        );\n        const chunksize = (end - start) + 1;\n        let fstream = null;\n\n        if (start > end || isNaN(start) || isNaN(end)) {\n          status['416'](res, next, { size: total });\n          return;\n        }\n\n        fstream = fs.createReadStream(file, { start, end });\n        fstream.on('error', (err) => {\n          status['500'](res, next, { error: err });\n        });\n        res.on('close', () => {\n          fstream.destroy();\n        });\n        res.writeHead(206, {\n          'Content-Range': `bytes ${start}-${end}/${total}`,\n          'Accept-Ranges': 'bytes',\n          'Content-Length': chunksize,\n          'Content-Type': contentType,\n          'cache-control': cacheControl,\n          'last-modified': lastModified,\n          etag,\n        });\n        fstream.pipe(res);\n        return;\n      }\n\n      // TODO: Helper for this, with default headers.\n      res.setHeader('cache-control', cacheControl);\n      res.setHeader('last-modified', lastModified);\n      res.setHeader('etag', etag);\n\n      // Return a 304 if necessary\n      if (shouldReturn304(req, lastModified, etag)) {\n        status[304](res, next);\n        return;\n      }\n\n      res.setHeader('content-length', stat.size);\n      res.setHeader('content-type', contentType);\n\n      // set the response statusCode if we have a request statusCode.\n      // This only can happen if we have a 404 with some kind of 404.html\n      // In all other cases where we have a file we serve the 200\n      res.statusCode = req.statusCode || 200;\n\n      if (req.method === 'HEAD') {\n        res.end();\n        return;\n      }\n\n      // stream may already have been assigned during encoding sniffing.\n      if (stream === null) {\n        stream = fs.createReadStream(file);\n      }\n\n      stream.pipe(res);\n      stream.on('error', (err) => {\n        status['500'](res, next, { error: err });\n      });\n      stream.on('close', () => {\n        stream.destroy();\n      })\n    }\n\n    function statWithAccess (file, cb) {\n      fs.stat(file, (err, stat) => {\n        if (err) {\n          cb(err);\n          return;\n        }\n        fs.access(file, fs.constants.R_OK, (err) => {\n          stat.readable = !err;\n          cb(err, stat);\n        });\n      });\n    }\n\n\n    function statFile() {\n      try {\n        statWithAccess(file, (err, stat) => {\n          const effectively404 =\n            (err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')) ||\n            (!stat || !stat.readable);\n            \n          if (effectively404) {\n            if (req.statusCode === 404) {\n              // This means we're already trying ./404.html and can not find it.\n              // So send plain text response with 404 status code\n              status[404](res, next);\n            } else if (!path.extname(parsed.pathname).length && opts.defaultExt) {\n              // If there is no file extension in the path and we have a default\n              // extension try filename and default extension combination before rendering 404.html.\n              middleware({\n                url: `${parsed.pathname}.${opts.defaultExt}${(parsed.search) ? parsed.search : ''}`,\n                headers: req.headers,\n              }, res, next);\n            } else if (opts.showDir && opts.dirOverrides404) {\n              // If showDir and dirOverrides404 are true, show the directory instead of 404.html\n              req.url = path.dirname(req.url);\n              showDir(opts, stat)(req, res);\n              return;\n            } else {\n              // Try to serve default ./404.html\n              const rawUrl = (opts.handleError ? `/${path.join(opts.baseDir, `404.${opts.defaultExt}`)}` : req.url);\n              const encodedUrl = ensureUriEncoded(rawUrl);\n              middleware({\n                url: encodedUrl,\n                headers: req.headers,\n                statusCode: 404,\n              }, res, next);\n            }\n          } else if (err) {\n            status[500](res, next, { error: err });\n          } else if (stat.isDirectory()) {\n            if (!opts.autoIndex && !opts.showDir) {\n              status[404](res, next);\n              return;\n            }\n\n\n            // 302 to / if necessary\n            if (!pathname.match(/\\/$/)) {\n              res.statusCode = 302;\n              const q = parsed.query ? `?${parsed.query}` : '';\n              res.setHeader(\n                'location',\n                ensureUriEncoded(`${parsed.pathname}/${q}`)\n              );\n              res.end();\n              return;\n            }\n\n            if (opts.autoIndex) {\n              middleware({\n                url: urlJoin(\n                  encodeURIComponent(pathname),\n                  `/index.${opts.defaultExt}`\n                ),\n                headers: req.headers,\n              }, res, (autoIndexError) => {\n                if (autoIndexError) {\n                  status[500](res, next, { error: autoIndexError });\n                  return;\n                }\n                if (opts.showDir) {\n                  showDir(opts, stat)(req, res);\n                  return;\n                }\n                status[403](res, next);\n              });\n              return;\n            }\n\n            if (opts.showDir) {\n              showDir(opts, stat)(req, res);\n            }\n          } else {\n            serve(stat);\n          }\n        });\n      } catch (err) {\n        status[500](res, next, { error: err.message });\n      }\n    }\n\n    function isTextFile(mimeType) {\n      return (/^text\\/|^application\\/(javascript|json)/).test(mimeType);\n    }\n\n    // serve gzip file if exists and is valid\n    function tryServeWithGzip() {\n      try {\n        fs.stat(gzippedFile, (err, stat) => {\n          if (!err && stat.isFile()) {\n            hasGzipId12(gzippedFile, (gzipErr, isGzip) => {\n              if (!gzipErr && isGzip) {\n                file = gzippedFile;\n                serve(stat);\n              } else {\n                statFile();\n              }\n            });\n          } else {\n            statFile();\n          }\n        });\n      } catch (err) {\n        status[500](res, next, { error: err.message });\n      }\n    }\n\n    // serve brotli file if exists, otherwise try gzip\n    function tryServeWithBrotli(shouldTryGzip) {\n      try {\n        fs.stat(brotliFile, (err, stat) => {\n          if (!err && stat.isFile()) {\n            file = brotliFile;\n            serve(stat);\n          } else if (shouldTryGzip) {\n            tryServeWithGzip();\n          } else {\n            statFile();\n          }\n        });\n      } catch (err) {\n        status[500](res, next, { error: err.message });\n      }\n    }\n\n    const shouldTryBrotli = opts.brotli && shouldCompressBrotli(req);\n    const shouldTryGzip = opts.gzip && shouldCompressGzip(req);\n    // always try brotli first, next try gzip, finally serve without compression\n    if (shouldTryBrotli) {\n      tryServeWithBrotli(shouldTryGzip);\n    } else if (shouldTryGzip) {\n      tryServeWithGzip();\n    } else {\n      statFile();\n    }\n  };\n};\n\n\nhttpServerCore = module.exports;\nhttpServerCore.version = version;\nhttpServerCore.showDir = showDir;\n"
  },
  {
    "path": "lib/core/opts.js",
    "content": "'use strict';\n\nconst aliases = require('./aliases.json');\n\n\n/**\n * @typedef {Object} ParsedOptions\n * @property {boolean} autoIndex\n * @property {boolean} showDir\n * @property {boolean} dirOverrides404\n * @property {boolean} showDotfiles\n * @property {boolean} humanReadable\n * @property {boolean} hidePermissions\n * @property {boolean} si\n * @property {string|function} cache\n * @property {string} defaultExt\n * @property {string} baseDir\n * @property {boolean} gzip\n * @property {boolean} brotli\n * @property {boolean} forceContentEncoding\n * @property {function} handleError\n * @property {Object.<string, string|boolean>} headers\n * @property {string} contentType\n * @property {Object|undefined} mimeTypes\n * @property {boolean} weakEtags\n * @property {boolean} weakCompare\n * @property {boolean} handleOptionsMethod\n */\n\n\n/**\n* Converts a user-provided options object into a ParsedOptions object\n* @param {object} opts - User provided options\n* @returns {ParsedOptions}\n*/\nmodule.exports = (opts) => {\n  /** @type {ParsedOptions} */\n  const options = {\n    autoIndex: true,\n    showDir: true,\n    dirOverrides404: false,\n    showDotfiles: true,\n    humanReadable: true,\n    hidePermissions: false,\n    si: false,\n    cache: \"max-age=3600\",\n    coop: false,\n    cors: false,\n    privateNetworkAccess: false,\n    gzip: true,\n    brotli: false,\n    forceContentEncoding: false,\n    defaultExt: \"html\",\n    baseDir: \"/\",\n    handleError: true,\n    contentType: \"application/octet-stream\",\n    weakEtags: true,\n    weakCompare: true,\n    handleOptionsMethod: false,\n    headers: {},\n    mimeTypes: undefined,\n  };\n\n  function isDeclared(k) {\n    return typeof opts[k] !== 'undefined' && opts[k] !== null;\n  }\n\n  function validateNoCRLF(str) {\n    if (typeof str === 'string' && (str.includes('\\r') || str.includes('\\n'))) {\n      throw new Error('Header is not a string or contains CRLF');\n    }\n  }\n\n  function addHeader(key, value) {\n    validateNoCRLF(key);\n    validateNoCRLF(value);\n    options.headers[key] = value;\n  }\n\n  function setHeader(str) {\n    validateNoCRLF(str);\n\n    const m = /^(.+?)\\s*:\\s*(.*)$/.exec(str);\n    if (!m) {\n      addHeader(str, true);  // Use addHeader instead of direct assignment\n    } else {\n      addHeader(m[1], m[2]); // Use addHeader instead of direct assignment\n    }\n  }\n\n\n  if (opts) {\n    aliases.autoIndex.some((k) => {\n      if (isDeclared(k)) {\n        options.autoIndex = opts[k];\n        return true;\n      }\n      return false;\n    });\n\n    aliases.showDir.some((k) => {\n      if (isDeclared(k)) {\n        options.showDir = opts[k];\n        return true;\n      }\n      return false;\n    });\n\n    aliases.dirOverrides404.some((k) => {\n      if (isDeclared(k)) {\n        options.dirOverrides404 = opts[k];\n        return true;\n      }\n      return false;\n    });\n\n    aliases.showDotfiles.some((k) => {\n      if (isDeclared(k)) {\n        options.showDotfiles = opts[k];\n        return true;\n      }\n      return false;\n    });\n\n    aliases.humanReadable.some((k) => {\n      if (isDeclared(k)) {\n        options.humanReadable = opts[k];\n        return true;\n      }\n      return false;\n    });\n\n    aliases.hidePermissions.some((k) => {\n      if (isDeclared(k)) {\n        options.hidePermissions = opts[k];\n        return true;\n      }\n      return false;\n    });\n\n    aliases.si.some((k) => {\n      if (isDeclared(k)) {\n        options.si = opts[k];\n        return true;\n      }\n      return false;\n    });\n\n    if (opts.defaultExt && typeof opts.defaultExt === 'string') {\n      let ext = opts.defaultExt;\n      // Remove the leading dot if it exists\n      if (/^\\./.test(ext)) {\n        ext = ext.replace(/^\\./, '');\n      }\n      options.defaultExt = ext;\n    }\n\n    if (typeof opts.cache !== 'undefined' && opts.cache !== null) {\n      if (typeof opts.cache === 'string') {\n        options.cache = opts.cache;\n      } else if (typeof opts.cache === 'number') {\n        options.cache = `max-age=${opts.cache}`;\n      } else if (typeof opts.cache === 'function') {\n        options.cache = opts.cache;\n      }\n    }\n\n    if (typeof opts.gzip !== 'undefined' && opts.gzip !== null) {\n      options.gzip = opts.gzip;\n    }\n\n    if (typeof opts.brotli !== 'undefined' && opts.brotli !== null) {\n      options.brotli = opts.brotli;\n    }\n    if (typeof opts.forceContentEncoding !== 'undefined' && opts.forceContentEncoding !== null) {\n      options.forceContentEncoding = opts.forceContentEncoding;\n    }\n\n    if (typeof opts.baseDir !== 'undefined' && opts.baseDir !== null) {\n      options.baseDir = opts.baseDir;\n    }\n\n    aliases.handleError.some((k) => {\n      if (isDeclared(k)) {\n        options.handleError = opts[k];\n        return true;\n      }\n      return false;\n    });\n\n    aliases.coop.forEach((k) => {\n      if (isDeclared(k) && opts[k]) {\n        options.handleOptionsMethod = true;\n        options.headers['Cross-Origin-Opener-Policy'] = 'same-origin';\n        options.headers['Cross-Origin-Embedder-Policy'] = 'require-corp';\n      }\n    });\n\n    aliases.cors.forEach((k) => {\n      if (isDeclared(k) && opts[k]) {\n        options.handleOptionsMethod = true;\n        options.headers['Access-Control-Allow-Origin'] = '*';\n        options.headers['Access-Control-Allow-Headers'] = 'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since';\n      }\n    });\n\n    aliases.privateNetworkAccess.forEach((k) => {\n      if (isDeclared(k) && opts[k]) {\n        options.headers['Access-Control-Allow-Private-Network'] = 'true';\n      }\n    });\n\n    aliases.headers.forEach((k) => {\n      if (isDeclared(k)) {\n        if (Array.isArray(opts[k])) {\n          opts[k].forEach(setHeader);\n        } else if (opts[k] && typeof opts[k] === 'object') {\n          Object.keys(opts[k]).forEach((key) => {\n            addHeader(key, opts[k][key]);  // Uses same validation path\n          });\n        } else {\n          setHeader(opts[k]);\n        }\n      }\n    });\n\n    aliases.contentType.some((k) => {\n      if (isDeclared(k)) {\n        options.contentType = opts[k];\n        return true;\n      }\n      return false;\n    });\n\n    aliases.mimeType.some((k) => {\n      if (isDeclared(k)) {\n        options.mimeTypes = opts[k];\n        return true;\n      }\n      return false;\n    });\n\n    aliases.weakEtags.some((k) => {\n      if (isDeclared(k)) {\n        options.weakEtags = opts[k];\n        return true;\n      }\n      return false;\n    });\n\n    aliases.weakCompare.some((k) => {\n      if (isDeclared(k)) {\n        options.weakCompare = opts[k];\n        return true;\n      }\n      return false;\n    });\n\n    aliases.handleOptionsMethod.some((k) => {\n      if (isDeclared(k)) {\n        options.handleOptionsMethod = options.handleOptionsMethod || opts[k];\n        return true;\n      }\n      return false;\n    });\n  }\n\n  return options;\n};\n"
  },
  {
    "path": "lib/core/show-dir/icons.json",
    "content": "{\n  \"_blank\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAWBJREFUeNqEUj1LxEAQnd1MVA4lyIEWx6UIKEGUExGsbC3tLfwJ/hT/g7VlCnubqxXBwg/Q4hQP/LhKL5nZuBsvuGfW5MGyuzM7jzdvVuR5DgYnZ+f99ai7Vt5t9K9unu4HLweI3qWYxI6PDosdy0fhcntxO44CcOBzPA7mfEyuHwf7ntQk4jcnywOxIlfxOCNYaLVgb6cXbkTdhJXq2SIlNMC0xIqhHczDbi8OVzpLSUa0WebRfmigLHqj1EcPZnwf7gbDIrYVRyEinurj6jTBHyI7pqVrFQqEbt6TEmZ9v1NRAJNC1xTYxIQh/MmRUlmFQE3qWOW1nqB2TWk1/3tgJV0waVvkFIEeZbHq4ElyKzAmEXOx6gnEVJuWBzmkRJBRPYGZBDsVaOlpSgVJE2yVaAe/0kx/3azBRO0VsbMFZE3CDSZKweZfYIVg+DZ6v7h9GDVOwZPw/PoxKu/fAgwALbDAXf7DdQkAAAAASUVORK5CYII=\",\n  \"_page\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmhJREFUeNpsUztv01AYPfdhOy/XTZ80VV1VoCqlA2zQqUgwMEErWBALv4GJDfEDmOEHsFTqVCTExAiiSI2QEKJKESVFFBWo04TESRzfy2c7LY/kLtf2d8+555zvM9NaI1ora5svby9OnbUEBxgDlIKiWjXQeLy19/X17sEtcPY2rtHS96/Hu0RvXXLz+cUzM87zShsI29DpHCYt4E6Box4IZzTnbDx7V74GjhOSfwgE0H2638K9h08A3iHGVbjTw7g6YmAyw/BgecHNGGJjvfQhIfmfIFDAXJpjuugi7djIFVI4P0plctgJQ0xnFe5eOO02OwEp2VkhSCnC8WOCdqgwnzFx4/IyppwRVN+XYXsecqZA1pB48ekAnw9/4GZx3L04N/GoTwEjX4cNH5vlPfjtAIYp8cWrQutxrC5Mod3VsXVTMFSqtaE+gl9dhaUxE2tXZiF7nYiiatJ3v5s8R/1yOCNLOuwjkELiTbmC9dJHpIaGASsDkoFQGJQwHWMcHWJYOmUj1OjvQotuytt5nHMLEGkCyx6QU384jwkUAd2sxJbS/QShZtg/8rHzzQOzSaFhxQrA6YgQMQHojCUlgnCAAvKFBoXXaHfArSCZDE0gyWJgFIKmvUFKO4MUNIk2a4+hODtDUVuJ/J732AKS6ZtImdTyAQQB3bZN8l9t75IFh0JMUdVKsohsUPqRgnka0tYgggYpCHkKGTsHI5NOMojB4iTICCepvX53AIEfQta1iUCmoTiBmdEri2RgddKFhuJoqb/af/yw/d3zTNM6UkaOfis62aUgddAbnz+rXuPY+Vnzjt9/CzAAbmLjCrfBiRgAAAAASUVORK5CYII=\",\n  \"aac\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnhJREFUeNp0Uk1PE0EYftruVlvAUkhVEPoBcsEoLRJBY01MPHjCs3cvogcT/4qJJN5NvHhoohcOnPw4YEGIkCh+oLGBKm3Z7nZ3dme2vjOhTcjiJJvZzPvOM8/HG2q325Dr3kLp7Y1ibpIxjs4KhQBZfvV6s7K5Vb0bjeof5ZlcGysP1a51mifODybvzE8mzCbrAoTDIThMoGXZiZ4YSiurf+Z1XeuCqJ7Oj+sK3jQcNAmg8xkGQ71mYejcAB49vpmeuzJccl0+dUj6KIAvfHCPg3N+uAv4vg9BOxcCmfEzuP/genpmeqhEMgude10Jwm+DuUIyUdTlqu2byoMfX/dRermBeExHsTiWNi3+lMpzRwDki8zxCIATmzbevfmClukiP5NFhJgwkjeRTeLShdOoVJqnAgwkgCAZ6+UdLC9twjQZ8pdzioFkZBHY3q6B3l4dJEEEPOCeD4cYVH7Xsf15F+FImC775INAJBJSkVoWo0QY9YqgiR4ZZzRaGBkdwK3bFxGLRZUfB3Rm2x4x9CGtsUxH9QYkKICDFuLxKAozGZwdTqBRs2FbLlXbiPdECMCHadj/AaDXZNFqedCIvnRcS4UpRo7+hC5zUmw8Ope9wUFinvpmZ7NKt2RTmB4hKZo6n8qP4Oq1HBkKlVYAQBrUlziB0XQSif4YmQhksgNIJk9iaLhPaV9b/Um+uJSCdzyDbGZQRSkvjo+n4JNxubGUSsCj+ZCpODYjkGMAND2k7exUsfhkCd+29yguB88Wl7FW/o6tT7/gcXqAgGv7hhx1LWBireHVn79YP6ChQ3njb/eFlfWqGqT3H3ZlGIhGI2i2UO/U/wkwAAmoalcxlNA1AAAAAElFTkSuQmCC\",\n  \"ai\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAk5JREFUeNpsU01vElEUPTPzZqBAQaSFQiJYUmlKYhoTF41L3Tbu/Q/+AvsX3Bp/gPsuWLrqyqQ7TUxMtAvF1tYGoXwNw7wv7zwYgtKX3Lw379575p5z77O01ohW+/DVh8zj7aYKhflGdG9ZsGwLNydffgVfr19YHvsEa+Zu/nxndob5StQK+dyzvZzyw/gKlmMj7IygFM+xvNcanp4/t5dAomXHBy2UUBOO2MAl/B9/cPb6PULuoHx0WM0e3GvpUOxD3wZAJWutZqYUYmqpSg5OMgH3YQObL59W0/ullpryR3HegkKEqiWBSGV4R3vQ7sIhScTZFTpHx3A215B5sluVY/WWMg7+ATB/lcLsKpTonHzD+OMFEuTz8ikkt9Kwt9YJZB38cpBdoQAZJdLvCGByfoPB6Xdk90pYy6Xg3c/DaWwArg09DaG5lCsUFN0pckZAojdC8m4auBqaALuSgez7VB1RtDSUWOQvUaBLFUzJBMJ2DwmPgd1Jwm0WoSgJfjDvrTKxtwAIyEkAOQ5hU//Zdg5uowDlUNMnwZLW0sSuUuACYhwQRwFvJxupCjEYUUccOkoaKmdOlZnY1TkgAcXAhxhOwLsDsHoN3u4O5JTDfVCH6I9nfjId3gIgSUATFJk/hVevGtOMwS0XwQ3AzB/FrlKg8Q27I2javVoZrFgwD4qVipAEyMlnaFArzaj/D0DiMXlJAFQyK2r8fnMMRZp4lQ1MaSL5tU/1kqAkMCh2tYI+7+kh70cjPbr4bEZ51jZr8TJnB9PJXpz3V4ABAPOQVJn2Q60GAAAAAElFTkSuQmCC\",\n  \"aiff\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAohJREFUeNpkU9tqE1EUXZmZpE3aTBLbJFPTtFURtSCthr7UCyKKFJ/9An3og6Ag/oXfoUj7og9asCBYKT6UIPHaWtpq7NU2aZK5z5wZ9xxMpMwZDuewz9prr32ZiO/7CNaDx3OLt6fOjBqGg/aKRCIInp8+KzfKH7fudnVF58nE16el+/yU2mBFSWZKpWJKVc0OgUBo02K4NDmU6o75Mx+Wdu9IUXFeiOA/pn1xHeYaugVDdzpbp91qGlAKGTx8dC19/Wpxhjnsxj/RRwk85hGJC9d1O6fneWAuoztDYSSLe9OT6SuXB2ccx73Z9uukwDwfls1g0xZIY/Ad/Gnyt/XVfbyYrSDRE8PExHB6/8B6QuaxIwRBFMt0iIAiMx+LCys8jfGJEUik2WpZOD2SQf9oDtVqQwopCAiY66FS/om3b75CVS2MlU7AJ2WiJBCZjZ2dJuRkDJZFwFAR7UCBja3fNfxY2YEoCtRCj9em3Tpds6FpJseGCBxS0GgYGBzqw62p84gnYnAI2CSbSbPhEpFAaE2zODaUAlWWwDoS5DheGqbWpVE/0CmqCY9qkEyINBceb2uADRNQ8bSWAVVzIFKomCQim+0luS4yKYlsHlRyZo7EsSEC23K5vAsXh/H92zZkuRvxeBS5nEx2yp2KqhxPoV5TYS/8CtdApylM9sZQKKSQzyeRTseRV2QoAzIYY8jme5DN9fI0dQoUIjANGydP9VM7PZw9p/AiBpNYrdbw/t0yTJqRtdU9UrfJCUMpSJIgbWzsYe51BcViHzLHeqCRqhZ1YX1tFwNfZBxS9O3NWkAcHqR606k/n/3coKAoV/Y7vQ/OYCZevlrmv3c0GsFh06u3/f4KMABvSWfDHmbK2gAAAABJRU5ErkJggg==\",\n  \"avi\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAm1JREFUeNpsU8tu00AUPXZcN0nzTpq2KQ3pAwkIAnWHqCoeexBb+AQ+ABZ8A2s+AIkdm266QUJIFWKBkHg1KpRHi5omJGkbJ3bGHj+4M1EQrTvSyGPPueeec++1EgQBxHp+/9mbyuriRZdxjJaiKBD3W+u1+p9a856max+gDO8ebT+WT20Ezi9NZi/crqadvn2MQBAGfpCOpqNru2937vxPIpY6Onjccx3Twck9MBiSU0ncfHirXFmZX3Md9wqCUwiEVN/zaQfHt0vfbBe5uQyuPVgpl5Zn11ybL4/i/lkICOw5niQRGQShoiqI6Bo43W2ub8n3hRtLZT7gTynk6gkCX9gAOxpAnxhHZDwC1/aI1EViJolu/QhKRMHZ1UX0Gr1USIEn5FPWHy+/wTokkrQOq2vBaHZBN4hmY9Jwfr4An/teiEB45ZZDwDiMhoExT0N+sYDCuUkkplLIlXP4/XEXdo+RUhdhBSSfUwtVTUG8MIHK9QVqI7D/uY6vr2pwmCPrkz+Tk9gwARWQ9WxppbXZhNnpw+ya4A5HZi6L4lIR8WyCcL6sTZiAWjWgAmpxkn5+kqTamK6WkCwmERmLDLvjB0ML9ikWXPLFuozYOap3L8HYN6DHdbS/d5CeTVBndBz87FCBLYkNTyIjBQemnIEsSY5lYrK1+UoWcToLMjEHAyIQ2BCBSx/NVh+ZUhrqmEqBebS3WyhdLg0zt/ugAaIklsSGLHCLa6zDMGhZ2HjyGsnpFPqNHnY2fmHv3R5SMymYbROszSQ2ROAY9qHiofvlxSc5xsKKqqnY3diRE9h4X5d/pzg7lnM4ivsrwADe9Wg/CQJgFAAAAABJRU5ErkJggg==\",\n  \"bmp\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmZJREFUeNp0U+1rUlEY/13v9YV0vq2wttI5CdpL9aEGBZUDv0df668I6n+or0UQ/RuuD0EgVDAZrsKF4AR1a6COKW5qXvXec27PuVeda3bgcF6e8/ye5/d7niMZhgExnK9fbTrm5pbBGMZDkgCyq+VyhTUaT6Eo2ZHJePPWXJXRhez3B1yxmM/QdctXUSCgtV4Py4CvY3cky4e1x5DlLCaGbbzjXDcousG5OQe5HPRSCQPK4PpsEM/XH4WvhS4noeu3JwHGGRiULhsMoKZS4I0GtEIB9mgULJGA0+9DPBpBT7sffvf1W/Lg6OgJufw8C0CRGEXWazUwiiyFQjA8bsjVKjaJzovMD/Q5gxyJhG2cvyeXe2cAuADQNGBmBvLaGuTFRaDfh31lBTWi9pumjbK0B4JQul3vOQpM8JdskOLrdCvDcDjAsjtg5TIkoiKLaokMNR2cnZbqNAMycqG7XbHKR2fMzwO/dsxSwu0BiBJsNsv2LwAJAJCI5ux2gXYbqNetcz5PoORI1cDS0n8AxGW7A+zvEYBKZ2ZlcsEtJLbedMjePBaCTQMghx45ulyWkzxMVUQ2RMQhLfFO16YAqCrixPnm6iqKrRb2W23EfF4cUNSrHg90cr7hDyB33MTnSmUKALVs4uIlROjxg+AsPhGVl3fuIl2tIOB0Ya91gkOi9mxhAal0ekork1ic/kGLBORMxy2K1qS9V1ZQbNThIj2EGh+2tsyOnSai8r1UxMNIBB+LRTTULr4Uds0K1tU/uOLxIrmbNz8XXSrnASSpubG9fbKRyVh1n/zSw29t9oC1b47MfwUYAAUsLiWr4QUJAAAAAElFTkSuQmCC\",\n  \"c\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAcxJREFUeNqEUk1rE0EYfmZnkgoJCaGNCehuJTalhJZSUZB66a0HwXsP/Qn+FM+9+hty0LNYCr2I7UVLIW0Fc0hpQpSS7O7MrO9MspuvVV8YMnk/nn2e5x0WRRFMvP/w6WSz5jbi/9NxfP693Wp3DrJCnMW5d28P7a+IE15lufR8o1ZEStwPhkWHsWbrZ+eNEPxsuubEF6m0TBv2Q4liPofXuzveulttSqW2UwH+GjqC0horpSL2njU89+FyMwjlTlxOJMTa9ZQHzDQIjgwdom9zLzfXPc75kbnOAswBJTlC2XrqQRMLxhi442DgB4UFBhgPpm3B5pgBHNUUxQKAHs8pHf3TEuFMetM9IKr/i2mWMwC0SnuSFTG2YKyppwKYVdGO7TFhzBqGIenVeLCUtfURgErucx5ECKREKBU4d3B718PHz6cICGT/1Qs8qpQtGOdyhtGEARWDQFqQJSeDL98u4VbLaKw9IRAJPwjtoJGlVAoDQ800+fRFTTYXcjlcXN2g++s36p5Lzzlve1iEROa8BGH1EbrSAeqrjxEqicHQt8/YSDHMpaNs7wJAp9vvfb287idboAVkRAa5fBYXP9rxO4Mgf0xvPPdHgAEA8OoGd40i1j0AAAAASUVORK5CYII=\",\n  \"cpp\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAfJJREFUeNqEUs9PE0EU/mZ2WgqpXX+QIDFdalVslh8NlAOQaOKFAwfvHvwT/FM8e/U/MOnBmwcj8WD0ACEGghIkbU0baaEthe3OTJ0ZWV26q37JZt68ee/b9733yGAwgMbL12/fz+azbnAPY2Nrt7Zfqz9JMrYZ+J4/e2pOFjiciRvXlgp5GzHonXk2o6S8V6k/TjBrM/xGA4MLyeOSPZ8jkx7D+uqCU3Amy1yIYizB36AlCSkwfjWDR4uu40yMl/s+XwjeWThQQ4Z6QNSnSkYykcDXasP4lmfvOZTSF9q8TDBEFPbN5bOqCglCCCxK0TvvZyIV4CIxbgpC+4gm/PUmFCIE8iJPyME/e8Lon9j4HvyHYLjKSwRCSEUgf9+15mFbx8QS6CZJMzJ9SlBCwX3fJDLG4PX7ykcwkmQmJtpEhWa7g1dvNlSwjwelebz7tAXLolh0p/Fxe9fErK2WDFGEgKjxfNjegX0lDTc/heNuF99/HGEslcKXwyoazWNDdlCr6+DoJgrBzdI0T9rYO6yg2zszMlaKM3Dv5OBzbuyZuzm1B16U4Nzz2f3cFOx0Gq12F9cztpExncsqYoaHpSIKtx0zJdVIFpHQ6py29muNk1uTN829o/6SHEnh80HFaE6NjmLnWxUJy1LyTltB3k8BBgBeEeQTiWRskAAAAABJRU5ErkJggg==\",\n  \"css\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAk1JREFUeNpsUktvUlEQ/u5DoCLl/RAKKKUvWmIxjYntQtcu3LvwJ/hTXLt16coFC2PsojEaMKZtCqFaTdGmjbS0CG3By+vei3OOBSGXSU7uzNyZ78z3zRF6vR6YvXzzPrMUCyf68bB9zO+VfpROn5hkOdfPPX/2lH/lfiLidztX5mN2jLGG0rKLENIE8liWpdzwP7HvqJqujmvudFU4bFY8Wk1FZsOBtKppd8YCDNu77CZevd3gflfTUFcUhP0ePLibiIR9rjSBpgwAfe4dVcV6dhtep4PH5msylGYLrzeybErcT85FYiH/CyPAf74gObC2vMhzsiRhPhpC6eQUM+EA1pJzILEnjRSuJsju7MJqsUCSRei6Dp3yXqcdGlHZ/rLPazQWGCn8+6YW4pAkEW0SjzUzanWlCa/LgcR0lNfovTEi6lcIkzesnM/R8RlN0INGp3h4DHoDsE5YRvQyiKiRSMzikRAOS2WoqoZWu41K7RwzlOOAVDMMMHhIGvFlRxJFrKYW0ep0IYgC3SDh4b1lTJjNfENsrazOAMAw680mPuW+8lFno1P4XDigRhOiwQAyJK7TbsNS/PaA7giAIAhYz2yRgBIfsVA8wIetPG6FAqhdNrC5u0f+TUyHgyMTDDToEt/ftQsEvW4EPG5OZcrvw0mlimarTXkPfpXPcNlQoGtjACgpryQXsPNtH/nvRXqBJpoKHMzGNkNB0Odls7LNyAYKpUq1dt1iuvB7fRDp9kr9D1xOFwkpoksXusmXaZWFn0coV89r/b6/AgwAkUENaQaRxswAAAAASUVORK5CYII=\",\n  \"dat\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAfVJREFUeNqMU01PE1EUPe/Na0uptmlASg3MoiZgCA3hQ8PHAjbqwsS9C3+CP8W1W/+BSReyYUPwI4QAVkAgUEgIbVIg1FZb2pl5b3zv2cHBjsaTTOa+e989OffcGeK6LhTevFv+OJoZHPHOfrz/sl86KpWfhxnLe7lXL1/oN/MSZqonOXU/k0AA6lfNhEFIrlAsP2PMyPtr1AscLpyg5pbtIHErhqez4+awmc45nI8FEvwNaiQuBHqTcSxMjJhmX0/Osp1xr878FxWEzwMinxAzEA4xFIpnOjedHTKpYbxW4U2CP4j8uWxmUKsghMCgFI2mFe9QgHZj0Ba4yhFF+KvGJToIRLuPC/efnjD6+26wB1Lq/xgbSCBXKeWJG/OTdky8cWTdT3C9RmWSGk2XCLlWo4xTNbfN5qh7PpXM72GjZeHt0gpq9QbmH4whGb+NpU/reDQ7hcWVVXxvXOHxzCQopQEKXKEbL6o1ZIcy+LC5g62DY2zsHeC0fA4zndIrHOjvg2XbAQRSfsuy9XxC2qzi/H5B6/68W0AsGkW0KyJPBLbDO0fg3JX/CUM81i0bD6WKe6j9qOPJ3EMcF0tSNsFA6g6alqW+VtZBUL78Vtk+Oqne7U9rs5qOQCjSheJFBeFIFOfVujSUYu3rIc4uqxWv76cAAwCwbvRb3SgYxQAAAABJRU5ErkJggg==\",\n  \"dmg\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAn9JREFUeNpsU01rE1EUPe9lkk47yWTStCmtNhFSWxos2EXVhSsRcasuxYV05V8Qf4DgD/AvCK5EV1oFI7iUBqmCNdDvppq2mWSSzEzy3vPOpFFq+uDNfR/3nnvueXeYUgrBWH1/9/NE7k5BKRnuRcfF2qdnmJq9DeF9tQ+2isuMsxXGWHh/a1mEVsPJSI5fSU3OPEj291IIlN49RXz0KqzEQjIeZS/L5Y/3wPGhDxIM/i/A7fZWgVG0t5EaG0ZUa0JGM8gvPrZmLt58QYwv91mfAqCIE0sAqgumBFITGQzpUYhuF0KfRa7waDyXXXolpVrsh/0tgSLDr5I+wUZo1UHCSkAficPzY6juFSmbRPrC/azjq+fkcO00gAqoU7B0ETKkfWbuCTjTYeq5oESAauexcTScX+ZACWFm0YQSLZKhHdr67+/wW0e0dgjYo3sCEXXybYtBDVSHLp2es3IpsILS24c42lkBg6DzRjgRzCDZ/xr0GNRJwwYiWgzt+hYMawleu0V3wbkT+kUirOc7IGJAz68R/Qak1BAlx3hqASPGBJRXpXOv58dkz3eAgQoOm4hyj57NgZm0MHvpBmK6QdUdg/DAg9cRkhicBSDaKJdeo1bdxmR2DtWDDUxl51HZ+QHTysD3XdQO95Gfv06aeGcAdBrY3Chi8lwO3768QWX7J5q1XWyVSxgajiOXLyBG2hzurRKV9lmt7ISNkkjo6HhNyjoK+2gXRsKE57ZIE2ot10Z1fz0Ue4ABVw3NMjnW14rInh8jTYywoTg3EOFpOM4mXNfH9PQUfGlrAwBOs3I8ljbtuMWhRWzIIPrkn+GcYcgIWEowbZ+0qB334/4IMADESjqbnHbH0gAAAABJRU5ErkJggg==\",\n  \"doc\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAppJREFUeNpsU79PFEEU/mZ39vZu77g7DokcP04BBSUmiEKCSCxs7Ei00JAYO2NlTKyMrX+CJhaGwopSQ0dMtFEsbDRBgiZEQIF4IHcg+2t2Z8eZ5QDlnM1mZ9+8973vfe8NEUJArfSNhzPG0VIfeIiDRSDkw1cWVt3N8rhG6SdSO2Gvn8dfuueqZwuNZqk3Jxg7iNcIfBbgXD6ZC8u5qffzX8eoYeyDxC77uygKhcouovgVUQj1H4YB2ovNuD9+tTTU0zMVBmG/+C8AIYh8F361DL/yE5HnADKYlVdg6MDAmW7cuz5WGuw+PsWDYGAvbL8ECFUt4K7/AHd/I9c7BLaxinD2Ld5Zo7g78RLuRhlBS2cpWbGfStfhfwCEpK0nUjCbWuGsLciSOELPhkq/YgdY3l6HsLfRcLYf+pHNbH0JigEPkLAyMsiEJ7NrqQzM1i7wyhoMZqOhvQs6Z0ovXgdAJACRoulEg5HOwrOroKk0zOY2BDtVpTF0CU6kLkQJXa+BNEoG0lMSsBBKQXWNQktmoGcaYeSaQCIVWOvUYQAiWZFQtk5mSMoSzEILtBrTfEcviC5bwVwQmoh96wA0ic5dB57ngeoaTIPCdb34zDITYNLOOIeVSsW+dQC+7+NSWx6jJ4tY/rWNV7PfcGv0tBoPTM7M4eKJVgx2FTE9u4QPS6x+kHzfw/mOAjarW2hJG3hy8zIceweuY+PRtREMdzbjzcd5WBqPB6xeRGUMGRzHjWvMmxQ7tiOF1JBN6FiTd6Sy9RuFbHpX7MMMqOD088Ii+op5OUAO7jyeRGfBwrF8Cg8mXuDL4neMXzgFwhwZz+hf7a9d5yu3Z6DTPjVQIY9k7erO7Y63Lvc8ErEeyq6JaM6efjai4v4IMABI0DEPqPKkigAAAABJRU5ErkJggg==\",\n  \"dotx\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAndJREFUeNpsU01rE1EUPTPzJk0y+WhMStW2qdVWxUVEQUF0I+4ELQiC7lz4N9z0T+hG9wrdZKUgLqulhrbSag1CKpT0g7RpYjqZmffle5NEKdMHlzfvvXvPPffcO4aUEno9f3Vt4dTp+BXOe+fB0u/NbVpv7h89NU1j1TCM8H7+xY9wJwPHZMbOjRadLAvE/2gToJTiTPx89k+OlVd/LT+0TPIPpO/SzyQk40xCMxBSZ9Z3CoAx5DOjeHT7SbE0XSpzwa8OWB9jINELolQg8AR0EgUKn1PIlIWpkUt4cPNxkTOU12trs8p95RiAXpqaztqou8q6SKQJJmZSqGwsodFsIJk1kcyLYv7IeafcLx4HUNkFF4jFTExMZ0B9DrfD4HUEusYhWs4GPEJg5wly/tBYRIOeDhpEwlS34xcyajdQr3UwOT2MlJOEBRuGNHWp9AQRVXDfQiFV/U5GBSiQ5p6ngBEa5z3fiIhC6g6IMDBwOdoHPkYnHPVyhN0tF7E4QSpr94CEOKELffq+y9Bq+DCJ7rWBoQQBVbPR2O6G4OlsLASJMtCZfQqm0NP5IVWnamdAkUxbyuIYtD7wWegb0YAzAVMkkI6NwPM9xEwHloyDGAmk7AKS9rAS0FKOdugbYeAHPu7OPEM+MY7q3hIKqTFQHmC3XcONc/fxdfMDrk/ew/edzyhvvTmBAddocVRqH3Frahau56qpZDho7+PnTgXffi/gbHYmLEvPSIQBp5JU62sYz13G609zKBXvoOMdYn2zgm7Xg2MVML/4Eu3uPgxhk2gXmNl8v/i2pcXTP8tKdTEcbWLZqDQXwu/l6pfwbEnSGsT9FWAA4mdHv2/9YJ4AAAAASUVORK5CYII=\",\n  \"dwg\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAoFJREFUeNpsU0tPE2EUPfOg006hD4rQh8WgbCSwkKgbF2owujaCiQsXxpX+D6MmbtXEsHCLmIAbE6NLo8YlGIxREIshIqVl+mQ6j8/zFVCb4UtuZua795577rl3FCEE5Bl79vPd5LHYiOP7cH1AUWi85ytmvlas1bJ9E5ryBntH3BpuP/X9i7ovkluuiE8N9SDepaLpCcRCCqa/VDCaMuIjSWP25Upl6n+QDoCz6Yh7KKzh3sI2LuUimPtRRyaqodj0MDloYiITSTi+mH29Wu0AUf9CsZPJoW5czJl48LmCc5kIKo5Al67B9gUGYxrun+5NnMlFZ+GKiQADj2a7AquseLIvjMv5KMaSBu4sWVir+3i8VIVKYSby0UTdFU8Znu8AYBHQgVOJEN5uOXi4UsdawwU0FSf6TaSoyw6DRvukPkgGWpDKy4F8a3jImCrqFDFn6rhKPR4VGnhvOTAY3WLcjifcQAsqRfhUc/Gq1MKNbBh9nIAMDjEppocxs9HCMktfGTCwP/oOBkUKNk/qF3pDYC6Ktk8RfWzyaaoKrqdDaBDwya8W1m0/CPCR3kFy7CcnmWQRUJqcRJFUKtTnPCeR71LwoeYF92CYyVnCFZpCTrRtCv5to2St8SOrKxiPqEEA4fkYT+mI0rdoeUiH1XZVuQPpsIKqw2QmfifTsnOABiWySlH9uU0Hh2MqjsZV5LtpPSoGeN9rKnhBX7ehoOSLIIPfnGONXGMMWN7xUfVldYDbjM3mrh5HCDgS17DhHgDQcIU+XbBxnDTn1x1UuQcJ9iv7l5Q5e1zLGri92EDJFnoAgHtcfr6wbbVXUqq193+0z97n3UJt1+d51n7aHwEGAAHXJoAuZNlzAAAAAElFTkSuQmCC\",\n  \"dxf\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAo5JREFUeNpsU0trE1EYPfNMmtdoH2kDNmJbaVFcaBVFpAsREQpFwY0bu3HjQnTj1mVd+ANcuC3qQixmry6E0kWFVIQ+bKy2tbFJm3emyXTujGca+4DkwsedfLnn3POd77uS67rw1vC79ek7fZEzpu3AYUqS9tKQGZPLpa3VXP0uFCmJ/8t9OLC3q/uJbcs5bkIybvdHoMsSbLKENRmvU2WcNnTjRFD7ML1WGSPJHI6sA4KRWMAWVDPxLYex3iCmfpuIh1QsFSyMxQO4GvXHHwOJ6XWSyIck8v6HQsnjAxFc7vTj2VwBg4aG78VdBHQFCk+dbVcxMdwev9gTSEC455sIBOu2KLsoJFzqasP9vjCeDBlYqzn4VXXwarGKZN7Crd5QfLDT/7KpBM84c9fFUFjFp2wdk6smflRsKKqMa7EgfJJ3Ac2OKlit2pEmBTQfngdpnupoU7BUtRGiiTe7fXiRqmK+KuDn6TpvYogmBRJcrOwIJLIWxmM+dOsyLKryQAaJpjJ1/AxrGO3SqdZt7kKZJrzJWBg5piHENuY8vV6e0UOye1TyftvC5l+gZB8SHJTwpSx4q4JeTUKaxhXoR57h7Rn+3iFolJ3xvPhab6HgJG/pJ7jsNP4sUX+jZiCgEsWd/DjH5IrSYpBUAr0yHpzSoXKOP25a6OBhndh0zcX1qIYM2RIbu6i0KiHD5B/GTMHG03kTGpEL7H80wHFOWwhqDZ+SpkBOtCDYJDhZE4gRcKNbYynAqbCMbXpwpVPFbEng0aKJGbYzK1p4wIegLlcEPmdt+DjXbzcsxFlCynRwwVAwW6hjqeg0Zt521SYCWCJvbe0Un29UDx7Hgrs3IEitHXkw3jOv2fl92D8BBgAJeyqBh90ENQAAAABJRU5ErkJggg==\",\n  \"eps\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmlJREFUeNp0U01vElEUPfMFCEVArdoSqEA0KV246UJdUJM2Lo2JK/9FjXu3utJqTNz4D9worrsQExbFpAFT0TYp0CZ8pIAiyMfMvBnvm2Foa9uX3Lw7c98979x77hNM0wRf7ufPsq7Z2SQYw2QJAkDxQalUZa3WI8hy3gmZr15bu+z8kILBkCeRCJi6bufKMji0NhwiCQR6iitdatTvQ5LyOLLEiWcYukm3m4Zhmbq1BX13FyoxuH7xAlbvpqKRK1fT0PWbRwEmDEyiy1QVg/V1GO02tO1tKLEY2PIy3KEAlmJRDLXb0TeZL+n9g4MHlLJ5HIBuYnSzXq+DlcsQLk/D9Hoh1WrIUjlPcpsYGQzS3LWoaBhvKeXWMQCDA1D9pt8PaXERUjwOjEZQFhZQp9L2yERiqYRCkPt/z58ogTGqHQLE1BLgUmC6XGD5AlipBIFKkbhanKHGYLBDqQ4ZED0OAbfLlo8OIxwGvhVgyTHlA3xkomjH/gegBgDURMv6faDbBZpN+/tHkUApkdTA/PwZAPxntwdUyjYA/+ZMqJHjLgM9iv/6zRt2GgMaIE21aVIjnSm0DGPfmhzyde0UAE2Dj+p7urKCPvkZku9eJILOSMUnkvVhIo7GYIB3xSKYdhoA1erXGVKXpvFxZwdBonnD68PQ7YEwM4O4xwMPxc8RYE87g4FIcz+kvfmnA0YzIJIy77/m0OCqsTkkCTysKPjJG3viLei63Gm3kCO6UWqcMejjxecMPmxsoFKtYop6UNirYL9Wtc5OHqzznIXHq1na7OfMJROcK8a6O7MjW7nfzZdrd7jzT4ABACh3NGsh3GcdAAAAAElFTkSuQmCC\",\n  \"exe\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAo1JREFUeNp0k8tPE1EUxr+ZzvRJO62lUAQaKIQ0FVJFjBBdoIkrDDHuXJi4NnHtX+HCjW408Q/QmHTRaCRRohIJifgiiBICTQu29mHfnc7MHc+MlECKdxZz595zf+c737nD6boOYzxJLC6Nhwej7e/24HkO779s7G6mMjcEwfKZ21+/d+em+RbagaFev28qEpZwzKg3ZckqCPH1nfS8hScIdyhBe6JqTG3PfyTTeLrwFhvbKdy9/xi5QglXL0yGJsKDccZY7LDIAwWHpSferWBh+RN8ni4UylVER8MY6PHj0uSpUK0hxzfTmWsUtnoEwO3rer64jEyxim6/Hy67DXaHExvJX3jw7CX8XjfORUdDlOohhU4fAVjILCPbm9V1yIqK2FgYt+ZmsZcv4lH8Nb5upXD7+hVMjIRQa8qeDg8UTYPU5cTcxSk4nS709XTD53ZhpD+IYMAPj+TBz93fZiz5oHV4AP1fGdlyHZIkIZkrI7GyhnK9CZXy+Aig6p1+HQAY003AcF8AVtGGfLWG9XTO4MLZ5cL0WAixoT4zVmPHADSiMo3hzHA/xgeDWFjbNg8H3A7kKnX0koEcPdTu/ylgRGZgOjNv38zoSXC8BZJDRKOlwGEV0VJVGM0y4joAPO1spXbx6sNHeD1uRIYGUCxVSRlDt1fC8rfvcDnsmJ+dOaLgoAs6AVLZPJJ7WdhEkUyT8GJpBflSBcVKDTvpDBw2GzQqQT1OgaZqUOhtFQUTUKnVTVWNpgy51YLVKph7sqKYkA4A1ScEfT66vm5kC3+ofh6Xz59FQ5bpkvE4QW3M5Apoyorhl9ABIKnFgNdTOh2NkJG6WSf9eRBJtmFwLDJmriUzeaOkYvvcXwEGAIVNH6cDA1DkAAAAAElFTkSuQmCC\",\n  \"flv\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmtJREFUeNpsUl1PE0EUPbssLYUCXdpaC9gWoSTgAyFigiRGY+KjvuuTr/4A44MP/gx/gMYfwIsan0RjIjGiJIZgSIGFIoXSD0t3Z3dnd70zpITazuZmJzP3nnvumaMEQQCx3jx69SV3a3KWMxetpSgKxP3m242Do43SQy2k/YRydvds67n8a63k+FRSn7l/bdg5tdsAuM3he/5weDC8vLdqPLgIIpba2niux52mg//DqlsYSg3iztO7mczN3DJ3+ByCLgCBH4hOFEF7cDpzPCRyOpaeLGXSc2PL3HbnW3XaRQCPEgWI2MsRVAVqrwbX9bHxbhOKpiJ/bzpDOr2k68V2BtRNzMtqDEqPejY/4zSGjb54BM0mQ8k4xsDoIMauXxnqYOD7PmwScP31d0SS/eAuh1lrolFpIBQNQw2pqJdqsAlIceB1AJCIkkE/FZskXDQVRXw6IYHiE0nBEcaPXSSvJnGwWkQXAE4acAhbxPMJpOdHweoMhc9b2F8zwKizbdlyPLVH7QLg+JKBYzoorxzjz3oRzUoToaEw9KyO8XQW5AE5jrFT6AbAYVVNxCZ0Ka3So+DSTAoDiej5ywTySbls1OEDobhFlMcXxrHw+AbINEjNXgb7y6BndLhk8cRkHHbD7g4gEhiJFxsdhrDqaamBaDKKerGGSKwPI9kR9EZCaNA5ubE7A5s8IFhsrxQkgJhZoa/06xC5xRz2v+3BOjFlbqcGlquxsondT9vY+2pAJdeZR6fI355CgQCN2A4O1w7gkQ7cdLUOAKdhV6uFSv3kd/n8mT68eC8dKWLnY4FsfeZQh7nVVt0/AQYAsf5g+SvepeQAAAAASUVORK5CYII=\",\n  \"gif\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmVJREFUeNp0U0tPE1EU/trplAqlL0laiw40xASByEJIZFGVnSvj1j+gWxNXJq7VrbrwF7h10cSNhMRHojEuACVBKmH6SJQyJeXRxzzv9dyZPiCtN5lMe8853znf953xcc4hztDzZ1+C6fQMHAfd4/MBFG+p6h/n4OAeAoGNToi/eOm+A50LKRaLh6amoty2vVpZdotNXccMEK3LwZxa2bsDSdrAqePv/mLM5tSdMwYBYqyvw9zdhUn/L59P4OGtG8qlZCoH254/DdCdQBCxqZu+ugqnWoW9swN5ehp2NotgIo6bGQWGtaS8+vQ5V9a0u5S+1gfABEilAqdUgm98HDwUQkDT8JXoPPq+BoM5kCYmFT9jryn1+hkAt7heBx8dhbSwACmTAUwTgdlZ/CVKJaLnI1GD8TikZiPSR8Gxib8chH95mZTxgwWHwH7+gFMswqcokIRbjMO2HDCnZ1VvArpjEmnKZc8+cZJJYGsLsMiZ8AgwEqaY6Mb6RQR33JFhGECzCRyfAFXNu9v+RVNRZWIMuDJNuYMAaDycUFGhCOgtuAtFVDA83G5A8TrFDw+F5QMAxAKJJxz2xnW3RPJGbm+rCyjotZetH4DGzaSSeDA3h4Zl4R0JOEZWTpIzF4n/m995bNdqZwB6m0gFft3Ak6vz+KYWwFsGlqIxXItEcDt1ARMEtKdVgZb+fwA0G2C2hXM0ZTZNRcSf0b1pmXi7uYnjI+Lfanm5fRQsK8BIxKcrK7i/uIgP+Tw+FlREqHN5fx/vyU4uHBE6UO4gDWqk/JFaLuMxcXeFk6TuJ90V0HOk1in7J8AAjmgkPfjU+isAAAAASUVORK5CYII=\",\n  \"h\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAbRJREFUeNqMUk1Lw0AQnf0woK0ttVqp0hwqVCl+UBERT94F7x78Cf4Uz179DT14F8WbYHtRkBYRLNqDtdaPZLObuLs1NGlXcWDJZGbey+x7QUEQgIqT07PL5WKhHL5H46J+22q22vsWpbWwdnR4oJ80LNiz2czGUjENhvj4ctIE4Wrj8XmPUlKL9nCYcOFzE9j1OKSTCdjdrtiLdr7KhVgzEvwW6krC92E6k4Kd9bJt57JV5vFK2KfRQRV+RAMkzxglYI1RaDy2dW1rpWRjQo5VGicYIorWVooFvQVCCAjG8Omw1MgG8AM0uSBUDSnCfk/IGCHwf3DCD/7UhOLBrFkDuep/hDUSSCv1iYo4rIfqGwmUSNJjfYbBcQKhZw0aBMA4B48LwBhBt/cON80HmM9NQ6fXg/Wlku4TwmNWDzaQqzHG+0PSKod5cH5Vh2RiAhYKc8DlV1UPSyuFMGygVlMg1/P6BC6DqXQK8jNZDXAYA1f21V34wMXYFaiyVw0rJyzLgs3VMkxOjGtix/V0XWChZ0cI2i/dzvXdfTd0Qf91BMPrhyNzgKfOmxaWypqaDXHfAgwAtCL8XOfF47gAAAAASUVORK5CYII=\",\n  \"hpp\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAehJREFUeNqEUk1v00AUHK/XKf1yZdESVRBXjRSRFqMQVBA5Ic5I3DnwE/gpnLnyG3LgXglx4UDDLZS0RWkDLiRxSusk9u6GXSembmLgWZbX7+2bnZl92mg0goo3b3ffO/ncdvyfjHef6q2Dlvs8Q2ktzr16+SL60jhhZ69bO8X8ClLC7w9XdKJVG8fuM0r1WrJG4gXjgqU1D0MGc2kBTytl+7a9XmWcl1IB/hZKEhccq5aJJ/e3bTu7Wg1CVo7rNLlRhUh4oMnXoDoyhoHGyWmUe+QUbELIa7W8CjAFlMzdzeckCwFN06ATAn8QmDMMMGlMuwWucpoCHNe4jBkAMenjYvRPTyi53JvuwX8AplleAeBcRFrH6rXIxLim9I/pi3QA1RhKaYxdjkN8IwalCMIwWs9ljMkh0wzk+9M7w179C3LZNXxve2h+c3Hu91HeKmD/6zHOLnw83ilB1/V0CeqU3Q81LC/O41b2Btx2N2JVP2riR8eTUxmi0TzBwrKZMsqMoz8MsDh/DWuWhUBKURLKxQIeOMWoptYPnS1c+INZBkwISomOSsmBZS7B+3WOzZvrKGzkMAiGqNy7g+LmRkRfekBnANy2163PZXrSbrQ6vch19Xz8fPDHyL39QzkHBKedXjfu+y3AAGU37INBJto1AAAAAElFTkSuQmCC\",\n  \"html\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmBJREFUeNqEUktPE1EU/mY605a+hhZTBNKRDApNrWIRA4nEBUZdmCgLNi4MK5f+FNdu3bFv1J1EXODCR1JJSMTwpqUP6NiCpe10Zjz3hj5Mm3iSybl37jnf+c53jmDbNpi9eb+6Ftcisea909bWNzNb6dwzSXKkhIt/r14+515qBqmDA8HpqKagh53XaopblpIbe+knDpFAhPab2Dw0TKvRK7lmNODzePBgZlK9oUWSpmVNdpIU8T+jaMsyMaD4MDcZVa+NhJMN00w0n6V2nN3yQgdHWZag+LzYPTomIAtT0THVtPGanmb/BbjwLFkvn2IttYGYplKyDzsHh7gdmyAWfh5zVq0Guhg4RAHFUhmfvq3j134aXo8bd+ITnMFOOovU5jbGRoZwNxFn1cxuAIcDW/sZDjA/c4u+BNxOJyxqaenpI3z88gMfPn9Hv98HQZS6RazW6kjExvFi8TGdDSy/W0Emf4LS6R8sv11BmfzSwkPcm74Jo9Ei0GZgmkw8QCOao8OXcaz/5vSZnPdnp3ApqBBLkWJE0Ci7ASzbIhCLLQ1E0iOkBDh9NpUgiUejo8oNuJwyn0YPABtn51UYFFivG3yBGCNZkuDtc/MW+ZQI3OrYpBaARCKufk3B5XIiWyhiL5ODp8+FfFHH+KiKSqWKUL8fC/NznGlPBmz+24dZjKnD0CJDcMoyW0SqXuMtHBFw7rhIAD1ErNUNafxKBNevapwu65NpEQ4FqXIA+RMd6VwBP3cPSERb6gLIFIq61+UqGWaFdcrVt/lmAuWjAi2aiMFwmOYuIJ/N6M28vwIMAMoNDyg4rcU9AAAAAElFTkSuQmCC\",\n  \"ics\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAhRJREFUeNqEUkFPE0EU/mZ2dra7bLNpi2AxQFKalkJrohICiYkXPagXrx78Df4K48GDBzmQePLMhUODNxQ5ciEkJVqDtJGmMWrCATRbd2ecoS5u3aovmezsvu9973vfPiKlhI4XL7c2r5YL81LIELEghLA3u/udxmHnPmfGW/Wuv+LpwwdneRYBx7PeWK0wOYYhcXxyckGV1fdbnbuMsXcklqPRJQxFMKz4RxDCtVO4s3xlRjWoB0FYjlQPEEBieChwKCRGMx5uLtaKs1P5ei8IKlGa/YkXMXYtlTEDlsnw/mMXhBJcqxSK6vlcpa4PEpCooUyIqs5M6hG1o2CUwqA091cFcYLf/sjzcX75EiQIojI9779CTYR4jwTBf+r7GAwh0AxCiL6JMT/04vQ79u8aI2O/7Jzg69o6Go8ewycUahtBpADhHKLnK/eVbkMdtROWIv80NQ2sPhncA9Htwn+9hZG0rY6DzFwJl+7dhs0ZstUy8rduwPS/wd/ehmi3kwq4zTHiWUgXp+EuL8FvNvFl5Rn4xAS86iyI2kY3n0Mv48ByrOQmancdi8I0Kcj3U5iuA29xAelKCUHrEIayzltagG2E4IwkFaQgSC6lYI09iN0d8It5uNV5nG5sgJdKYC0G8WoTOZvBISFNEBxnsuzD3GX4vfDsszzqAu0jkJQDedCGbB6AWg54pYbPo+NGVPdTgAEAqQq70PytIL0AAAAASUVORK5CYII=\",\n  \"iso\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAjlJREFUeNp0kstrU0EUxr/k5qbJzdPYpGkpsUJoA2q1oLjTdiGiIC5cuXHlxv9BEOrStTvBnQvRrSAIsejCrlqpsURq2hCJNQ+TNLm5uc/x3MmzJh34mDNnvvnNzOE4GGOwx8+t9XQkfn0VE0Y5/7Z+kHm+dvOhtd3P9c/xwNZh7nWaMYtNUmX/Fct/vlN7/8J5aRRgyzm8xzpRDjGE2aVH4VTqdnoUYg/XkEhmy+Cx3DhA5tMzdFolvg5Mx3Fx9SmH0JIg79Zo3j4GADMIokJTKtjbfAKXU4Y/2NvSfyH75TFOxa9Cmr0XnlPFl5ReOQ6wNMDsoFX6AElqQlNV1KsOuNwS/AGFjEUIDhmn5+/DMM16/9igBowAzFKIswPJr6MjlxFP3sV04gaP7RzMPe6xvWM1gNUBM2UKYlBau3QghGphg29J3gDlLLilWNdD3gkvIIDRhD9yGe2mCV0V4HFXuCxT5Dlv8Dz3sIkAs03FalDxBMQSt9BRBMhNncuO7dyU28c9tnf8C/Q0ZtR4GImeQSj8APLRH772BWcgiFODffCv/t8H9tO0v3RjV7VqkeeXLlzDfvYjj88uXhl4JwIsrYxmLY/M1gYclIvGE9jZfNPrSCD3/QgLyeWTADV6wW9AryIcCkB0u1Aq/oCPumlufoF72vIheaLDr4wCLIOqrYnULA14PSoqpSJEAUilZrD77Sv3LK+cI0+Be8cAbbmAOrob0agtD491LYfkoqvnyZLsWRkA/gkwABL4S3L78XYyAAAAAElFTkSuQmCC\",\n  \"java\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAjxJREFUeNp8U01v00AUnNiOEyepQyhQobRBSlVIoRCBEPTAjQsSEneE+An8FM5cuXLNoQduIAE3qopKNJAIIppA2jrOR93aa6/N8yZuUxyxkrXr3ffmzczbTQRBgHC83nj3ca28dD36nx6fvnzrNNrdp4oibyUmey9fPBezEgWVFuYLdyvlPGaMY4fl1aRS+9pqP5ElAkmcnknRwuO+Nyt5u/ETYfyj9WrpZnmpxn2/Ok1Swn/GvtnH5k4TLue4kNfxoFoprRQv1TzOb8cAIu3+ZD7oD/Hm7XuxzqRUNDtdkuLiTmW5tFxceBXlnXgQTAORSMt2oGezUJJJrK9dFWdEH7Ik4dB29LiESeUEJXd7/dAT3L+1ivlCHr8NEzutXTBvbJPPSdO/AH5wysChwM/1HzCGlmAzOrKxu2eCud6Z2Jke2MwThpUXL6Nn2ZAVFTlNw70bK0iRnGAq9qwHtOmTRpsx1NsHyKRVnNPnoMoK9kc2BjbD4vk5JGV5NkBoEPM4FFnCteJFWOS4ntHEfphQyKaFTWFLw704AJ26ZFx/ZEEi3YyY0O1Dmr4EKTUHA8hUnS6siI0DEHLYog+b28RCRuNXR/iQUpPUEQ+NVht6Lodnjx+GXYgDSFRnq97Ed2pXSlXhUSeGhxYc5sKlNXM5DGLR2TMwfZVPAIi+otGNWy1fEZUKeo4qc4ysI+F8VksLIJfYcD9QYgB/DNPMptWBlsnBIS86xmDMTBo/PWd0LB6VZfdEbJT3V4ABAA5HIzlv9dtdAAAAAElFTkSuQmCC\",\n  \"jpg\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmlJREFUeNpsU8luUlEY/s4dmMpkWxRopGJNNbiwhk1tItbGtXHr0hcwmvgOdWld6Bu4coXumtREE3ZKu8FgOlC1kIoXtC3jPfdc/8PUIpzkBM7wf+f/hsts24YczuerGUc0moBlYTAYA+i8sbdXtAzjITRtq39kr73s/Gr9DTUYPOeamwvYnHdrdR0SnDebuCbswJGqpX+Uf92Hqm7hzFAG/4TgNr1uCwEJ0trcBC8U0Kb1/PQkHt9JxSLnL6TB+Y2zAIMOJBGLXmtsbEAYBsx8HnqCGKVScAX8uHf5EpqmGXv18VO6VDEe0PXsKABN8+AAgiabmYFNNJTDQ2RUFc8+Z9G0OPR4PKYwvKari0MAgiY/OQGCAajhMNR4nDZMaInrKBGl70SPMScck1NQG3X/CAWLE3/dAWV5hRRVIJxOWNksrP19sFgMqqAebUGYHMI6teq0A9oTVAhqu2sfbYYjsL7lCZ3683gA70T3TK7/B4BNoO020GwB9TpwfAz8LgMtWn/NkV8EHgoB81c7nYwCyBZlEVkHcqMTKFnkmehJTOPvEfCnKi0fAyADJKfXC/h83TaZTJjaa5lANLpOFqAXtlEAorAwO9u5syT5UxLfU0e3o1FMu1x4u7ODYq02BKAMAVSrSNLrK1MhLPj8mNF0vFm+C1ZvwKBwXXE4AGn1WAASazESwUW3BzUSMeJ2o1Aq4sPurvQYSRLwlhRR6mSaYyi0WlpAJrFRx3ouh5/lMt5lv8BLwXp0M4lSpYL17e2uK5wP6lj/c2ZPn2RI+YT8fDvqoyegVLyfG5kBKaQQOfvF2pLc+ifAABiQH3PEc1i/AAAAAElFTkSuQmCC\",\n  \"js\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RUQ5ODY5Q0NGMTE4MTFFMTlDRjlDN0VBQTY3QTk0MTEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RUQ5ODY5Q0RGMTE4MTFFMTlDRjlDN0VBQTY3QTk0MTEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpFRDk4NjlDQUYxMTgxMUUxOUNGOUM3RUFBNjdBOTQxMSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpFRDk4NjlDQkYxMTgxMUUxOUNGOUM3RUFBNjdBOTQxMSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PoT8zQ8AAAJdSURBVHjadFNbTxNREP52t7S0bktbKFAvTUVaw60YqkExUTD6oD74qC/yD/wp/gh885XEEI0RAyYQUiMpIBGMkYR6o23abi+73e2uc04v1LROMtnZPTPffvPNHMGyLDB7sbJ2ciUSli3U35smkK9t7x9v7n2dD/g8KUkUwWqeP3vKz23NxJGzgwOx0RC6mSgIo+WKuvP56MeUzy2nJEk8PWsGJVVTuhWbpgmHw47FB7d98Wg4mVWK52o1sxOg3Va3PmFp+Q2PdUquaFUM9/vw+O6cP3bxwm46Xwh1ALR3/vL1e+hGjcc9koScUsTSq3coVDQsXJ3wzo5HEs3clgZNMTVdx1T0Ep7cn6//QRQwMhzA6uZHLD5cIFEFSKIU+G8LK+tb0KsGZKcTJoEyP08AbpcLy6sbPKdQrigdAGaDwWxsDH1uGbliCYIgcM8WFPg8Mq5Pjzdyu4jYbCE44EepXMHuwXe+A8x3KKYxYsjvbUzmlPGpBmYdgI1oYjSMbL4Ao1YXMkcM2Dd2xnbAamPQAqg1GORLZdycmYTdJqFKk2DPR3fmwI4zBDrg9RADqxPAbPBif2WTSB584/3/TGegEOit+DRcvQ4OZJi1LgwIQKVCg2i6nb1I7H3Br3QWqT9pBAP9uDY5xjdSM3RqxeoUkfVnEOW8UkLykERTNXjkM7h3Iw6NNvHw6JjuhAhVrba0+QeALozcI9nQR0VvNxJc/ZmxCNGvIBQcpDG6udA22kyW29HC72wu8yG579ZoiSYuR/ly2+y9CA4NceWLmo717T1i5ULqJNtapL8CDACskxPFZRxLwQAAAABJRU5ErkJggg==\",\n  \"key\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAlZJREFUeNpsU11PE0EUPbM7u/2AtJUWU6qiiSYYo5EmmPDCD9AH46sx8cEnja/+CB989z+Y+MKPgMiDsYQACcbaWBBogYD92t2Zud7ZlQZsbzKZ3bl3zj3n3IwgItjYeDO3MlWme0bjUth8e8/fO2tHzx3XqUEk50uft+Ndnhdmc3SlfNPkVZT8Cy600DoIISvVfKYtlvfX1p66XmoIYsMZdjJQWvEFbbsC/S5g2QhSkKUK7rx6OzvzqLpsovAhaAxA3DUBQn2TUFsl7KwTfm4Z9DoO5LW7uPXi9Wxpfn7ZKF09vyPxX2iWcNRkKGZz0mQWKoNs8AVB6x1yRY2pYnc2LLofuXTxMgAlmlXIfngCxNxEzM+DPv6NQa2BygLgZyX6JT83ngHTN5GAL0WSoUQkSQnXkyBh/k0GegTAaldM20sTKvet+yyhIZApECamL0jUSe3oFChx3TopM4TeEQP2gc6BgGIwb4KGNXRhCkMGxgg2kJeybRiZM45D8W61qEAknSmpHStBhywu0nFVupSCTAcM4ECwqapv+NQ6LS9JGALoMIIoPYDjZiEL1xHtbyO39AQUDaA7R1AH23DSeSA4hv5RG/VAhxomPYP8sw9A4TaC9iHkjUWmrtGvbyC18BLe3GP0m3WW4I5hEBEnPIStXzyuFIxb4EkMEJ79Qa/xHbKxCdM7xeCwzUZOjgEwnuzt7qLz6T3cySmQP43uzjeIiTJM6io6W19B/NLCKMVGCzkCoLR/0lrfOI2fNy/huKC1FTsK/rbGNeMRC8dHpHByfu+vAAMAL/0jvAVZQl0AAAAASUVORK5CYII=\",\n  \"less\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RjZERjZENTJGMTE4MTFFMUIwOEVERjQ5MTZEMkVBREUiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RjZERjZENTNGMTE4MTFFMUIwOEVERjQ5MTZEMkVBREUiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpGNkRGNkQ1MEYxMTgxMUUxQjA4RURGNDkxNkQyRUFERSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpGNkRGNkQ1MUYxMTgxMUUxQjA4RURGNDkxNkQyRUFERSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pl1w97IAAAJhSURBVHjahJNLbxJRFMf/wPAIMIxMkUI7tS0VYqlGDLGhjdKkqyZ24cJFN925de+XcONHaHRj4k7TND6SGo1VWwmp2kSLhlqMDbQ87gzPYcY7k4GgoJ6bmdw598zvnvM/95pUVYVma+svcovx8yMnFZHAMJPJBJfDzq5vpX6+/vD5qo/z7DOMBdo/d26t6jFMJ3iY51jBz4M+LP6wxEw40Gy23qYzB3HO7fpmpZCOmfEfa7Xb4NxOrC4lvbPToe2yKE3K1PdPwNOtHdx79ESfq4qKkijB5/XgevIyHxEC24USmewDqD2ABxubaLRkfW6zMqjWGlh7/ByyAtxYnOPnL0Q2+gGGmKRaw8zUBJaTiS5QOO1FJnuIAM8hciaIWHgi8NcSNt+loVDY8JBXh2ojJAR1HbTSNFMUpV8Dxcjg0nSYBrtBxdLbqI1iheCUh9XXNGurAwCdEkb9QyBSFam9TDfoPZ1LUg1BH28IiwEARTVAQOzcFKRaHZpLoa9avY6L1Gfs0c32t4PU6W2lWsV8LAorw0Cs1nXftYWE3qZGqwWHzYp2zzlgetuolVFvtiDLbRRKFTAWCxx2G/KlMtXFhWPqOzsWHJwBx7rxKv2R7mwFz3lw9/5DLC/M4Us2RwV0g3U58XJnF7dvrsBOoX0Abbej/DFKRMKI30fTVGC32WA2m5H9cQQvhYi0vE/7Wdgczn6ARA9QPBrBszcp/XvpyqxebzQ0Tlsq6llxLhe9bD4cFMr9XdjLHpLv+SLGBYHAYiVu1kNOpAaRTWbCejgiw0zGhFGSK1aw+zXbvfK/BBgAPwADAs5GpGsAAAAASUVORK5CYII=\",\n  \"mid\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnhJREFUeNpsU01PE1EUPdOZKWUotKUKFLEWkQ1EASGGxGBi4sIVrt27IixN/Cn+CxfVnQsXJiz8IAoqRBGEaMUUWzofnXkz781436QDkjKTyXuZe96595x3rxJFEeTzaKW6dmdpfIoxjuRRFECGn7/4Utvarj/syWgflU5s891qvGoJePJasfBgeSpnW+yEIJVS4DEBx3FzGT2qfvh0tJxOE4mCU0yy8X3BLdODRQTJZ5oMzYaD0UuDePzkbnnx1mjV9/lMp+izBKEIwQMOzvnJGoYhhBDgFKtMjmBl9XZ54WapSjLnknMnEkQYgflCVhKXLt+/dRMy2d5OHdVnPoxeHUtLV8u2w5/S78UzBJwLMC8gAsosIqy9/ga37WNmvgKVKmEkb7JSwI3pIdRq1kBXBZJAUKkb6wd49fIzbJthdn6cIhE0XUWbyP4cmshmdZAE0eUBD6gCN0DtZwM7Xw+RUlVEJCui7CmyPaS94zC06ZMedREERNA6djBWHsS9+9fRS3p9AraOXbhELMlUQju2G2O7JAQENk0XhpHG3MIVlEZzaDbdOKO8jWy/TraGsMmL4L8KTgnIfcfy4JBWeQNp0j10MQtB4EJOg6qFMI/bEH3pGNtF4LOAjHMxO1dGvW4jXzDi7Iw60TB0jJRyONhv4MdunbDneMA6BMPDA6iMFzExcQH9AxkUiwby+QzevtnF2OU8lBT1i8fOa2UO1/FwdGTHE2STHM/14+vlPOz0RxibKPfn9AHXZHBzYx866ZdTKkuVndhHuqenS1h/v4ffvxqyvbUuAtPizZ0Dp7X1fTs+FA9cMnWd4ZG90NOjomVFzeTcPwEGACDGeYddZX86AAAAAElFTkSuQmCC\",\n  \"mp3\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnxJREFUeNp0U89PE0EU/ra7XWxpSsFYIbVQf9REFBHkYBRIPJh4wrN3DsZ4MPGP8b/wUCIHEw5EY0w04o9ILcREGmwVgaXbbXdnd2bXNxPahGyczebtzrz3ve99740WRRHkWn5cebu4cH6SMY7e0jRAHr9c3WxsVvcemmbys9yT6+uHJ8oaPefypdPDD5Ymh5w26wMkEho8JtDtuEOZFCrvN/4uJZNGH0T59D58X/C27aFNAL3Xthmsww5GCyN4+uzu+OLtQsUPxPQx6ZMAoQjBAw7O+bEVCMMQgqygs+LFs1h+dGd8bna0QmXO9OL6JYgwAvOFZKKoy3V44CgNfv7Yx8oLH+lUEgvzF8Ydhz+n41snAGRG5gUEwClzhHdvttFxfNyYK0EnJozKK5eGcf1qHo1GOxtjwI+pfvm4g/W1qtJgerYE2SXJSIL9+W0jk0mCShAxDXgQKgbNXxZq35vQKCiKQkSUXdc1+gcch1FHGPmKuIgBCdc66qJQHMG9+1NIpUylxxHtuW6gEiTIu+N4yjdWgty0yTmdNjFzcwKjY0MU7MLt+IjoSad16FoIx3b/A0DZ7FYXnsdpAjUMDOjI5zPgfoBsRodhhGhZHfBBU/nGAGRtxWIOg5lT2NtrI5dL0SB5KJzLodloqXaOEatPGztKq5gG3S5DNjuAK5NjKJfPYKI0okBkSdemCiSgS/rkQNLSePtxBj4LSCwfFtE0krqqX7ZVMnu9XlMXy2l7ME0dzA3iANQyY6vWxC61UY41zTyNcYh6/QCNXQvzi5dR39nHVq1BUyuMGAARsF6tbbe4iKD1r7Om5iFBdmW1SsDflLiuB6sX90+AAQDHAW7dW0YnzgAAAABJRU5ErkJggg==\",\n  \"mp4\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnBJREFUeNpsk99r01AUx79psrTrujVtbceabnZs4DYRHSoMh6Dgq77rn+AfoA/+If4Bok+C0CfxVRDBh+I2NqZzrpS1DVvbtU3SJPcm8SSlsJlecsn9dT73nO85V/B9H0H78OLdt/LDlQ1uMYybIAgI9n99OWxoe83nkiz9hDDae330JvxL48O51Xxm/enNtKPbVwAh0Ec6kYpXat9Pnl2GBC02HrjM5Y7h4P8+7FtIFVJ49OrxUnl7ucIdfhv+BIDv+fBcj7p/tXMPrs2RXVTw4OX2UnFTrXCbbY7tpMsA13FDSDAOQ4gJEGUJLs0PPh9CkESsPrmxxEz2lra3rnpAt3G6adgdQhBpmeLkFodNmsjpOPoXBrQTDcmFFNS7i3MRDzzPCw/vva8ikU+COQxm14BBhvJcHLGpGPTOAJxxeLbrRgAkYujBdH4G5oWJWXUW19YL4XqunAMFhnq1BqWYgaY1MAHASQOiU96zKzkU76mwehaOvx6h9uMv7KFN3RopL4oTAI4HRh4wSl399xla+00YbR3yrIzM9SzSqgJJnoKcklGrH08CcJjnBtLLCsSEGGpSWJvHtDKNoFippsJ0ulIsDDUCCATMlBQkNuahEyiZTcLsmFBKaQxaOk53TlHeKkM70AjAooCghBOk9sKtIvqtPqS4FBaRnJSRX8tj2DOh3lFB5Qw2ZNFK5LRo6w4sKt2ggAzywidAMN/9uIPSZglBLDO5FF3mRD3wHE9qVRvoHrUpfn+UEQK0/7ShtwboHJ6jdH8RZxSC57hSVETb7e5/2u0FxqPHJow+8iZ4lYY2QGu3idhIxO7Y7p8AAwALCGZKEPBGCgAAAABJRU5ErkJggg==\",\n  \"mpg\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnxJREFUeNpsU0tPE1EU/ubRdlqmnUBboa0UeUQDiUGCC1+JmrhxoXt/gBvXJi74If4AV0Y3sNKF0YUaICqoIfjgVShEiGF4tDOdO/fOeOaSKtie5GZu7pzz3e/c7ztKGIaI4vn9p+/P3h4e4a6Pv6EoQBDiy7P5rc1P1Xt6XP8M5ejXo6UJ+dWbuemeTGdpvNdiNe9YvQLe4Bi4PmTpRmyq8m71rp74BxKF2twIHvAo+f/l1T2Yp0zceHizfOZa/xRnfBRhG4CQqAYioBWeXDyA8Di6ei1ceXC1XBwrTXHPH2vW6ccBBBMI6BsSUEQzakGL6xB0tvjyBxRNxdCtc2Xf8R9TyaWTDOg2TjfVdw6hqIoE9B2GxkEDWlLH7s4ette2kSp0oDRezrQwCIIA3oGHr0/mKMmE53qo23W4+w5S+Q5ohob9X3tgHgO8ULQACC7gMx9mKQP30EW6mEHpYi8xcJEdzMucjfkKcrTfmqmiFYBxCF/Id+gayKJwoQjHdrA5v4HK7Cq44KjZNWpagaqp7QACks0H9znW365ia24DzoEDozOJbH8eVtGShXHTwNracnsG7q6LzsEuaAlNPm9h7DSSVjLyCMkppDI+GS2StQWA1RlKo0X56n2X+6QHkmkDakxF9WMVqWyK+s/BrthYfvWz1Ug+zUDcjMPMm0h3pxEjFma3CbIuCud7oMc0LL1ZgmElpGJtW3B+15HIGNITrMYIlOH7i0U41NrInREylYbu4R5qQbQBaAh95fVKZCnpQCnb9DrWZyrRERS6NDeUw+yHaXh7rt4C4B8y+9vkwn7kwKNRpDoa9aiFKBYnF+RcREqQ2e1m3R8BBgAy9kz9ysCE6QAAAABJRU5ErkJggg==\",\n  \"odf\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAi5JREFUeNp0UktrU0EU/mbu3FfE1KRRUpWYheALNBURUVy7cy9UkO6KW/+Lbt0IPsFui4gLBbUqFaUuXETUKCYa0jS5yZ2ZO557b5MmTXpgmDPnfOc7jznMGINYPi0de5UvmpORxpjE/kbNqW005DVu8TWw1H758ZfkFgNgJmtyxSPRjJIj0QTW/RDiYGXGb7Dl32/eXrVsd0gSCx9miqC0ooCdp69g5Q/h6OLN0ty5ynIkwzMwUwh2FwMdcbDiCZQXlkqFCpEoPT/wih1YjLInANcD+/Ua9bu3wJlGvrBZCmet2+S6ME5g4oGlZ9A/I70XCDhhDexPNTFmswJBwcnuXkF86VSNZxVu0ukLSGnBcqlnN4HoCQIaIuIv7LUooMOgQ7q75LAAb59B9gCBHSKgqemRr94mMKmD24CfM8nb7THYGQNLpAkUkcb66JyGBFFEWRVL57gFEH5qj8Lxwca2qS3EZaugmzAw24dR/XQgwtsCSBjPIdWbUoE2UJLBnV8Ac/ciWHsK9/glWLnD6K2vgPszsOdOQdfeQ1c/ThKoTgDn9A3KUED/52d45xchZsvorD6Bf/Z60riV3Q9Z/0bbGU1uopYGkfERSQ3VbsMwl0qlqoIARmSoPYXWy0dor79LfBMEEd8jGs/uQ3Yl7PJFNFbuEXiV2riCf88fovXhBbo/vqP3t02/ZYmJFqTkzY160Go9uEMbFK8hR/NrdXtFuUVmnmySVGgO4v4LMAAjRgmO+SJJiQAAAABJRU5ErkJggg==\",\n  \"ods\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAetJREFUeNqMUj1IHEEU/i7u7Z23e8tGgneGQPw3hZDkkhQiSuwMQREba4uUgpVlCrvEQhurkCoWqcQQ0oTAaYKNqJygGEwgHCSB6Knn7eXcdX/GmdHVPWYFP3gw78173/vmvYkQQsAwNvckq96UnyIEh7/d4t7uUd/8y+85P+bXSX4grkhI6nJYPW7LrXpBK2YxiSoShhu4Buq1NPofDeqdrZ3Z4cl7D4J3UtA5VyVAlmJoru9Af2ZAp1lcCQ3nqgiuKmbY3l/BH+MnHM9GVLP0Ww3KNA33CQoQQnL834Fj74PUGkANEIkCSSsa8gQqgYTIcB0PVsXB318GInRiCVWCkpRFAs+j5gKlA4t29Ggh4d0t04FKt9PQqF4UFgumSEA8ApeaElilWbYRVy/lsns/N1QBkxtENF4jxPxcgcB1CZVOrvMteK5IQDtJJIGh++PcX9iYwWjXK37+vP0WdYk0Ht99jtX8JywWFkQChw4tc+cZcvlF7rMze+ubbxN40fMalRMDP/6twaiUeK7wlZ0TD0a5hLTWxo2d45KKprqHKJslTsy209s2wnMFBTYNZjc/oLt9gPvLOx+hxVJIKS2YW5pCbSyJTGMK775O8VyBwDJd2LTDl/X5i8v3S7NVw9vJb51tITDEUwEGANCx2/rXEEFFAAAAAElFTkSuQmCC\",\n  \"odt\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAepJREFUeNqMkz1II1EQx/+7Ca6JkqyYiJ8cKEpAQbBQFDm0sVOsFBS9wt5KOTgEG5twxVlZ+XEnKNiIghYKxx5nwEpIIXaiSAgKGmMi0d23u8+3T7OaZJEMLG9mmPnN/w1vBUopLPNNhRWXHOyDg0nx82TiJtZPlPVoNpftc2cTotcHtxx06kdXpSQ/BvzKESZzIDmAz6y+NojOjpDMZiqRPIgNoFyWM8DrKUV7axO+gcp4g7AzmquAdVNqOgL2z2I4id1B0wgeygOyt/rLL5buLwAIDgA9dY+L+DkuDQOCrkMgBsRglcMOqAGwIstMg8AkGsuZMNUMRMkLqE+QGloglvlA7uIOAKvZajR0qJkUj/XHe0BTIclVKKlrfKsj9qA8gA6wqSJzPaXlr7ky//tdLEUfawsBjExUFGVWbT7AxSa42H2LMfODmvd3wKb7RAMLYwM8nts8xJ/pEe7/3PmP2eGv3D+9usb35W0bINoA7RmjXSHsH0f5Z/mUSZ0Ir2JmsBtD80s8/rGyzWsLFTD5yUQCbfUBHl9d38LvkdDTXIuHVBo0k+bbt06qO+yAPGXwe/cA4wO9PN44jKDG70GougIzi2tQ00ms7/3lpwnBBgjZ37Kkd1Shht5XzBIFl/ufFtniT/lFgAEAU//g6kvdGBMAAAAASUVORK5CYII=\",\n  \"otp\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAcJJREFUeNqMkssvA1EUxr+ZjkdbrfFKVD12ErYSRELY2fkH+BMsLcQaSwsrSzZi47EjJEQkEhYkFlhYSVtFpdqOqpk717l3jKZmiC+5mZlzv/s795wzCuccQncz3YeRBj4KHz0/RrOZe2NsZPP20o255zQ3EAxzEAC+6uzTw13G4TFQAakA/CWtIYbY0KBOrx7IvwDQqlHV1o3YxKTOvyAUvfQCfqmA3e4ikyS/zRAKvOot7eoSHEgZIHrCfQAfBqBaKQQDKScQAExd8emBANg+2U2CvNMkkgSqBmrCxFB8mujeoJBWwEqARcssKTAJEGrmaGrjqK1zvNknH4BtyxKl2VUpRxmj5W+x73q9AEaZrR/ND1EJluIpS3i9JQiA+a+hSq8HwJjTsLrRaWitPTCOlhEZn5N75sM1qigmlN+dB3u++Qao5W4TtbEXXIsiszGL4PA00itTsu6XnQWo0TjMTAJqfMDx/ryBJcaVzSNSH4fW0Q+rkIf5rsjRiid7yyN7uoXS3Zn0egE0NiORAN9bQ017D1Lri7CLlP2EDr3Rf7C/itzV2bfXA/igLDaRixfngFhSCooH2xVPCWBlwKcAAwBX1suA6te+hAAAAABJRU5ErkJggg==\",\n  \"ots\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAfZJREFUeNqMUk1rE1EUPS8zmabJdDKB2glEwY9ExJYiBUEQpV25qgtBXfgbpEtXuujKf+AfEKRddOdOGHClbYVCvyKWaijT2mhjphk7Sd7Me76ZONp0EsiBYWbOvfe88+69hHOOAE9f3zTVnDKNHvhlsfqPw/rM0ovyWsRFdXJEpDIyRnSlVz0KSkmvabaJeXSJBEhgAJzTDNybmtUnS5Pmg/lrN07H5NM/f13FoMgpXDSuhiIiK3Qi6LUugX7FAbaPPsJqfIHHKCStqRsXVFPQuZgD9BBxjikSiRq41AAkgCQBzVf0+BWEBX7GBm0xgHHUqk1UbBuEcIydzyCZlOI9YEGuDxwduCCitS3Xh3viCZ4jrcq4PJ6DLHd67tjtuAAXib54dCPVEfQ5XIcik/0/2iDeOYz3ceCxrisMi904y0XiMQFfkB7lg6xFHwFxEqUMV0anUNBLWKm8xd3i4zBWOzmASx0UsiW831mA59Xjm+h7HCOygduXHqJatzA7Poey9QnXjTuoVD/j/sRcmDOWLgqnLC5A2wwST+Pn8T629lahSCo291bwu9XA7vcy3m2+gTaUR14thrk9BXasbdiOjSe3nmPpwys0xSi/HpbDd3bIQC6dx/q3ZbRb/j8BEi3Po5cTJpHI9CBNDEa++GyDBN9/BBgAwfDlCVUQaNAAAAAASUVORK5CYII=\",\n  \"ott\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAdFJREFUeNqMU89r02AYfpJ0iVm7EqhVOxw7dDBEdpiCE1RoEZRddvUgbIex/Rs7eehppyF4LOzQu4MxwYp0HgShIuwwUVSCVtl0s13afl+SzzcpyZYmyF74eN583/s+PO+PSEIIeJZdrtQVI19Cgmk/Ph39bpllXq82g7sgLxVcyKNZpIx8Uj5u5zSjc9Gov8ZihCRC8D+7On4JczevGeTGSEIC4ctKJtB1DTPXi1iCCEkIm1EFlC2Em0iwtWfinXkIzjiO0jljtDC5TtflGIGUQMB+mfja/oPv2Rx9MMjpMdJxOXyXTwkcwIkewfqQ1QtQNB385zcI14FrtQexsSb6SRysZ4Fbf+F6eHwATc9gJGNAm5iCTL5n/LCVRGADNoeaGoHqyaXj5gqQlTODovcwNk5Aj6wXqV8eCo7EDhMonEHpW+dZC7gUG98D3geo7vkb01h9cAvPdt76OGy1xntUd3bjUxAk3+l2sHJ/FgtrT0MUJNfDSm0bjQ/72Hzxxo+NK+h3B7XRNO4UrwymQtMIkdTBU0m+sBOayLsn8Ka78mQDjx/e87HXPkb1+UsfP37+AmZ1fP/suknBb6nefVQXjl06TxMlJfWKNWr+Kv8TYAAkUueexJF47QAAAABJRU5ErkJggg==\",\n  \"pdf\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmhJREFUeNp0U0trU0EYPTP35qYxaW6TlDapNKWGbgo2FkF8rARB6rboXusf0F/hyq2U4krFqugqSBeuAyL4SERBstHa0iR9JKZJ7mvu+M0tqZGkH3x8987jzDnnm2FSSqh4ns0VU1ybFzj674Wa3uWiWbfsFQb+jrGj8Xvbm0HlvYVRxhJprpmTlGmum+OMm5uNPZNbtjk3l82ey8++8oW4Jv/H/wdA456g2kvH99FyHNiuAz2dwflbN8YW8zMK5Go/CMfQkAhpGsyQgRCtlpE4jIULyC9fHzu7MPPEl/5ib6WOE0JJNRiHHg6j86mMjw/2gG4bkbY4PW4Yj2j64skA5FTHdaEMPiAJszt1sK0d4suJmY4k0+IDDGRfqmh0u5gejQc+fG8eYCIahRQCEfgQnIuhEkgtONE+dGxYxEDj1DhiEycZ+1YXdUpHCqTMJIYyEES5aXXQsi2kYlGEia5GtHVKn+amPBeCutPgfLALPuVu+xDVPw2EQyFEjHDghbpYNm1yKVVnYjTOerepn4E6XQmLGSPkPkOXWATMSDcjQEkAaqOu6+i/rccALtFL53LI3r0Nq1ZD4/MXZJaWYFer+PXiJc6s3IEgY3+uPYZHTAcAHM+DTE8gnM1CSyaCulv+GrRy8uYyElcu4XfhLVpkpNtn/DGA5Uu0abFH36WnzzCayWAkmYJvWeCkfb9SwY+NDbSoOx4bYqJF8rZqVRRXV/HhzWtUSmWwmWl0RmN4v76OUqGASrmMOkntSHF8MOs954dT08W248wzYsJDOujRBAaqqikTpRo/qqd0/dv97c3Lat9fAQYA4z8bX9nTsb8AAAAASUVORK5CYII=\",\n  \"php\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAhNJREFUeNqMkltrE0EUx//ZbDaXNrvZzdIkbYOXGgxYQlCK2IIY6EufxGdB8Av44AdR8AP44JOPBR+Ego0PClUKTTXQSmkTYtOkmubSJrQ1e3H2yJSEJNIDs3PmP+f89pyZcdm2DcdWvn7LzkxFHmCIra7nm9ulg8yLZ09yXON55Dgjt1PM2iPs0+aW/frdh8bzV2/SvQBnCLiEqcFxLKSSodlrU9leiGPihWePBkgeEZO6ShC2dCAZNuf6ADb+ldQ5PUPx4BCFcgXfdwq4Ph1Dtd5CZi4Nw7SQiMdCXkl6yVIy/QBWgcU+yx/XsLK2cdHndqlK/lZxH/OpJO7fnsWY3z/YAq+g0TmHpoUH2vB5PXi8RD9Fo10aAmDJTgWyIuOupmK38rsPcOvqJO33XWEvwLJsmKxHRVEwf/MKWl/yUMf8mIloWN8rw+sP0D6PHQmYuzGNgCRiMZVA17IQV4OIaTI8buH/AJMFd02Tkp05PO4jnWvc57EDAINt7u1X8Pb9KgI+Lxbv3cFR8xjx6AQ+b+Txs/qL9KePlih2CMBCq92hg2qzt1AoV7H5YxdhdqhHzRbgcpFeqdUplpvQW4FhmAixZ/sws4BoWCM/qmsE5XqE3dDQCrqGAYWdejqZgK6GUD8+IV9VghBFN1RZJv3sT5diBwC15gncggCPJKF0WCPN8dun55jQdVpz3Ynl9leAAQAJhiGatD9AOgAAAABJRU5ErkJggg==\",\n  \"png\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmtJREFUeNpsU9tOE1EUXXPp0CAUWmJbC04xBANNTF+kKhG8fID6aqL/gPEj9E0lIf6Dj30HL03wxQtVIC0QKrWxNG1Dk9Z2Oj1zxn1m0oIZTnIyZ8/ee+211z5Hsm0bYg29fLGpxWIJWBYGS5IA8ncKhT9Wvf4Yqprtu+w3q85X7f9QxseD/pmZMZsxN9fnc5JNw0ACGGv6tPSvyvEDKEoWZ5Y8OHHObKpucw4B0t3agnl4CJPs2YkQVu4s61ORaBqMJc8CDBiIRhhVM9bXYdVqYAcH8M3NgS0tQQsFcfdKHEbvlr6WyaR/V6uPKPy7B4DT7lUq4MUipMlJ2MPDUKtVfKZ2nn/5BoNbkONxXeb8LYXe/A9AJLNWCxgdhZJagDI9DZg9qIkEytRSkdqTSFQtGILSbgc8LViM+tc0yPfukzIyOJ359k9YR0eQdB2KmBbpwXoM3Dod1SkD+scpEapCI5DdpsJhIJcjajQZagcjI+5oLe4VkeQnyiZgdIH2X6BJ7dSqQLfrggjw0AQwP+/GegCIHppNoFAgEMO1RZKo7BQgRi3yN05cnwdA0BQMAgF3C6pnbuNg92M9AFT1diSCh6kb+FGvo2MxnBB9ocZxp4Mns1cde213B81e7xwAcl4jkaa0IUSjUdLJwkL0Ej6VSvArCt7l81iku6GrKnYEU89VJlSJRmR0Dax+fI9suYxSo4HlWIw6M3FBlnD9YhiXabyOsOeIqG7TzDeIYo6EDGp+ZPb2kKKqH8h+mkxiI5/D1/19J3bwYPvPWXq2skkiJVxesqt0XzghpKM8nRVV2Lv2q9eLIvSfAAMAaacnllcFBmYAAAAASUVORK5CYII=\",\n  \"ppt\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAkhJREFUeNpsU11rE0EUPTM7ySZpmzT9DNamWAtFfSiCigr+AxF9zKtv/hvf/Aki+FEi6ov4ItWHPGiwiBUKoUqqTUJImmR3M7Mz3t0kNe1m4LIwc+65595zlxljEJzdR5uf5nLmsvZx6gSvtd9W9bjhF7jg5dH9nRc/wq8YXaTSJptb0xklx7IZoKUEz1zJ2DUU69/37vFYrDxegJ9U0lC+AoIIVGg9CL+vIObP48KDQn7x0sWiVnJrnEDg7KGk+i/Ac4iUM/R7BsmrSSxtXMfa3X7el8+Kjf3KfUJ+iRJQw4w0Tc8BRyWGRAZY3rBR/VlC+XED2ayDhZyXl03+hNA3TxNQshlGLAnE44zCIL1goXZwiMNvB1i6zbC0KuAsxNITWwgNMYPeLVJiFEO9ArjHAivrAjNzBr4f4vwIgdGD4YUACsZCE8AtYGWT5jCsGQw5wEYJzP/pj5RwYTA1b07eQmfZ8P0sgdaM2FlYwWkMgMpl6NQAO33GKM0wsQWflkh1uqGVmVWblsiDkQyqxwfag35SqcktaEWTUTHYNx4iGU/C29+BvX4Lpu/C7zYgFjegSY63WySsHyXwpYHU00ieu0bAOuJbBTArBkiXKiaAmTzcvRJUV9E8rOgqBwqlY8ASs/AadbRLb8CzeTjVClqft6FdB17tL7yeCbFRBYoLr6vR/PiSEl5BZJaBD0/R2nkOZqfQ2fsKt+0SEQ+GLSIEUvJm+6jbah2+pS2aon+4g/afd4SYJVuA7vvXdC/IHQtSoTnK+yfAAIEaId1m+vudAAAAAElFTkSuQmCC\",\n  \"psd\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAqxJREFUeNpsU01ME0EYfbtdKKWGtoItRWgJHApCBE2I0YuoiSaaeDJeOJh41YN3TfTixcRwMfEk8eDJGA+Eg0YTTRRMg02KKFooCBbTlkJLS7f7P+u3K9Xo8iWT3Zn55s173/uGM00TVlwZfzJztD92iKO5ouvQGQPHcQDN380vlDPr65fdLj4Oa41i9sFt+ytgN7o7woGOrqgvvpLBaF8vWj1NUAwGTVNRM3mf5vU/zaU+XySQuTqIFXz9hxmGLkoS7r+YxvVnrzGzlgXPDOzUZPT4m3Dt/KlIuH9oUjXYEHZZ/wOgGQZi4TZcGI5hLb+FO++TSOSKcLtcMA0dI0EPrp4+HtnfG5skiUecDGwQE2MjAwiGWlFVNDz+tIyCokJhPKYSX7Gdz2I01hOJdnY9rJ/7UwPGTEiqjtbmJtw4MYx78S/4Wa3h5UoOYwPdIOp2Xi/t18rlFgcDw6o+ydiWVRwOBnCpL0oOAMmNEhLZIgSeoxwGSWcERon/M9DoBknTIdNQNAMnO4PIVGpIFXcwndlA2OtGc4MAxml27p4AIulWSIa9QVadiYSoJxhqBJivKgh5ad3k9gaw6JdlDaqq7q5wINY4F22HaLHSDZQkBW72O9cBYFEviBIURQH7a7MN0uDisUW12ZZcaGlmdq4DwCqeTo1zNtZuW7hUqGIw7MNqSUS2ImNsKEpSdEwt5lGhfQdAkQBEoub3NNrDJfAIeBuRrcrY5xGQ2RFJAjl00I8PCckJUCB9q1URBnk38XEJEuk41tmGwZAf66s1VOh2keqwoUnYpFxHH4iKIixkN3HzVQKP3iQR/5GDKMuYmE3h+fx3MHqh1sMafztHLuiCg0FAk0uFdLqcpGY5QEXbTC/j7mIaVjc18DxufUtBJ/vcggs+3ijVz/0SYABsJHPUtu/OYwAAAABJRU5ErkJggg==\",\n  \"py\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAlVJREFUeNpsUktvEmEUPTPzTUFmgJK2UqXQFG3pA6OBLrQxamJcaYwuu3Dp0l9iXLvVtRuDpgt3JIYaTVSaxtRHsJq2xEJBHgXmifebMhECXzKZme+ee+65516h2+2Cn2cb2VwyHl12//vP2/zOQaF4uD7GWN69e/LogfNm7kUsPBFaXYwHMeK0OlpQEJApHJTuykzK98dE98O0bLM/UNgr4v32Dj1fwSQRt9dSsfmZcMa0rIv9ODaqYrPVxuPnL1Cu1aEbJu7fvIZUIo4bqeVYRzcyv/8c3SPYpwECt/dmu4ON3Ed4TymI+hQc1ZqoE+F+uQLDsnHlwkKMscJTgl4eJOi9fxZLePNhGx6ZQRRFqH4VjZaGSv0Y6cQcJLpra0ZguIWegqDiw7lYBBZV6xiGk9DQDLzK5bEyF4Hi9VLMsoYI7J6Es5PjeHjnOl5ubqHaaJGBEkzbxplQAKIgDmBHekDTgI+qKKqKLvNApgmEgyquLs1CoFn2Y4cIeLJpkjoCLkWnUSIF3JxISIUsCjAoxhWNJLBIJs3YeXj/08oYZkOKY65HllE/bkMmY504YUd40HUq2JSSyW6iVPmLiXE/ZMYQCU+hXK3h1toqdNN0sEObyKtqtDQ6kXDwcadDS2TBryp4nX2HxXjsJK6bDnZIAZem6Tp5YMMmicn5OC4lztNWtvB9cg+hQABtWjKL2jH/T3GgBcYDXEE6mcDM6SlaJAGMWkivLBC54ZgniZaDHSI4rNSqn7/t1vgkGJPwZXffSeCjk2iUWz9+nSTQN8e6ef8EGAClUi/qoiOc3wAAAABJRU5ErkJggg==\",\n  \"qt\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnVJREFUeNpsU8tu00AUPU5sp41NkzRxpfSZqi0VIIQqEEJUZYXECvbwCWxYsuBD+ABUFrDrCnWBQEJdIWigBSr6pqRJ1ebhxrE9M7aZmSrQ4o505fHMnXPPPWdGiaIIYrx89GKpNDdxmXkU3aEoCsT+z8W1Sm21+jCpJctQTvaerj+TX7WbnJ+0cpfuX8mQtn8GgJ4AZtIFY2Hz3foDVRcgyt+cRHcS0IARh+D/8G0PpmVi7smd0dLs+AIjwTVEiANEYYQwCHlEZyJgIQKfoX84g9uPZ0cHZ4YWmE9nuufU0wABCSSImMsWEgqSuoqA/39/swZFTWLy7vQo7dDnfPvWWQa8GuOV3IYLJXmyzDzG2/ChZ3pwbHdQ267BKJoYuj7SF2MQhiF8LuDK/Gf0DKTBKINz1IbTbEMzU1ANDW7LAfEIQKIgBsBFlAx6LYOz6MAcvoDCtAVGGPKlAiIu/F55F33FDA6W93EOAOMaMOl7biKPwRtD8Foetj5sYPfTDtxjl1f3Ubo5jkQieQ4ACSUD2iE4XDpAdbUiW9D7UsiN9WNkZgxajwbd0LGzt3keAJPUc1N5SVeENT0Ao2BKV6QzwlZeRBSKAYhe3aYHcZWn7l1EfjyPypcK9LQGa8qCvW9j9+MvaasQOHaRhGWdhsNLR8hwodYWf6B4tYjDjSOovRqq32rSYq/lytw4A77o1V2ERiAtzY5kkUrrsH+3QF2KY87ArTtQuQ6nAf4x6FCV1D001+vYersBM2vA4y1Rm2D7/Rac/TZIw4d/6MrcGAPf9htN0miJh7Lyuoyvr8rQeP9iVJcrSKgJ+TrFcyYebXTP/RFgAFQobmIOBxbsAAAAAElFTkSuQmCC\",\n  \"rar\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnpJREFUeNpsUktPE1EU/u68OgylZXi0hZACQU1LEKKCMcat7jTRnQsXxsQtv4E/4M74P1iriUaNCw1FgxpjCJQKKAU60+m8mJnrmSll4XCTc8+959zz3e88GOcc8aq9evChOHl/lvMoubvWX/z4+BwTlbvw7bXdg8b7h6LE1gGW+O88CRMt4XTlR6/rYxce5Xv3jlHH19fPkBu+gWy5mlcFb3Wn/umeKOEMJF5C7xCFbtA9dRXjFoYKGiTRAlPGUV1aKU9O3VwNQ74A8DQAIZxqAuAhBPIMFYpQVAVB4CPSZjEzv1weH5tbDQN+JQ2Abu488mnzIbAAA3o/VK2PwDJo7r5Fy7ZRuvi4PFS6+qIXdVYD8Jg6BUcuOD8BozSLlRWyicgVKkTMQWwUlFF0Ooe5FIPk57BD7G0SiywyjD8bCDyHsOkeeeR3SUxEkROmU6BfQYFJMHfhWXV8efkUrb13VPMTsrcTQSzxZ/+n0GVA6EGbSGdgG9vo15fg2nFgbO8k70SRdd+mahDT81vUxTZRlJBRMsjq89C0EXCvSf7TIBZ136YZUJEiE7LgJ2dN01BZuE0dkIhxE7KcQTK1QUj+cwAEyrPZ+IydzRoyah+mLy2isbWBweESJEnB9q+1RM9Ub9GQOWkABg8HjRr2d9Yh0hTlBlRsfn+D4vg0BvUC9rZqECUJuk7Tzr1zahCYlB6HJAREPwfbbMBzLBzsbUKVI0qBgQkc+SxgWUYaIAqOpKwKXJ6bgGlaaDV/YvHaFNrtDsKTfVSrJeqIg/bRNwjclFIALeP3saybhu8SC4VBHwnhBXXIKocYRXD9QzBi4Xgchmkd9+L+CTAAMqwy+ZzluBgAAAAASUVORK5CYII=\",\n  \"rb\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAixJREFUeNqEUktvElEU/mag5f2yJhXLwxIt0kiqsVEXujP+A925cu1Pce3WtXVtYuJCF7KtTY0NrVQIpRVKeXTkMcO9F8+9ZVooJJ5kcmbmfOe733fO1YbDIWS8+/g1dycVX7W/xyO3vdsuVKqvnE7HZ230783rlyo7bVBicSGyfjsVwozomVbIPe/c+FmsPHfoRKJd1HT7hXHBZjVbA4aA14NnD9bC2VR8gwuxPi5Sx39Cp+M0XUP0ahhP1jLhW7HFD4zze3b93ILtXYyyVKlR8/5hFbnvO9gtlrGSjOF+OpXkYviWyo8mCS4R6bqO4p86vm3v4fC4DrPfw4unj1XN6JvBaQtjChzUXK43sVU4wNFJA43Tv/B73edQwTmfIhAjCVL6UdPAj1IVFSKhCdAcAI9rnjBiAjtBYEu3GEeh1sKJ0YXR68sVIujzIhzwY8DEBHZqiLRKkicQDfvABxaiQTc4Y/C65pCOXwcjcmlvJgHtlwi4epYifiQWgmoLZwPW6HQG07LgcOgKO0UglAKOTt/E+09fwAiUWU7QAE9xUK3jbvomsispZVHMVEDSZdHo9rCZ/4VIMKAu0XGjpU7d2S8hk0pCELHEzrjKnCQOYJoD+Dxu1RyiwUm5LaMDo9NFt2cqDLvY4oQFp/QpfT/MrmI5FkWebt+NpWto0j2QmQkOjZ9hpwhqjXZzM/+7LU+cc7lRrjXh8/lVLRK5ovLWXglOsiOxdt8/AQYAzv8qbmu6vgEAAAAASUVORK5CYII=\",\n  \"rtf\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAe5JREFUeNqEU01PE0EYfnZmd5FSvgLYFuwWt9EgHyEaox68eDJevHvwJ/hTPHv1N/QgZ2NC4g3kUAQKFKGhjVKqRrvbnRlnht262FHfy+y8877PPM8z71pCCKh4/ebt+rJfXEz26Vjf2mnsN5rPKKWbVpx7+eK5Xu2kyMtNTd5d8MdhiJ9BOO7atFI9ajy1UyAqSPIRMR6ZmoNehNHMMB7fX/UWvEKFMbYKE8DfQnAhwRmmJkbx6M6S5+WmK2Evup2c9yUk2nnKA0XVcSiGXAe1k5beP1i+4RFCXqnPywB/AKVzK34RjHNYlgVKCH50w7EBBogbTa/AVM5SgBdn0gc2AMDjPsbFPz2xye9asweS6n+NTbG8BCCfUtLjff2WoVnVpAH6z6hMUtJE3EykYfpF4vUiL3QNS7FMeSAQRBHW3r1Hq91B+VoBQRji4+ExFsvz6Hz7jm7Yw5OH92AcJKW9G4SoHhzhy/lXbB98Qmm2oCXN5WawsV2TACEoJXqwTKOsb3BtR2ucmZxANpPB8JUhyPnHWDaDpfJ1eZFALzJJ4MKO5MEtv4TSXB7V/br8iQLMz+almRZWbvoo5q9qRlxwewCgeXbe3qrVO5ZkUD/9jJGRLPaOm6COi92TU1DbxYe9umRD0DrrtJO+XwIMABWp9nS+FgaoAAAAAElFTkSuQmCC\",\n  \"sass\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MDNDMTBBM0JGMTE5MTFFMTg3N0NFOTIyMTQ2QzhBNkQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MDNDMTBBM0NGMTE5MTFFMTg3N0NFOTIyMTQ2QzhBNkQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDowM0MxMEEzOUYxMTkxMUUxODc3Q0U5MjIxNDZDOEE2RCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDowM0MxMEEzQUYxMTkxMUUxODc3Q0U5MjIxNDZDOEE2RCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Po72XUcAAAJcSURBVHjahFJdTxNBFD1bykc/ttvdtttWGgI0bYrUgDZoNYqRJ014kMRXHvwB/hQTH/wFhMREJfFBQxBjhMRIFEQSCAlQxKYGggiU3e3HbnfX2bFt1EU9k9m9mblz5p4zlzFNExYmpue/jmTSZw5PZAl1MAwDT0c7O72wvPdudeNakPNtOZ0tsM7cvzdOc5yN5LDAsTFRAJks/kC2PxFRVe39Si6f4byez62EpAEH/gNN18F53Ri/Ocxf7OtdLMpKT42s/ZPg1cISJp/P0tg0TBzLCoK8D7eHh4RkLLJ4cCz12AjMXwgez8yhqtVo3NbqRKlcxcSL16gZwJ2Ry8KVc8kZO0HdTKlURn+8G6PD2SZhLMQj96WAiMAh2RXFYKI78lcJcx9WYBCycICnpNbojUWpD5Y0C4Zh2D0w6hWc70uQZC+IWfQZrXF0IsHvY+meBd08haAhoVMMQFJKWF7PNZM+klhRyogGhbqxOIXAMOtEwGAqDqVcgbVkkE+5UsEAWavf0az2t0ZqvK2qabh6IU3joizDwTgwej1LdVfJXkdbK8mt2QkayO99A0/0trQ46I1lVcX+UREhnsP34yLp1AD1xibBMuntpzU8mJyi3Tc1O4+l9U06n7x8Q/8PHz1DrrALt8tlr0CrkbJMHTop9Sk5sLa1g8L+ARJdnShKClY3tunN69t5iGLYTlCtakjFY7gxNABdN3B37BaqqoYT8pyX0in4ORbRkIA46YlDRbUTbBZ2Jb/Pw4qiKFnapcpPo9pdbrg8DjAOBsFgELJmsGs7eWkkc5bu/xBgAHkWC6UPADTOAAAAAElFTkSuQmCC\",\n  \"scss\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RkM4QjYyNDVGMTE4MTFFMTlBREZCNDNEM0ExMTk0MUIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RkM4QjYyNDZGMTE4MTFFMTlBREZCNDNEM0ExMTk0MUIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpGQzhCNjI0M0YxMTgxMUUxOUFERkI0M0QzQTExOTQxQiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpGQzhCNjI0NEYxMTgxMUUxOUFERkI0M0QzQTExOTQxQiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pkf1yeMAAAJbSURBVHjahFNdTxNBFD0tLULpB91uodVWPmorUIxo0VSiNSExMYYHE33l0Ud/in+C+OSjYgjRGDBRCKJIUkIEWi0WKlja0ul22+5219lJ26gLeiezuXvn7rnnnrlrUFUVms3Mvd2bjIyezRVLBA0zGAzo6jhjm1te+7EU37rFO+w7JlMbtG+ePJ5mOaZmci/nsPl6ONBtw18WDQc9tZq0sp7YjTisXV/NFKRpRvzHpHodDqsF03djzuvDg6vHJWFAprF/Arxe/oins6+YryoqCiUBvNOO+7FrXMjnWc0WyIAOQP0N4Nn8IqqSzPx2swllsYqZl28gK8DDyRvcxKXQvB6gISYpiwgH+jEVi7YAfW4nEqk0PJwDofNejAX7Pae2sPhhHQoF63U5Gai2Bn1epoPWmmaKoug1UBoMrgwHabIVVCx2jdrKFwm67TZ2plldPQGg2cK5HheIUMbaZqKV9In6giDCy3MNYXECgKI2gICxoQAEsQItpNCHWKngMo01arTY/jFIzbutShJuXh1Fm9FImYiM7tTtKOtbO+toN9Nc+fQ5SGUOIVYl7HzPIH2YRZ0y2KZ+sVzBHn2v1mpMGx0DTaR3nzfwfGEJdybGkdo/wEigDyvxLzg4yiESvojZhfd49OAeLJ2degaSLIPOO6vwgiYaaRErTRREEdn8MeJbSVZ5M7nLdNExqFLaQwEfFfACQn1+HBWKSKb3MT4Sgstuh9vVDa+bQ4DORE6o6RlspzMk9TOPfr+fiLJCLFYr3TZSKNcI7+aJwWQmPM+TkqRg49tu65f/JcAAMwMas6WUKd8AAAAASUVORK5CYII=\",\n  \"sql\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAh5JREFUeNp8kctrE1EUxr+ZyXMkoa1NBROaSkpTBE23PhZ25cql2y5duvAPUdGFS1FxIRRBXZlFQ9GVdDENIhGJxkDsw2mneZnM83ruNZlOmNoDhzlzz3d/9zv3Sowx8Ch/qlYK2XM3cEJsbH0+qjV/rd6/u6aN18b7RMFT+9aosP/Ex+0ae/puw7j36PlKEMAzctKJ3aGFamMHjV0d+wcGitkMrpWWp6hVIciEk2MAOwbUWjosx0UiFoWqJpGMx5DNzODq5aIPoa82AWBg/lyKLMH1PMp/a9XvLXLzG1cuFlBaWpiKxaIPSLY6CaC93ggQjyiQZRkeQSzLRovGaPciWLt5faSWEBoh6KBvOhiaNga0+Y9pwaFxvu7rfp8F5pWDt+qNMp2IijHGwddWCvN+33/CoAOP5nVdT9SdoQ1JkggiQ6Yvr7V60+9z7akA2gfH9cRF8hO5F5Ve4lQAF9uuK+qFsylkzsQxrcaQm04hdWkR83Mzfp9rQ3fAFzu9Ph6+WMfjl6/pGBdb2jbKmx8QlRjWy5vkyhUZBPgOeGNHN9AbDLGUz6He2hVj3Ll9C8/evsdgaMK0HV8bcmDTU0UUBYXcedR+NLGnH0I3jvDk1Rsy46FP4C/1BtrdntCGHNiOAzWZgEKQ5Qt5lIqLojbaXSQTcRy2OwT4SZqk0IYAOgkVWUE+lxX/zb0DpFNpkTzmZmfFtzewhHYcfwUYAMZmVaZQlLFHAAAAAElFTkSuQmCC\",\n  \"tga\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnxJREFUeNp0U89PE0EU/ra725K22ILRGipb22pMG6JcSEQTbUIwnozxpBcvepeEP0KPogcT/wlNT17kIKbEmChFUYKGVtL0R2gLtNCl3Z1Z3+zSAlonmezOe/O+973vvZEsy4JYnqdPMu6RkSQYQ29JEkB+PZcrslrtPhQl23VZc8/tr9I1yMHg0EA8HrBM04lVFAhoY38fSSDQVN3pfKV8G7KcxZHl6v1xblqU3eLc3p2VFZjr6+gQgwsnhzGTuq6Nhs6kYZqXjwL0GFhEl3U60OfnwWs1GGtrUKNRsKkpeIIBpKIRtI1J7cX7hXRhc/MOhXw5DkCZGG2zXAajzFIoBMvng1ypIKOqmP30GW3OIEcimovzlxRy5RgAFwDEAIODkCcmIMdiQLsNdWwMZdJlg8pzEUt1aBhKq3XinxKYqF9yQbqRIqsMy+0Gyy47bKgUWXSLtDENE5wdtuqQATm50F1VnPbRGeEw8HXZbiV8fsDvI9ldju9vADAyihLEbrWAZhOoVp3z6iqBUiB1A4nEfwCEsbkL/M4TgE5n5jDx+oTEzp1d8m9tC8H6MaAB0imzx0NU/WKUYE+loEyawDBo2ui6TGfT6ANAxrvx87gYCGCxXEKVJvCWFsG3eh1vN/J4OD6Od4UC8o0G3TX7TGLHwI9iEQmvF9X6Fh7F4/iYy+GcLOMSlfEgGsP0qdNOmX0BiGKpVkV1bw/1nW2b/gCpf1PTcI+Y7eg6ps+G4bG4PR99SjAVo9HE4q+fKNE0vl5awuSohjeijbRefVjAtUgEQRK7Yhi9OKn7nKWZxxlSPWl3QwgnaIrW8QMhD542vUbx/W49m7sq4v4IMABOqi3Ej7bAEAAAAABJRU5ErkJggg==\",\n  \"tgz\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnhJREFUeNpsU1trE0EYPbMzSTfdtInFtkkpiaXVWou2FRUEn/so6JugL/oH/Af+B1988if40jcFERQURNBSQdDWlLQN2lsue8neZsZvc7FoOrDszM75znfOmVmmtUYyvry++36yfOeS1qqzDtvH2P76ApPlW3Drb2sHex/uccHWAdbZX30kO2+B3siN3zhTnHuQ66+95i423jzFzOVljBdKOZNHazvVT7e5wF+SZBj9iZJ+3J11mbW2kR8T4LwFli5i4fqTUvnczTUp9RLtDhKgJx0q4dEwWAxrREKICHEsoYYXMXvlcWmquLgmY71yCkG/c0AkARgLMZpnMDMpGNzEYe0dGp6HwvmHpbHC1Wf9MnFCkHQOyYEPzSJwQ2B65Tm5NZG3Fshim6wbMNJn4bpHowMKtIqo2COgR2IcAptwjvcgo6i77igjEmVDqbY8xQJ1VwRULhiBI6+G9Zf3cbTziuzIDkmHSNqECTFgQScEcYuc2NA8TcdYwXD+GkK/TYVN+u72WrIudiAD8o6oAR2RRCmQMjis3CIy1iSpPySCXhFTXeyAgh4BR+JVw8pauLi0Cp4yCX9A90FQhnSBYtnF/k+Q+HYam9itfIZB3QvT8zj8XSW5EhNTs9ivbSLwPUzPLNPJBIMEKnaQYg6aB9+RGR5F5VsNgnNKXMI1NdJGG5WfHzFVLJ7k8c8xUngpVodlDSGbFYj8Y4yMpOG09lHf3yIFPzA3fwHZTAQVtU4JUTeFDrdgDdlI8wAz5Qy2KxswReI7QODZcOr0ZH3q2hIDBI7zq16tuk3FNPxAI4wN+pkoccYoE4YJU5EdUtM4Qst26v26PwIMAKj3P/2YUKgYAAAAAElFTkSuQmCC\",\n  \"tiff\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmRJREFUeNp0UktPE1EU/qYzHWstlrYJNcWUElyUJsaNGh9B0g1Lo0v9Ey78EbrVxBhXuHShm25YGBJRQpAYBDEWpaEPEhksdVpbyjzveO4MfZDCTWbauefc736PIziOA77OPH2yJCcSGdg2uksQAKofFou/7VrtASRpvVNynj13f6XOhjg8HAlMTIQdy/LO+v3uYUPTkAHCTb+cK+0pdyGK6+hbvu4/xiyHbncYAwfR19ZgbG/DoO9LsSgeTd9JXoxfyMG2rvQDdBlwIZauQ5ufh12twioU4E+nYU1NIRCNIDs+Bt28mXzx8VNuZ796j9q/DgAwomwqClilAmF0FE4wCInAlkjO4y+r0JgNX2os6XPYS2q/cQyAcQatFjA0BPH6NYipccAwIGUy2CVJFZInkKlyJAqx3T4/IMGmJkeWIWSz5KgI5pdhb3yDXS5DSCYh8rTID8s0wexeVD0GtMd85KkkefFxUfE47M1NokbJkByEQl6tL+ouAI+MUwbFhnYbaJKc/Sqg0x4H4eDRGDA56fUOABA9/GsCpaIHwr8FOhQ823O5RfW66tUGADhNy3RNRDjcN41HLxdQ8J6jYTsOQLfOJBK4f+s2/uoathoNGKT1MtFeVHZxdWTEZfEq/wMKl3rCJOIzTV6ADs2R5ulYDDNkYjp0DhrF+zCVgkw31+v1UxjQZkNV0SADd2o1MIuc9gmY+/kLxb0/UFoHePd9A1qzeUoKpilx9xcLWzgg+u/zeVfuQqkM9bCN1ysrWKXxdtPgvScwUAm58XZ52W16QyPtifRUzi588GbEi1ztHPsvwAC4uC9qhnsZvwAAAABJRU5ErkJggg==\",\n  \"txt\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAeJJREFUeNp8UrtOG1EQPfsyXiyzBguIJSyChZBBEFCKpKHLo6egpErNn8CHgH8gkZIiTSIXLhJAWCgkoMgRMSiRBSK29z4y9+I1d/HCrFb3MTPnnjkzlpQSynY+fP70fGF2gQuByCz6lfdd9Uurfvrrjes6762eb3tzQ69uFJwPsqOPC+MBEmxxphi4tlU5OGmsOzaBWLc+O9oIIVhScidkyGZ8vH62nHtSKlaI4cse6TjAfSaFBBcco0EWqyvzubmpyQrj/FXk75cQaSEMeMXU8xykPA/Hjd/6/LRcyjEpt2i7HAe4A2TeLZWKUOJaVLxj27j813EHGKCXaAJExu/4BOdiAED08riQD2riOrexyRoYc3CvsAbLGAAjZga7vgZG23WMCdBvoxKJc36TRBlMiaa2JByjNqqD8qkYc1pjDK7abey+/YhrWlfKswhpiCR96aEU9o5+QE3g2ovVWDm2Sc22bBQm8vrVpbkS9r+doPr1EOWZaQ0yFoxg2PcREosEAI4uvZhJpzFMP+cSXRbq+043RManez+tNWKMI6GN0g0Z04HFR+NoNC/0yx717efZOSbzY3AcR4Op2AGA5p/W31r9e0vNgSrh9OwCrpeCkqvZuqTybnpRqx/r2CjvvwADAJC/7lzAzQmwAAAAAElFTkSuQmCC\",\n  \"wav\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAApFJREFUeNpsU1tPE0EYPXtpKbX0wqUQKVQMFdIXQBNCQBs06KP+B8ODGh+Mf4b/4IsGE54kxhcMBrkp7YOQgBRvSKG73fvsrt8Otoask0xmd+b7zpxzvm8E3/cRjPkniyulW0NFy2JoDkEAguOlpXJ9p3L8MBqVl4O9YHxae8pXuRlcGO7KPLhfTDVUqwUgigJMy4Whm6lEXHjxYf3XnByRN0QB/2KaH7btMlUxoRJAcyqKhdOaht7+DJ49n+2cvTnwynXcsb+kLwJ4rgfmMDDGWqvneXCZS9ND7mov5h9ND85M9y86Dpto5rUkuJ4Py3YDJpy6QGJPayqB+Njf+43XL220t0cwOZkfrNXsBUqZugDA6CbLdAiAwaek1ZU9LmP8Rh6S78GsGxjOp9FdzKJaVZIhBgGASzK21w/wbrnCk8euX+EMAjaaZuPHdwUdHVFYluuGPGCORwwYjg5rqOwccRk+3Ux0IEvntmsNG4ZmUayL/wAwKHUNfZfTKN0ZRaw9Cof8qJ/pMAyHy5KkAMTksSEJtnMenM7EMVMawbejMzJRh67bXEYiIXEAVTW50SEAhzqwfqrBcXx4VOhYm4RsNgHbsJFOyZTsQ1MN+hcohoUlkFiMT+TQFpMwXOjGpXgE+XwGk1N5pFJtKNCequgYGupCRBbCDOp0KBJc4VoP3dyBONW8uydBgBHUThqQKCk3mEZ/LoUG+RBioJO7VarAwEAntjYPiUUW9Hh4b2R7k9j98hN37xWx8fGAt3eIAdVMLn+uUv+b2KReSCZjZJiB9bV9jIz2ofr1BKvvd7G9dRC80lae0HzOt+cWVnrSKDrMJykifwNBpCgE/UAllEXufmDu8Zlffvvm8XSQ90eAAQA0pF7c08o4PAAAAABJRU5ErkJggg==\",\n  \"xls\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmxJREFUeNpsU0trFEEQ/mamZ3Y2+0zIC2MmITEkUYgERFQErx5E8KTi1b/h79A/4SW3nCNeYggBYZVEMU/y3N3Z7M7OTD/G6lk2ruw20zRdU/XV91VVG0mSQK/3n1a/jky6d6Xs3G8WXS+Pw5N6LXjLLGuna/78oZKerGsYKtrDE16uJGL1L9gEOOcYd2dL1fNwrbL//aXN7J1efPMmkUqEFAk0A0VZNbFEaQCBscIkXj975y3NLq9xye8PBkAniHOFph+j2eC4rsdoB4LsFubGl/Hq8RtvYWpxTQi52o1jvWiGYaRZL0/auDgOkC/Z8BYL2Pqxidp1FZkhoDxpeaXA/Ujuj/4HoOxKKjiOiek7RUShRNQWaNYFQuMafrYCxiw4ozZKfqbYJ0EvRdl1DQyyTs8XCNTA6UELMwvDyLpZWIZNNlNLlQOK2LMJRJ+5AkuZ1S7CFFzJzk56GnUjQWlYkqCoBWFbonEVYcLLA4dNnB624GQsDBWIgfZJEgxkoChzSFWvn4VpQemDm2VwXQsXJwF1h6c+gxlQ5jgSiEUEt0wdIe7tMES+nEG2aCLiJMOIIWIr9e0DEELAMUrwRuchVAyTKimUwO75Jm6VF3Bv7imOaj+xd7UFKVS/BPJF1b/E4tgTrE49J60O5kceoNqowiuuYKa8ghHXA48U9MT2AQgyRvTThE30bQiaSGa4yLMJNFo+Dq/2cHt4CYlwyFf2S6BHwwrMw/avDbR5C1k7h1YQ4KH3Amf+AcZyEbZPv9CItzQD1l9EbtYOjv74v/d3O9RMPTDrsEwGIWN8q2yk7XNYRs9JrRv3V4ABADSGR6eQ0/NQAAAAAElFTkSuQmCC\",\n  \"xlsx\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmlJREFUeNpsU8tqFEEUPVXdPY/ueWZIoiYZiSYKYhJc6EbduHOhgijo3t/wH1z6B0JAhOyMILhxo4kJGk1ASTAxwWF0Mpp5dHc9vFUzYwidaoqmq+8959xzbzGtNcx69PTS26ETmQtS9r4Hy/xv7MW7jV+th5yzVcaYPX/++It9u4NAv+CVR6tBUUTqMJsDcRzjZOZM8W9ZLKx+/XDb4e5/kH5In0lpIYWGUaC0YTZnBCAEKoVR3L36oDo7NbsglZwbqD6iQKOXFMcKUVfBkBAoQhlD5xxMDp/HrSv3q1JgYW3z0x0KXzkCYJaRZljru23aHWTzLiamAyytv0O9UYdf5PArqlppBfMUfu4oALErqZBKcUxMFRCHEp0DgW5Lo4N9NIN1dF0XXsVFOUyPJTzo+WBANDidjp8tgHGG3c0DnJ4uIRf4cOCBaW5KjY8xkZL72xpJ9QcFz5bVqHUJGHZL2YtNmKi06YCyiVFb4s/vEKMTAf1p4edOG6mMi1zR6wEpdUwX+vLDtkCzHoK7ptcM6ayLmGajvtex4PliyoIkFRjmUEASelB2rXQRSfjUCT9PlWpmW21iTGzCAyEkUixPRqXhe2V4zKczbdmybgkpJ0cGOuA6Y2MTCsKoi5HsNK7N3MN+uwYaWbxYfoLLkzdxcew6lrYWaZhm8PHHG3zffp1UwJSHz9vvkU8PodbcQYYYS5lxYkxTkGdVDQdV1Js1qPgYD6JIuIE7gsXVefIhIuM05k7dwMbeMmh87a18ufIMaVYyprrJLgje2Nr+1tzYXANnDnr3zRhHj37Vvy2wpXHtNAd5/wQYAD6WMuT2CwoVAAAAAElFTkSuQmCC\",\n  \"xml\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAilJREFUeNqMks1PE0EYxh+g3W2t1G0sEqyISynUFJsSOShNwCamiYZED3LgIkcuxoN/iCZePZiYGD2aGD+i0F5KMChxlVaakAK2ykcAt+WzdLu7zkxo3WZL4pu8mXfmeeY3885ug67roPFh5nvc62m9hjoR+5LMp7MrkYf370qVtco+VtCUFpbj+jGR+JbWn76OyQ8ePwsZATQb8R/hanZgINgj9IqeuBFCw1Kt9OMBnNWCs24XwkG/QKYUEiGjVAPQof/rq0783pShET3ULQo8xz0iS5FaANmrHQH2DoqY+DSLSz6RzecWlnD9ymU47LYjd4O5BXqDTG4FM3NpTEkpdJ5rw0AowLRMbhUfp58gTOaD/UHmNQPI6YmvKWRX1zESHUJ/oBs2nmPa+Mgw0ZIM3tZyGoJwygzQNB2jNyJIZX7iB0lpPoM70UGmPX8zCU+rG8NDVxHwdiC5mKsPUFUN/gvtLLf39sFzVqaN3YrC6TjBauqhXhNA1TQoqloV7Da+pjZq1FsXUCamF29j6LvYhf3iISamZ3Fv9DZevouhRzzPfOG+3hpA9U9UyioOlTJ7pFeTCQS6RGzIebyf+oz5pSzWtmSW1EO9phvQ00slBRt/8qR3DoWdXbiczUiTzd52D+tdLmyTB14mx1rMAKVcRpEATjrsuElee/HXGmnFRyBOGD30C/nEDjNgs7CDpsYmnHG3YPegBCvHs9oYfm8nG9dJa5X4K8AAQzQX4KSN3wcAAAAASUVORK5CYII=\",\n  \"yml\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAdxJREFUeNqMUl1rE0EUPbM7m5Y0Zptu21AwWwhYpfSDFh+kvvRd8N0Hf4I/xWdf/Q158F0QoQ+CVsFKaLSQpt/dpmvztTOzzky6cetOpWcZZvbO3MO5514SxzEU3r57/3GpWllM/tP4sL3TarROXuSo/SWJvX71Uu80Cfhlr/T4UdWFAVfdnmsTUtvdP35OUyQKVnJgXDBTcj9icAsTeLax7j/052qM81UjwW1QJXEhMF0qYnN90fdnvdogYmvJPU0/VBApD4hcDrWRcyikfB17srzgW7b9Rh1vEvxDlI4tVytaBSEEtmWh0xsUMwpwnWjqAlcxogiHd1wiQyCu87iI/+sJtf6+NXsgpd7FWCMB50KvkYMGMbLdZgLlfj+K9K4+FnFQ2x7WntIs50AbmiGwLILt+k+EvzvSNIHzdigdJ/AmXQRhiHv5POSwYmG+cqPVo0HqDxj8uTK2vn1Hfa+JmdIkvtZ/4fOPXU3WPDpFeNWVyUKryCiIGMN4zsH98gym3CIcOTwT+XHdXrdQQHAZotE8kBPpSqPNHtBOr48HUmLOcXRJT9dWNMGYJFby91pHOAvaykSaITg+bwefdhrteDRTMSwyrFCgI88E056Hy+4Ah2cXQZL3R4ABALUe7fqXWFN6AAAAAElFTkSuQmCC\",\n  \"zip\": \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAm9JREFUeNpsk0tv00AUhc+MY6dOmgeFJg1FoVVpUWlFC0s2IFF1jxBbhKj4BSxYdscPYcEmQmIDq0gsERIViy4TpD7VFzF1Ho5je2a4thOqNhlp5Mz4zudzzp0wpRTC8fPrk0/TC6+fDtYicLH97T1Kc2vQDcs+rH3eUAxVznn0fn1DRM8E+iOdv5ct3XmZG6yVlNj6solUbgVTt0q5FGtX6vXqC6VklTE+KAO/OODHSIQPRQpsXC+kkEz2ELA0ystv84tLzyucsbWByisAGf+QAS2CCDRRLMJMmxC+i8C4jdLCm/zM7OOKFGptcO6/BTpJ0yeQB0Y+mfKQuZZG0jQgeRbW8Xdomobs9LN8scc+UPHNy4Dwq8IljotIIQEm59/RoSyM1CKkXKZNBm7kIVgyM6wgAnSgRK9vqQfHPiMFDHqyFVsLR9Cm0o4YzoAASrSjCelQfRPb1Vc4qn0EY5L2W9GEaBLcxQgFHpGbkMIDJ69e+wjJ8VXqRgKid0r7ftQdxkRs9SqA2kgAm14SSIQh9uhuLGPMnKJs/5KquL1x0N0RCsizigoDaLqBdHoMiyvrlBsHVx1wphD4BCewoqxGKKDwAgtOy8JufYuk+5golGGaGZwc1sIGoDz3AOPZSVLaHgVwydoJDM1H4DbQODughB3YpOD44HfoHgnu4e7So0uAi0stHLJ3Aud8B9bpHu6vPoSu9TtDl6tUuoFiIYOgu0+158MKmOxomtyD3Qi/3MTR7i8K0EDG1GHO5DE3X4DvNahZlJOwEkOATvdPc2//hx3mXJ5lFJaF8K8bStd0YGfnOJbMGex21x6c+yfAAOlIPDJzr7cLAAAAAElFTkSuQmCC\"\n}\n"
  },
  {
    "path": "lib/core/show-dir/index.js",
    "content": "'use strict';\n\nconst styles = require('./styles');\nconst lastModifiedToString = require('./last-modified-to-string');\nconst permsToString = require('./perms-to-string');\nconst sizeToString = require('./size-to-string');\nconst sortFiles = require('./sort-files');\nconst fs = require('fs');\nconst path = require('path');\nconst he = require('he');\nconst etag = require('../etag');\nconst url = require('url');\nconst status = require('../status-handlers');\n\nconst supportedIcons = styles.icons;\nconst css = styles.css;\n\nmodule.exports = (opts) => {\n  // opts are parsed by opts.js, defaults already applied\n  const cache = opts.cache;\n  const root = path.resolve(opts.root);\n  const baseDir = opts.baseDir;\n  const humanReadable = opts.humanReadable;\n  const hidePermissions = opts.hidePermissions;\n  const handleError = opts.handleError;\n  const showDotfiles = opts.showDotfiles;\n  const si = opts.si;\n  const weakEtags = opts.weakEtags;\n\n  return function middleware(req, res, next) {\n    // Figure out the path for the file from the given url\n    const parsed = url.parse(req.url);\n    const pathname = decodeURIComponent(parsed.pathname);\n    const dir = path.normalize(\n      path.join(\n        root,\n        path.relative(\n          path.join('/', baseDir),\n          pathname\n        )\n      )\n    );\n\n    fs.stat(dir, (statErr, stat) => {\n      if (statErr) {\n        if (handleError) {\n          status[500](res, next, { error: statErr });\n        } else {\n          next();\n        }\n        return;\n      }\n\n      // files are the listing of dir\n      fs.readdir(dir, (readErr, _files) => {\n        let files = _files;\n\n        if (readErr) {\n          if (handleError) {\n            status[500](res, next, { error: readErr });\n          } else {\n            next();\n          }\n          return;\n        }\n\n        // Optionally exclude dotfiles from directory listing.\n        if (!showDotfiles) {\n          files = files.filter(filename => filename.slice(0, 1) !== '.');\n        }\n\n        res.setHeader('content-type', 'text/html');\n        res.setHeader('etag', etag(stat, weakEtags));\n        res.setHeader('last-modified', (new Date(stat.mtime)).toUTCString());\n        res.setHeader('cache-control', cache);\n\n        // A step before render() is called to gives items additional\n        // information so that render() can deliver the best user experience\n        // possible.\n        function prerender(dirs, renderFiles, errs) {\n          const filenamesThatExist = new Set();\n\n          // Putting filenames in a set first keeps us in O(n) time complexity\n          for (let i=0; i < renderFiles.length; i++) {\n            const [name, stat] = renderFiles[i];\n            filenamesThatExist.add(name);\n            const renderOptions = {};\n            renderFiles[i] = [name, stat, renderOptions];\n          }\n\n          // Set render options for compressed files\n          for (const [name, _stat, renderOptions] of renderFiles) {\n            if (\n              opts.brotli &&\n              ! opts.forceContentEncoding &&\n              name.endsWith('.br')\n            ) {\n              const uncompressedName = name.slice(0, -'.br'.length);\n              if (filenamesThatExist.has(uncompressedName)) {\n                continue;\n              }\n              renderOptions.uncompressedName = uncompressedName;\n            }\n          }\n          for (const [name, _stat, renderOptions] of renderFiles) {\n            if (\n              opts.gzip &&\n              ! opts.forceContentEncoding &&\n              name.endsWith('.gz')\n            ) {\n              const uncompressedName = name.slice(0, -'.gz'.length);\n              if (filenamesThatExist.has(uncompressedName)) {\n                continue;\n              }\n              renderOptions.uncompressedName = uncompressedName;\n            }\n          }\n          render(dirs, renderFiles, errs);\n        }\n\n        function render(dirs, renderFiles, errs) {\n          // each entry in the array is a [name, stat] tuple\n\n          let html = `${[\n            '<!doctype html>',\n            '<html>',\n            '  <head>',\n            '    <meta charset=\"utf-8\">',\n            '    <meta name=\"viewport\" content=\"width=device-width\">',\n            `    <title>Index of ${he.encode(pathname)}</title>`,\n            `    <style type=\"text/css\">${css}</style>`,\n            '  </head>',\n            '  <body>',\n            `<h1>Index of ${he.encode(pathname)}</h1>`,\n          ].join('\\n')}\\n`;\n\n          html += '<table>';\n\n          const failed = false;\n          const writeRow = (file) => {\n            // render a row given a [name, stat, renderOptions] tuple\n            const isDir = file[1].isDirectory && file[1].isDirectory();\n            let href = `./${encodeURIComponent(file[0])}`;\n\n            // append trailing slash and query for dir entry\n            if (isDir) {\n              href += `/${he.encode((parsed.search) ? parsed.search : '')}`;\n            }\n\n            // Handle compressed files with uncompressed names\n            let displayNameHTML;\n            let fileSize = sizeToString(file[1], humanReadable, si);\n\n            if (file[2] && file[2].uncompressedName) {\n              // This is a compressed file, show both names with separate links\n              const uncompressedName = he.encode(file[2].uncompressedName);\n              const compressedName = he.encode(file[0]);\n              const uncompressedHref = `./${encodeURIComponent(file[2].uncompressedName)}`;\n              const asterisk = `<span title=\"served from compressed file\">*</span>`;\n              displayNameHTML = `<a href=\"${uncompressedHref}\">${uncompressedName}</a>` +\n                `${asterisk} (<a href=\"${href}\">${compressedName}</a>)`;\n              fileSize += '*';\n            } else {\n              // Regular file or directory\n              displayNameHTML = `<a href=\"${href}\">${he.encode(file[0]) + ((isDir) ? '/' : '')}</a>`;\n            }\n\n            const ext = file[0].split('.').pop();\n            const classForNonDir = supportedIcons[ext] ? ext : '_page';\n            const iconClass = `icon-${isDir ? '_blank' : classForNonDir}`;\n\n            // TODO: use stylessheets?\n            html += `${'<tr>' +\n              '<td><i class=\"icon '}${iconClass}\"></i></td>`;\n            if (!hidePermissions) {\n              html += `<td class=\"perms\"><code>(${permsToString(file[1])})</code></td>`;\n            }\n            html +=\n              `<td class=\"last-modified\">${lastModifiedToString(file[1])}</td>` +\n              `<td class=\"file-size\"><code>${fileSize}</code></td>` +\n              `<td class=\"display-name\">${displayNameHTML}</td>` +\n              '</tr>\\n';\n          };\n\n          dirs.sort((a, b) => a[0].toString().localeCompare(b[0].toString())).forEach(writeRow);\n          renderFiles.sort((a, b) => a.toString().localeCompare(b.toString())).forEach(writeRow);\n          errs.sort((a, b) => a[0].toString().localeCompare(b[0].toString())).forEach(writeRow);\n\n          html += '</table>\\n';\n          html += `<br><address>Node.js ${\n            process.version\n            }/ <a href=\"https://github.com/http-party/http-server\">http-server</a> ` +\n            `server running @ ${\n            he.encode(req.headers.host || '')}</address>\\n` +\n            '</body></html>'\n          ;\n\n          if (!failed) {\n            res.writeHead(200, { 'Content-Type': 'text/html' });\n            res.end(html);\n          }\n        }\n\n        sortFiles(dir, files, (errs, dirs, sortedFiles) => {\n          // It's possible to get stat errors for all sorts of reasons here.\n          // Unfortunately, our two choices are to either bail completely,\n          // or just truck along as though everything's cool. In this case,\n          // I decided to just tack them on as \"??!?\" items along with dirs\n          // and files.\n          //\n          // Whatever.\n\n          // if it makes sense to, add a .. link\n          if (path.resolve(dir, '..').slice(0, root.length) === root) {\n            fs.stat(path.join(dir, '..'), (err, s) => {\n              if (err) {\n                if (handleError) {\n                  status[500](res, next, { error: err });\n                } else {\n                  next();\n                }\n                return;\n              }\n              dirs.unshift(['..', s]);\n              prerender(dirs, sortedFiles, errs);\n            });\n          } else {\n            prerender(dirs, sortedFiles, errs);\n          }\n        });\n      });\n    });\n  };\n};\n"
  },
  {
    "path": "lib/core/show-dir/last-modified-to-string.js",
    "content": "'use strict';\n\nmodule.exports = function lastModifiedToString(stat) {\n  if (!stat.mtime) {\n    // stat error (eg, broken symlink)\n    return 'Unknown Date';\n  }\n  const t = new Date(stat.mtime);\n  return (('0' + (t.getDate())).slice(-2) + '-' +\n          t.toLocaleString('default', { month: 'short' }) + '-' +\n          t.getFullYear() + ' ' +\n          ('0' + t.getHours()).slice(-2) + ':' +\n          ('0' + t.getMinutes()).slice(-2));\n};\n"
  },
  {
    "path": "lib/core/show-dir/perms-to-string.js",
    "content": "'use strict';\n\nmodule.exports = function permsToString(stat) {\n  if (!stat.isDirectory || !stat.mode) {\n    return '????!!!???';\n  }\n\n  const dir = stat.isDirectory() ? 'd' : '-';\n  const mode = stat.mode.toString(8);\n\n  return dir + mode.slice(-3).split('').map(n => [\n    '---',\n    '--x',\n    '-w-',\n    '-wx',\n    'r--',\n    'r-x',\n    'rw-',\n    'rwx',\n  ][parseInt(n, 10)]).join('');\n};\n"
  },
  {
    "path": "lib/core/show-dir/size-to-string.js",
    "content": "'use strict';\n\n// given a file's stat, return the size of it in string\n// humanReadable: (boolean) whether to result is human readable\n// si: (boolean) whether to use si (1k = 1000), otherwise 1k = 1024\n// adopted from http://stackoverflow.com/a/14919494/665507\nmodule.exports = function sizeToString(stat, humanReadable, si) {\n  if (stat.isDirectory && stat.isDirectory()) {\n    return '';\n  }\n\n  let bytes = stat.size;\n  const threshold = si ? 1000 : 1024;\n\n  if (!humanReadable || bytes < threshold) {\n    return `${bytes}B`;\n  }\n\n  const units = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];\n  let u = -1;\n  do {\n    bytes /= threshold;\n    u += 1;\n  } while (bytes >= threshold);\n\n  let b = bytes.toFixed(1);\n  if (isNaN(b)) b = '??';\n\n  return b + units[u];\n};\n"
  },
  {
    "path": "lib/core/show-dir/sort-files.js",
    "content": "'use strict';\n\nconst fs = require('fs');\nconst path = require('path');\n\nmodule.exports = function sortByIsDirectory(dir, paths, cb) {\n  // take the listing file names in `dir`\n  // returns directory and file array, each entry is\n  // of the array a [name, stat] tuple\n  let pending = paths.length;\n  const errs = [];\n  const dirs = [];\n  const files = [];\n\n  if (!pending) {\n    cb(errs, dirs, files);\n    return;\n  }\n\n  paths.forEach((file) => {\n    fs.stat(path.join(dir, file), (err, s) => {\n      if (err) {\n        errs.push([file, err]);\n      } else if (s.isDirectory()) {\n        dirs.push([file, s]);\n      } else {\n        files.push([file, s]);\n      }\n\n      pending -= 1;\n      if (pending === 0) {\n        cb(errs, dirs, files);\n      }\n    });\n  });\n};\n"
  },
  {
    "path": "lib/core/show-dir/styles.js",
    "content": "'use strict';\n\nconst icons = require('./icons.json');\n\nconst IMG_SIZE = 16;\n\nlet css = `i.icon { display: block; height: ${IMG_SIZE}px; width: ${IMG_SIZE}px; background: no-repeat center; }\\n`;\ncss += 'table tr { white-space: nowrap; }\\n';\ncss += 'td.perms {}\\n';\ncss += 'td.file-size { text-align: right; padding-left: 1em; }\\n';\ncss += 'td.display-name { padding-left: 1em; }\\n';\ncss += `\n@media (prefers-color-scheme: dark) {\n  body {\n    background-color: #303030;\n    color: #efefef;\n  }\n  a {\n    color: #ffff11;\n  }\n}\n`;\n\nObject.keys(icons).forEach((key) => {\n  css += `i.icon-${key} {\\n`;\n  css += `  background-image: url(\"data:image/png;base64,${icons[key]}\");\\n`;\n  css += '}\\n\\n';\n});\n\nexports.icons = icons;\nexports.css = css;\n"
  },
  {
    "path": "lib/core/status-handlers.js",
    "content": "'use strict';\n\nconst he = require('he');\n\n// not modified\nexports['304'] = (res) => {\n  res.statusCode = 304;\n  res.end();\n};\n\n// access denied\nexports['403'] = (res, next) => {\n  res.statusCode = 403;\n  if (typeof next === 'function') {\n    next();\n  } else if (res.writable) {\n    res.setHeader('content-type', 'text/plain');\n    res.end('ACCESS DENIED');\n  }\n};\n\n// disallowed method\nexports['405'] = (res, next, opts) => {\n  res.statusCode = 405;\n  if (typeof next === 'function') {\n    next();\n  } else {\n    res.setHeader('allow', (opts && opts.allow) || 'GET, HEAD');\n    res.end();\n  }\n};\n\n// not found\nexports['404'] = (res, next) => {\n  res.statusCode = 404;\n  if (typeof next === 'function') {\n    next();\n  } else if (res.writable) {\n    res.setHeader('content-type', 'text/plain');\n    res.end('File not found. :(');\n  }\n};\n\nexports['416'] = (res, next, opts) => {\n  res.statusCode = 416;\n  res.setHeader('content-range', 'bytes */' + opts.size)\n  if (typeof next === 'function') {\n    next();\n  } else if (res.writable) {\n    res.setHeader('content-type', 'text/plain');\n    res.end('Requested range not satisfiable');\n  }\n};\n\n// flagrant error\nexports['500'] = (res, next, opts) => {\n  res.statusCode = 500;\n  try {\n    res.setHeader('content-type', 'text/html');\n  } catch (e) {\n    // errors may have triggered headers being sent already, make sure we don't hide the underlying error\n  }\n  const error = String(opts.error.stack || opts.error || 'No specified error');\n  const html = `${[\n    '<!doctype html>',\n    '<html>',\n    '  <head>',\n    '    <meta charset=\"utf-8\">',\n    '    <title>500 Internal Server Error</title>',\n    '  </head>',\n    '  <body>',\n    '    <p>',\n    `      ${he.encode(error)}`,\n    '    </p>',\n    '  </body>',\n    '</html>',\n  ].join('\\n')}\\n`;\n  res.end(html);\n};\n\n// bad request\nexports['400'] = (res, next, opts) => {\n  res.statusCode = 400;\n  res.setHeader('content-type', 'text/html');\n  const error = opts && opts.error ? String(opts.error) : 'Malformed request.';\n  const html = `${[\n    '<!doctype html>',\n    '<html>',\n    '  <head>',\n    '    <meta charset=\"utf-8\">',\n    '    <title>400 Bad Request</title>',\n    '  </head>',\n    '  <body>',\n    '    <p>',\n    `      ${he.encode(error)}`,\n    '    </p>',\n    '  </body>',\n    '</html>',\n  ].join('\\n')}\\n`;\n  res.end(html);\n};\n"
  },
  {
    "path": "lib/http-server.js",
    "content": "'use strict';\n\nvar fs = require('fs'),\n  union = require('union'),\n  httpServerCore = require('./core'),\n  auth = require('basic-auth'),\n  httpProxy = require('http-proxy'),\n  corser = require('corser'),\n  secureCompare = require('secure-compare');\nvar { minimatch } = require('minimatch');\n\n//\n// Remark: backwards compatibility for previous\n// case convention of HTTP\n//\nexports.HttpServer = exports.HTTPServer = HttpServer;\n\n/**\n * Returns a new instance of HttpServer with the\n * specified `options`.\n */\nexports.createServer = function (options) {\n  return new HttpServer(options);\n};\n\n/**\n * Constructor function for the HttpServer object\n * which is responsible for serving static files along\n * with other HTTP-related features.\n */\nfunction HttpServer(options) {\n  options = options || {};\n  var proxyAll = options.proxyAll === true || options.proxyAll === 'true';\n\n  if (proxyAll && typeof options.proxy !== 'string') {\n    throw new Error('proxyAll option requires \"proxy\" to be configured');\n  }\n\n  if (options.root) {\n    this.root = options.root;\n  } else {\n    try {\n      // eslint-disable-next-line no-sync\n      fs.lstatSync('./public');\n      this.root = './public';\n    } catch (err) {\n      this.root = './';\n    }\n  }\n\n  // CRLF injection prevention\n  for ( const [key, value] of Object.entries(options.headers || {}) ) {\n    if (typeof key !== 'string' || typeof value !== 'string') {\n      throw new Error('Header is not a string or contains CRLF');\n    }\n    if (key.includes('\\r') || key.includes('\\n') || value.includes('\\r') || value.includes('\\n')) {\n      throw new Error('Header is not a string or contains CRLF');\n    }\n  }\n\n  this.headers = options.headers || {};\n  this.headers['Accept-Ranges'] = 'bytes';\n\n  this.cache = (\n    // eslint-disable-next-line no-nested-ternary\n    options.cache === undefined ? 3600 :\n    // -1 is a special case to turn off caching.\n    // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Preventing_caching\n      options.cache === -1 ? 'no-cache, no-store, must-revalidate' :\n        options.cache // in seconds.\n  );\n  this.showDir = options.showDir !== 'false';\n  this.dirOverrides404 = options.dirOverrides404;\n  this.autoIndex = options.autoIndex !== 'false';\n  this.showDotfiles = options.showDotfiles;\n  this.hidePermissions = options.hidePermissions;\n  this.gzip = options.gzip === true;\n  this.brotli = options.brotli === true;\n  this.forceContentEncoding = options.forceContentEncoding === true;\n  if (options.ext) {\n    this.ext = options.ext === true\n      ? 'html'\n      : options.ext;\n  }\n  this.contentType = options.contentType ||\n    (this.ext === 'html' ? 'text/html' : 'application/octet-stream');\n\n  var before = options.before ? options.before.slice() : [];\n\n  if (options.logFn) {\n    before.push(function (req, res) {\n      options.logFn(req, res);\n      res.emit('next');\n    });\n  }\n\n  if (options.username || options.password) {\n    if (!options.username || !options.password) {\n      throw new Error('Basic authentication requires both username and password to be specified');\n    }\n\n    before.push(function (req, res) {\n      var credentials = auth(req);\n\n      // We perform these outside the if to avoid short-circuiting and giving\n      // an attacker knowledge of whether the username is correct via a timing\n      // attack.\n      if (credentials) {\n        // if credentials is defined, name and pass are guaranteed to be string\n        // type\n        var usernameEqual = secureCompare(options.username.toString(), credentials.name);\n        var passwordEqual = secureCompare(options.password.toString(), credentials.pass);\n        if (usernameEqual && passwordEqual) {\n          return res.emit('next');\n        }\n      }\n\n      res.statusCode = 401;\n      res.setHeader('WWW-Authenticate', 'Basic realm=\"\"');\n      res.end('Access denied');\n    });\n  }\n\n  if (options.allowedHosts) {\n    before.push(function (req, res) {\n      let host = req.headers && req.headers.host;\n      if (host) {\n        // don't include port number in host check\n        host = host.split(':')[0];\n      }\n\n      if (!host || !options.allowedHosts.includes(host)) {\n        res.statusCode = 403;\n        res.end('Access denied');\n        return;\n      }\n\n      return res.emit('next');\n    });\n  }\n\n  if (options.coop) {\n    this.headers['Cross-Origin-Opener-Policy'] = options.coopHeader || 'same-origin';\n    this.headers['Cross-Origin-Embedder-Policy'] = 'require-corp';\n  }\n\n  // CORS configuration:\n  // --cors enables CORS by setting Access-Control-Allow-Origin to '*'\n  // --cors=header1,header2 also adds custom headers to Access-Control-Allow-Headers\n  if (options.cors) {\n    this.headers['Access-Control-Allow-Origin'] = '*';\n    this.headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, Range';\n    if (options.corsHeaders) {\n      options.corsHeaders.split(/\\s*,\\s*/)\n        .forEach(function (h) { this.headers['Access-Control-Allow-Headers'] += ', ' + h; }, this);\n    }\n    before.push(corser.create(options.corsHeaders ? {\n      requestHeaders: this.headers['Access-Control-Allow-Headers'].split(/\\s*,\\s*/)\n    } : null));\n  }\n\n  if (options.privateNetworkAccess) {\n    this.headers['Access-Control-Allow-Private-Network'] = true;\n  }\n\n  if (options.robots) {\n    before.push(function (req, res) {\n      if (req.url === '/robots.txt') {\n        res.setHeader('Content-Type', 'text/plain');\n        var robots = options.robots === true\n          ? 'User-agent: *\\nDisallow: /'\n          : options.robots.replace(/\\\\n/, '\\n');\n\n        return res.end(robots);\n      }\n\n      res.emit('next');\n    });\n  }\n\n  if (typeof options.proxyConfig === 'object') {\n    var proxy = httpProxy.createProxyServer();\n    before.push(function (req, res, next) {\n      for (var key of Object.keys(options.proxyConfig)) {\n        if (!minimatch(req.url, key)) continue;\n        req.proxy ??= {};\n        var matchConfig = options.proxyConfig[key];\n        \n        if (matchConfig.pathRewrite) {\n          Object.entries(matchConfig.pathRewrite).forEach(rewrite => {\n            req.url = req.url.replace(new RegExp(rewrite[0]), rewrite[1]);\n          });\n        }\n        \n        var configEntries = Object.entries(matchConfig).filter(entry => entry[0] !== \"pathRewrite\");\n        configEntries.forEach(entry => req.proxy[entry[0]] = entry[1]);\n        break;\n      }\n\n      if (req.proxy) {\n        if (options.logFn) {\n          options.logFn(req, res);\n        }\n        proxy.web(req, res, req.proxy, function (err, req, res) {\n          if (options.logFn) {\n            options.logFn(req, res, {\n              message: err.message,\n              status: res.statusCode });\n          }\n          res.emit('next');\n        });\n      } else {\n        next();\n      }\n    });\n  }\n\n  if (!proxyAll) {\n    before.push(httpServerCore({\n      root: this.root,\n      baseDir: options.baseDir,\n      cache: this.cache,\n      showDir: this.showDir,\n      showDotfiles: this.showDotfiles,\n      hidePermissions: this.hidePermissions,\n      autoIndex: this.autoIndex,\n      defaultExt: this.ext,\n      dirOverrides404: this.dirOverrides404,\n      gzip: this.gzip,\n      brotli: this.brotli,\n      forceContentEncoding: this.forceContentEncoding,\n      contentType: this.contentType,\n      mimetypes: options.mimetypes,\n      handleError: typeof options.proxy !== 'string'\n    }));\n  }\n\n  if (typeof options.proxy === 'string') {\n    var proxyOptions = options.proxyOptions || {};\n\n    if (proxyOptions.changeOrigin == null) {\n        proxyOptions.changeOrigin = true;\n    }\n\n    var proxy = httpProxy.createProxyServer({\n      ...proxyOptions,\n      target: options.proxy,\n    });\n    before.push(function (req, res) {\n      proxy.web(req, res, {}, function (err, req, res) {\n        if (options.logFn) {\n          options.logFn(req, res, {\n            message: err.message,\n            status: res.statusCode });\n        }\n        res.emit('next');\n      });\n    });\n  }\n\n  var serverOptions = {\n    before: before,\n    headers: this.headers,\n    onError: function (err, req, res) {\n      if (options.logFn) {\n        options.logFn(req, res, err);\n      }\n\n      res.end();\n    }\n  };\n\n  if (options.https) {\n    serverOptions.https = options.https;\n  }\n\n  this.server = serverOptions.https && serverOptions.https.passphrase\n    // if passphrase is set, shim must be used as union does not support\n    ? require('./shims/https-server-shim')(serverOptions)\n    : union.createServer(serverOptions);\n\n  if (isNaN(options.timeout) || isNaN(parseFloat(options.timeout))) {\n    this.server.setTimeout(120);\n  } else {\n    // set custom timeout only if options.timeout is a numeric string\n    this.server.setTimeout(Math.max(0, Number(options.timeout)));\n  }\n\n  if (typeof options.proxy === 'string' && options.websocket) {\n    this.server.on('upgrade', function (request, socket, head) {\n      proxy.ws(request, socket, head, {\n        target: options.proxy,\n        changeOrigin: true\n      }, function (err, req, res) {\n        if (options.logFn) {\n          options.logFn(req, res, {\n            message: err?.message,\n            status: res?.statusCode });\n        }\n        res.emit('next');\n      });\n    });\n  }\n}\n\nHttpServer.prototype.listen = function () {\n  this.server.listen.apply(this.server, arguments);\n};\n\nHttpServer.prototype.close = function () {\n  return this.server.close();\n};\n\nHttpServer.prototype.address = function () {\n  return this.server.address();\n};\n"
  },
  {
    "path": "lib/shims/https-server-shim.js",
    "content": "/* eslint-disable no-process-env */\n/* eslint-disable no-sync */\nvar https = require('https');\nvar fs = require('fs');\nvar core = require('union/lib/core');\nvar RoutingStream = require('union/lib/routing-stream');\n\nmodule.exports = function (options) {\n  var isArray = Array.isArray(options.after);\n  var credentials;\n\n  if (!options) {\n    throw new Error('options is required to create a server');\n  }\n\n  function requestHandler(req, res) {\n    var routingStream = new RoutingStream({\n      before: options.before,\n      buffer: options.buffer,\n      after:\n        isArray &&\n        options.after.map(function (After) {\n          return new After();\n        }),\n      request: req,\n      response: res,\n      limit: options.limit,\n      headers: options.headers\n    });\n\n    routingStream.on('error', function (err) {\n      var fn = options.onError || core.errorHandler;\n      fn(err, routingStream, routingStream.target, function () {\n        routingStream.target.emit('next');\n      });\n    });\n\n    req.pipe(routingStream);\n  }\n\n  var serverOptions;\n\n  serverOptions = options.https;\n  if (!serverOptions.key || !serverOptions.cert) {\n    throw new Error(\n      'Both options key and cert are required.'\n    );\n  }\n\n  credentials = {\n    key: fs.readFileSync(serverOptions.key),\n    cert: fs.readFileSync(serverOptions.cert),\n    passphrase: process.env.NODE_HTTP_SERVER_SSL_PASSPHRASE\n  };\n\n  if (serverOptions.ca) {\n    serverOptions.ca = !Array.isArray(serverOptions.ca)\n      ? [serverOptions.ca]\n      : serverOptions.ca;\n\n    credentials.ca = serverOptions.ca.map(function (ca) {\n      return fs.readFileSync(ca);\n    });\n  }\n\n  return https.createServer(credentials, requestHandler);\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"http-server\",\n  \"version\": \"14.1.2\",\n  \"description\": \"A simple zero-configuration command-line http server\",\n  \"main\": \"./lib/http-server\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git://github.com/http-party/http-server.git\"\n  },\n  \"keywords\": [\n    \"cli\",\n    \"command\",\n    \"static\",\n    \"http\",\n    \"https\",\n    \"http-server\",\n    \"https-server\",\n    \"server\"\n  ],\n  \"scripts\": {\n    \"start\": \"node ./bin/http-server\",\n    \"test\": \"tap --reporter=terse --allow-incomplete-coverage test/*.test.js\",\n    \"test-watch\": \"tap --reporter=terse --allow-incomplete-coverage --watch test/*.test.js\"\n  },\n  \"files\": [\n    \"lib\",\n    \"bin\",\n    \"doc\"\n  ],\n  \"man\": \"./doc/http-server.1\",\n  \"engines\": {\n    \"node\": \">=16.20.2\"\n  },\n  \"contributors\": [\n    {\n      \"name\": \"Charlie Robbins\",\n      \"email\": \"charlie.robbins@gmail.com\"\n    },\n    {\n      \"name\": \"Marak Squires\",\n      \"email\": \"marak.squires@gmail.com\"\n    },\n    {\n      \"name\": \"Charlie McConnell\",\n      \"email\": \"charlie@charlieistheman.com\"\n    },\n    {\n      \"name\": \"Joshua Holbrook\",\n      \"email\": \"josh.holbrook@gmail.com\"\n    },\n    {\n      \"name\": \"Maciej Małecki\",\n      \"email\": \"maciej.malecki@notimplemented.org\"\n    },\n    {\n      \"name\": \"Matthew Bergman\",\n      \"email\": \"mzbphoto@gmail.com\"\n    },\n    {\n      \"name\": \"brad dunbar\",\n      \"email\": \"dunbarb2@gmail.com\"\n    },\n    {\n      \"name\": \"Dominic Tarr\"\n    },\n    {\n      \"name\": \"Travis Person\",\n      \"email\": \"travis.person@gmail.com\"\n    },\n    {\n      \"name\": \"Jinkwon Lee\",\n      \"email\": \"master@bdyne.net\"\n    },\n    {\n      \"name\": \"BigBlueHat\",\n      \"email\": \"byoung@bigbluehat.com\"\n    },\n    {\n      \"name\": \"Daniel Dalton\",\n      \"email\": \"daltond2@hawkmail.newpaltz.edu\"\n    },\n    {\n      \"name\": \"Jade Michael Thornton\",\n      \"email\": \"jademichael@jmthornton.net\"\n    },\n    {\n      \"name\": \"Jorens Merenjanu\",\n      \"email\": \"jorensmerenjanu@gmail.com\"\n    }\n  ],\n  \"dependencies\": {\n    \"basic-auth\": \"^2.0.1\",\n    \"chalk\": \"^4.1.2\",\n    \"corser\": \"^2.0.1\",\n    \"he\": \"^1.2.0\",\n    \"html-encoding-sniffer\": \"^3.0.0\",\n    \"http-proxy\": \"^1.18.1\",\n    \"mime\": \"^1.6.0\",\n    \"minimatch\": \"^10.1.1\",\n    \"minimist\": \"^1.2.6\",\n    \"opener\": \"^1.5.1\",\n    \"portfinder\": \"^1.0.28\",\n    \"secure-compare\": \"3.0.1\",\n    \"union\": \"~0.5.0\",\n    \"url-join\": \"^4.0.1\"\n  },\n  \"devDependencies\": {\n    \"eol\": \"^0.9.1\",\n    \"eslint\": \"^4.19.1\",\n    \"eslint-config-populist\": \"^4.2.0\",\n    \"express\": \"^4.17.1\",\n    \"request\": \"^2.88.2\",\n    \"tap\": \"^21.0.1\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/http-party/http-server/issues\"\n  },\n  \"license\": \"MIT\",\n  \"preferGlobal\": true,\n  \"bin\": {\n    \"http-server\": \"./bin/http-server\"\n  }\n}\n"
  },
  {
    "path": "public/404.html",
    "content": "<html>\n  <head>\n    <title>404</title>\n  </head>\n  <body>\n    <h1>404</h1>\n\n    <p>Were you just making up filenames or what?</p>\n  </body>\n</html>\n"
  },
  {
    "path": "public/index.html",
    "content": "<html>\n  <head>\n    <title>node.js http server</title>\n  </head>\n  <body>\n\n    <h1>Serving up static files like they were turtles strapped to rockets.</h1>\n\n    <img src=\"./img/turtle.png\"/>\n  </body>\n</html>\n\n"
  },
  {
    "path": "test/304.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst ecstatic = require('../lib/core');\nconst http = require('http');\nconst request = require('request');\nconst path = require('path');\nconst portfinder = require('portfinder');\n\nconst root = `${__dirname}/public`;\nconst baseDir = 'base';\n\ntest('304_not_modified_strong', (t) => {\n  const file = 'a.txt';\n\n  const server = http.createServer(\n    ecstatic({\n      root,\n      gzip: true,\n      baseDir,\n      autoIndex: true,\n      showDir: true,\n      weakEtags: false,\n      weakCompare: false,\n    })\n  );\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    const uri = `http://localhost:${port}${path.join('/', baseDir, file)}`;\n\n    request.get({\n      uri,\n      followRedirect: false,\n    }, (err, res) => {\n      if (err) {\n        t.fail(err);\n      }\n\n      t.equal(res.statusCode, 200, 'first request should be a 200');\n\n      request.get({\n        uri,\n        followRedirect: false,\n        headers: { 'if-modified-since': res.headers['last-modified'] },\n      }, (err2, res2) => {\n        if (err2) {\n          t.fail(err2);\n        }\n\n        t.equal(res2.statusCode, 304, 'second request should be a 304');\n        t.equal(res2.headers.etag.indexOf('\"'), 0, 'should return a strong etag');\n        server.close();\n        setTimeout(() => { t.end(); }, 0);\n      });\n    });\n  });\n});\n\ntest('304_not_modified_weak', (t) => {\n  const file = 'b.txt';\n\n  const server = http.createServer(\n    ecstatic({\n      root,\n      gzip: true,\n      baseDir,\n      autoIndex: true,\n      showDir: true,\n      weakEtags: true,\n      weakCompare: false,\n    })\n  );\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    const uri = `http://localhost:${port}${path.join('/', baseDir, file)}`;\n    const now = (new Date()).toString();\n\n    request.get({\n      uri,\n      followRedirect: false,\n    }, (err, res) => {\n      if (err) {\n        t.fail(err);\n      }\n\n      t.equal(res.statusCode, 200, 'first request should be a 200');\n\n      request.get({\n        uri,\n        followRedirect: false,\n        headers: { 'if-modified-since': now },\n      }, (err2, res2) => {\n        if (err2) t.fail(err2);\n\n        t.equal(res2.statusCode, 304, 'second request should be a 304');\n        t.equal(res2.headers.etag.indexOf('W/'), 0, 'should return a weak etag');\n        server.close();\n        setTimeout(() => { t.end(); }, 0);\n      });\n    });\n  });\n});\n\ntest('304_not_modified_strong_compare', (t) => {\n  const file = 'b.txt';\n\n  const server = http.createServer(\n    ecstatic({\n      root,\n      gzip: true,\n      baseDir,\n      autoIndex: true,\n      showDir: true,\n      weakEtags: false,\n      weakCompare: false,\n    })\n  );\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    const uri = `http://localhost:${port}${path.join('/', baseDir, file)}`;\n    const now = (new Date()).toString();\n    let etag = null;\n\n    request.get({\n      uri,\n      followRedirect: false,\n    }, (err, res) => {\n      if (err) {\n        t.fail(err);\n      }\n\n      t.equal(res.statusCode, 200, 'first request should be a 200');\n\n      etag = res.headers.etag;\n\n      request.get({\n        uri,\n        followRedirect: false,\n        headers: { 'if-modified-since': now, 'if-none-match': etag },\n      }, (err2, res2) => {\n        if (err2) {\n          t.fail(err2);\n        }\n\n        t.equal(res2.statusCode, 304, 'second request with a strong etag should be 304');\n\n        request.get({\n          uri,\n          followRedirect: false,\n          headers: { 'if-modified-since': now, 'if-none-match': `W/${etag}` },\n        }, (err3, res3) => {\n          if (err3) {\n            t.fail(err3);\n          }\n\n          // Note that if both if-modified-since and if-none-match are\n          // provided, the server MUST NOT return a response status of 304\n          // unless doing so is consistent with all of the conditional\n          // header fields in the request\n          // https://www.ietf.org/rfc/rfc2616.txt\n          t.equal(res3.statusCode, 200, 'third request with a weak etag should be 200');\n          server.close();\n          setTimeout(() => { t.end(); }, 0);\n        });\n      });\n    });\n  });\n});\n\n\ntest('304_not_modified_weak_compare', (t) => {\n  const file = 'c.js';\n\n  const server = http.createServer(\n    ecstatic({\n      root,\n      gzip: true,\n      baseDir,\n      autoIndex: true,\n      showDir: true,\n      weakEtags: false,\n    })\n  );\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    const uri = `http://localhost:${port}${path.join('/', baseDir, file)}`;\n    const now = (new Date()).toString();\n    let etag = null;\n\n    request.get({\n      uri,\n      followRedirect: false,\n    }, (err, res) => {\n      if (err) {\n        t.fail(err);\n      }\n\n      t.equal(res.statusCode, 200, 'first request should be a 200');\n\n      etag = res.headers.etag;\n\n      request.get({\n        uri,\n        followRedirect: false,\n        headers: { 'if-modified-since': now, 'if-none-match': etag },\n      }, (err2, res2) => {\n        if (err2) {\n          t.fail(err2);\n        }\n\n        t.equal(res2.statusCode, 304, 'second request with a strong etag should be 304');\n\n        request.get({\n          uri,\n          followRedirect: false,\n          headers: { 'if-modified-since': now, 'if-none-match': `W/${etag}` },\n        }, (err3, res3) => {\n          if (err3) {\n            t.fail(err3);\n          }\n\n          t.equal(res3.statusCode, 304, 'third request with a weak etag should be 304');\n          server.close();\n          setTimeout(() => { t.end(); }, 0);\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/accept-encoding.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst ecstatic = require('../lib/core');\nconst http = require('http');\nconst request = require('request');\n\nconst root = `${__dirname}/public`;\n\ntest('properly handles whitespace in accept-encoding', (t) => {\n  t.plan(3);\n\n  const server = http.createServer(ecstatic({\n    root,\n    autoIndex: true,\n    gzip: true\n  }));\n\n  server.listen(() => {\n    const port = server.address().port;\n    const options = {\n      uri: `http://localhost:${port}/gzip`,\n      headers: {\n        'accept-encoding': ' gzip, deflate'\n      }\n    };\n\n    request.get(options, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200);\n      t.equal(res.headers['content-encoding'], 'gzip');\n    });\n  });\n  t.once('end', () => {\n    server.close();\n  });\n});\n\ntest('properly handles single accept-encoding entry', (t) => {\n  t.plan(3);\n\n  const server = http.createServer(ecstatic({\n    root,\n    autoIndex: true,\n    gzip: true\n  }));\n\n  server.listen(() => {\n    const port = server.address().port;\n    const options = {\n      uri: `http://localhost:${port}/gzip`,\n      headers: {\n        'accept-encoding': 'gzip'\n      }\n    };\n\n    request.get(options, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200);\n      t.equal(res.headers['content-encoding'], 'gzip');\n    });\n  });\n  t.once('end', () => {\n    server.close();\n  });\n});\n"
  },
  {
    "path": "test/allowed-hosts.test.js",
    "content": "const test = require('tap').test\nconst path = require('path')\nconst httpServer = require('../lib/http-server')\nconst request = require('request');\n\n// Prevent errors from being swallowed\nprocess.on('uncaughtException', console.error)\n\ntest('allowed hosts functionality', (t) => {\n    t.plan(4);\n    new Promise((resolve) => {\n        const server = httpServer.createServer({\n            root: path.join(__dirname, 'fixtures'),\n            allowedHosts: ['localhost'],\n        })\n\n        server.listen(0, async () => {\n            console.log('server listening on port', server.address().port)\n            const port = server.address().port\n            const url = `http://localhost:${port}`\n\n            try {\n                await new Promise((resolve, reject) => {\n                    request.get({ \n                        url,\n                        headers: {\n                            Host: 'example.com'\n                        }\n                    }, (err, res) => {\n                        console.log('response', err)\n                        t.error(err);\n                        t.equal(res.statusCode, 403);\n                        resolve();\n                    })\n                })\n\n                await new Promise((resolve, reject) => {\n                    request.get({ \n                        url,\n                        headers: {\n                            Host: 'localhost'\n                        }\n                    }, (err, res) => {\n                        console.log('response', err)\n                        t.error(err);\n                        t.equal(res.statusCode, 200);\n                        resolve();\n                    })\n                })\n            } catch (err) {\n                t.fail(`allowed hosts test failed: ${err.message}`)\n            } finally {\n                server.close()\n            }\n        })\n    })\n})"
  },
  {
    "path": "test/cache.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst http = require('http');\nconst request = require('request');\nconst ecstatic = require('../lib/core');\n\ntest('custom cache option number', (t) => {\n  let server = null;\n  try {\n    server = http.createServer(ecstatic({\n      root: `${__dirname}/public/`,\n      cache: 3600,\n    }));\n  } catch (e) {\n    t.fail(e.message);\n    t.end();\n  }\n\n  t.plan(3);\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    request.get(`http://localhost:${port}/a.txt`, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200, 'a.txt should be found');\n      t.equal(res.headers['cache-control'], 'max-age=3600');\n      server.close(() => { t.end(); });\n    });\n  });\n});\n\ntest('custom cache option string', (t) => {\n  let server = null;\n  try {\n    server = http.createServer(ecstatic({\n      root: `${__dirname}/public/`,\n      cache: 'max-whatever=3600',\n    }));\n  } catch (e) {\n    t.fail(e.message);\n    t.end();\n  }\n\n  t.plan(3);\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    request.get(`http://localhost:${port}/a.txt`, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200, 'a.txt should be found');\n      t.equal(res.headers['cache-control'], 'max-whatever=3600');\n      server.close(() => { t.end(); });\n    });\n  });\n});\n\ntest('custom cache option function returning a number', (t) => {\n  let i = 0;\n  let server = null;\n  try {\n    server = http.createServer(ecstatic({\n      root: `${__dirname}/public/`,\n      cache() {\n        i += 1;\n        return i;\n      },\n    }));\n  } catch (e) {\n    t.fail(e.message);\n    t.end();\n  }\n\n  t.plan(6);\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    request.get(`http://localhost:${port}/a.txt`, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200, 'a.txt should be found');\n      t.equal(res.headers['cache-control'], 'max-age=1');\n\n      request.get(`http://localhost:${port}/a.txt`, (err2, res2) => {\n        t.error(err2);\n        t.equal(res2.statusCode, 200, 'a.txt should be found');\n        t.equal(res2.headers['cache-control'], 'max-age=2');\n        server.close(() => { t.end(); });\n      });\n    });\n  });\n});\n\ntest('custom cache option function returning a string', (t) => {\n  let i = 0;\n  let server = null;\n  try {\n    server = http.createServer(ecstatic({\n      root: `${__dirname}/public/`,\n      cache() {\n        i += 1;\n        return `max-meh=${i}`;\n      },\n    }));\n  } catch (e) {\n    t.fail(e.message);\n    t.end();\n  }\n\n  t.plan(6);\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    request.get(`http://localhost:${port}/a.txt`, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200, 'a.txt should be found');\n      t.equal(res.headers['cache-control'], 'max-meh=1');\n\n      request.get(`http://localhost:${port}/a.txt`, (err2, res2) => {\n        t.error(err2);\n        t.equal(res2.statusCode, 200, 'a.txt should be found');\n        t.equal(res2.headers['cache-control'], 'max-meh=2');\n        server.close(() => { t.end(); });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/check-headers.js",
    "content": "const request = require('request');\n\nmodule.exports = (t, server, path, check) => {\n  server.listen(() => {\n    const port = server.address().port;\n    const uri = `http://localhost:${port}/${path}`;\n\n    request.get({ uri }, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200);\n      check(t, res.headers);\n    });\n  });\n  t.once('end', () => {\n    server.close();\n  });\n}\n"
  },
  {
    "path": "test/cli.test.js",
    "content": "'use strict';\n\n/* this test suit is incomplete  2015-12-18 */\n\nconst test = require('tap').test;\nconst request = require('request');\nconst spawn = require('child_process').spawn;\nconst path = require('path');\nconst portfinder = require('portfinder');\nconst httpServer = require('../lib/http-server');\n\nconst node = process.execPath;\n\nfunction startServer(args) {\n  return spawn(node, [require.resolve('../bin/http-server')].concat(args));\n}\n\nfunction checkServerIsRunning(url, msg, t, _cb) {\n  if (!msg.toString().match(/Starting up/)) {\n    return;\n  }\n  t.pass('http-server started');\n  const cb = _cb || (() => {});\n\n  request(url, (err, res) => {\n    if (!err && res.statusCode !== 500) {\n      t.pass('a successful request from the server was made');\n      cb(null, res);\n    } else {\n      t.fail(`the server could not be reached @ ${url}`);\n      cb(err);\n    }\n  });\n}\n\nfunction tearDown(ps, t) {\n  t.teardown(() => {\n    ps.kill('SIGTERM');\n  });\n}\n\nconst getPort = () => new Promise((resolve, reject) => {\n  portfinder.getPort((err, port) => {\n    if (err) reject(err);\n    resolve(port);\n  });\n});\n\nconst stripAnsi = (str) => str.replace(/\\u001b\\[[0-9;]*m/g, '');\n\ntest('setting port via cli - custom port', (t) => {\n  t.plan(2);\n\n  getPort().then((port) => {\n    const options = ['.', '--port', port];\n    const server = startServer(options);\n\n    tearDown(server, t);\n\n    server.stdout.on('data', (msg) => {\n      checkServerIsRunning(`http://localhost:${port}`, msg, t);\n    });\n  });\n});\n\ntest('setting mimeTypes via cli - .types file', (t) => {\n  t.plan(4);\n\n  getPort().then((port) => {\n    const root = path.resolve(__dirname, 'public/');\n    const pathMimetypeFile = path.resolve(__dirname, 'fixtures/custom_mime_type.types');\n    const options = [root, '--port', port, '--mimetypes', pathMimetypeFile];\n    const server = startServer(options);\n\n    tearDown(server, t);\n\n    server.stdout.on('data', (msg) => {\n      checkServerIsRunning(`http://localhost:${port}/custom_mime_type.opml`, msg, t, (err, res) => {\n        t.error(err);\n        t.equal(res.headers['content-type'], 'application/secret');\n      });\n    });\n  });\n});\n\ntest('setting mimeTypes via cli - directly', (t) => {\n  t.plan(4);\n\n  getPort().then((port) => {\n    const root = path.resolve(__dirname, 'public/');\n    const mimeType = ['--mimetypes', '{ \"application/x-my-type\": [\"opml\"] }'];\n    const options = [root, '--port', port].concat(mimeType);\n    const server = startServer(options);\n\n    // TODO: remove error handler\n    tearDown(server, t);\n\n    server.stdout.on('data', (msg) => {\n      checkServerIsRunning(`http://localhost:${port}/custom_mime_type.opml`, msg, t, (err, res) => {\n        t.error(err);\n        t.equal(res.headers['content-type'], 'application/x-my-type');\n      });\n    });\n  });\n});\n\ntest('--proxy requires you to specify a protocol', (t) => {\n  t.plan(1);\n  \n  const options = ['.', '--proxy', 'google.com'];\n  const server = startServer(options);\n\n  tearDown(server, t);\n\n  server.on('exit', (code) => {\n    t.equal(code, 1);\n  });\n});\n\ntest('--proxy-all requires --proxy', (t) => {\n  t.plan(1);\n\n  const options = ['.', '--proxy-all', 'true'];\n  const server = startServer(options);\n\n  tearDown(server, t);\n\n  server.on('exit', (code) => {\n    t.equal(code, 1);\n  });\n});\n\ntest('--proxy-all does not consume following positional args', (t) => {\n  t.plan(4);\n\n  const root = path.resolve(__dirname, 'fixtures', 'root');\n  const targetServer = httpServer.createServer({ root });\n\n  targetServer.listen(0, () => {\n    const targetPort = targetServer.address().port;\n    getPort().then((port) => {\n      const options = [\n        '--proxy', `http://localhost:${targetPort}`,\n        '--proxy-all',\n        root,\n        '--port', port\n      ];\n      const server = startServer(options);\n\n      tearDown(server, t);\n      t.teardown(() => targetServer.close());\n\n      let sawRootLog = false;\n\n      server.stdout.on('data', (msg) => {\n        const text = stripAnsi(msg.toString());\n        if (text.includes(root)) {\n          sawRootLog = true;\n        }\n        checkServerIsRunning(`http://localhost:${port}`, msg, t, (err, res) => {\n          if (err) {\n            t.fail(err.toString());\n            return;\n          }\n\n          t.ok(sawRootLog, 'root path should remain positional argument');\n          t.equal(res.statusCode, 200, 'proxied request should succeed');\n        });\n      });\n    });\n  });\n});\n\nfunction doHeaderOptionTest(t, argv, obj) {\n  getPort().then((port) => {\n    const options = ['.', '--port', port].concat(argv);\n    const server = startServer(options);\n\n    tearDown(server, t);\n\n    server.stdout.on('data', (msg) => {\n      checkServerIsRunning(`http://localhost:${port}`, msg, t, (err, res) => {\n        t.error(err);\n\n        for (const [k, v] of Object.entries(obj)) {\n          t.equal(res.headers[k], v, 'expected header value matches in response');\n        }\n      });\n    });\n  });\n}\n\ntest('single --header option is applied', (t) => {\n  t.plan(4);\n\n  doHeaderOptionTest(t,\n    ['--header=X-http-server-test-A: hello'],\n    { 'x-http-server-test-a': 'hello' }\n  );\n});\n\ntest('single -H option is applied', (t) => {\n  t.plan(4);\n\n  doHeaderOptionTest(t,\n    ['-H', 'X-http-server-test-A: hello'],\n    { 'x-http-server-test-a': 'hello' }\n  );\n});\n\ntest('mix of multiple --header and -H options are applied', (t) => {\n  t.plan(7);\n\n  doHeaderOptionTest(t,\n    [\n      '--header=X-http-server-test-A: Lorem ipsum dolor sit amet',\n      '-H', 'X-http-server-test-B: consectetur=adipiscing; elit',\n      '-H', 'X-http-server-test-C: c',\n      '--header=X-http-server-test-D: d'\n    ],\n    {\n      'x-http-server-test-a': 'Lorem ipsum dolor sit amet',\n      'x-http-server-test-b': 'consectetur=adipiscing; elit',\n      'x-http-server-test-c': 'c',\n      'x-http-server-test-d': 'd'\n    }\n  );\n});\n\ntest('empty header value is allowed (RFC 7230)', (t) => {\n  t.plan(5);\n\n  doHeaderOptionTest(t,\n    ['-H', 'X-http-server-test-empty-a:', '-H', 'X-http-server-test-empty-b'],\n    { 'x-http-server-test-empty-a': '', 'x-http-server-test-empty-b': '' }\n  );\n});\n\ntest('setting default content-type via cli', (t) => {\n  t.plan(4);\n\n  getPort().then((port) => {\n    const root = path.resolve(__dirname, 'public/');\n    const options = [root, '--port', port, '--content-type', 'text/custom'];\n    const server = startServer(options);\n\n    tearDown(server, t);\n\n    server.stdout.on('data', (msg) => {\n      checkServerIsRunning(`http://localhost:${port}/f_f`, msg, t, (err, res) => {\n        t.error(err);\n        t.equal(res.headers['content-type'], 'text/custom; charset=UTF-8');\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/compression.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst ecstatic = require('../lib/core');\nconst http = require('http');\nconst request = require('request');\n\nconst root = `${__dirname}/public`;\n\ntest('serves brotli-encoded file when available', (t) => {\n  t.plan(3);\n\n  const server = http.createServer(ecstatic({\n    root,\n    brotli: true,\n    autoIndex: true\n  }));\n\n  server.listen(() => {\n    const port = server.address().port;\n    const options = {\n      uri: `http://localhost:${port}/brotli`,\n      headers: {\n        'accept-encoding': 'gzip, deflate, br'\n      }\n    };\n\n    request.get(options, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200);\n      t.equal(res.headers['content-encoding'], 'br');\n    });\n  });\n  t.once('end', () => {\n    server.close();\n  });\n});\n\ntest('serves gzip-encoded file when brotli not available', (t) => {\n  t.plan(3);\n\n  const server = http.createServer(ecstatic({\n    root,\n    brotli: true,\n    gzip: true,\n    autoIndex: true\n  }));\n\n  server.listen(() => {\n    const port = server.address().port;\n    const options = {\n      uri: `http://localhost:${port}/gzip`,\n      headers: {\n        'accept-encoding': 'gzip, deflate, br'\n      }\n    };\n\n    request.get(options, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200);\n      t.equal(res.headers['content-encoding'], 'gzip');\n    });\n  });\n  t.once('end', () => {\n    server.close();\n  });\n});\n\ntest('serves gzip-encoded file when brotli not accepted', (t) => {\n  t.plan(3);\n\n  const server = http.createServer(ecstatic({\n    root,\n    brotli: true,\n    gzip: true,\n    autoIndex: true\n  }));\n\n  server.listen(() => {\n    const port = server.address().port;\n    const options = {\n      uri: `http://localhost:${port}/brotli`,\n      headers: {\n        'accept-encoding': 'gzip, deflate'\n      }\n    };\n\n    request.get(options, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200);\n      t.equal(res.headers['content-encoding'], 'gzip');\n    });\n  });\n  t.once('end', () => {\n    server.close();\n  });\n});\n\ntest('serves gzip-encoded file when brotli not enabled', (t) => {\n  t.plan(3);\n\n  const server = http.createServer(ecstatic({\n    root,\n    brotli: false,\n    gzip: true,\n    autoIndex: true\n  }));\n\n  server.listen(() => {\n    const port = server.address().port;\n    const options = {\n      uri: `http://localhost:${port}/brotli`,\n      headers: {\n        'accept-encoding': 'gzip, deflate, br'\n      }\n    };\n\n    request.get(options, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200);\n      t.equal(res.headers['content-encoding'], 'gzip');\n    });\n  });\n  t.once('end', () => {\n    server.close();\n  });\n});\n\ntest('serves unencoded file when compression not accepted', (t) => {\n  t.plan(3);\n\n  const server = http.createServer(ecstatic({\n    root,\n    brotli: true,\n    gzip: true,\n    autoIndex: true\n  }));\n\n  server.listen(() => {\n    const port = server.address().port;\n    const options = {\n      uri: `http://localhost:${port}/brotli`,\n      headers: {\n        'accept-encoding': ''\n      }\n    };\n\n    request.get(options, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200);\n      t.equal(res.headers['content-encoding'], undefined);\n    });\n  });\n  t.once('end', () => {\n    server.close();\n  });\n});\n\ntest('serves unencoded file when compression not enabled', (t) => {\n  t.plan(3);\n\n  const server = http.createServer(ecstatic({\n    root,\n    brotli: false,\n    gzip: false,\n    autoIndex: true\n  }));\n\n  server.listen(() => {\n    const port = server.address().port;\n    const options = {\n      uri: `http://localhost:${port}/brotli`,\n      headers: {\n        'accept-encoding': 'gzip, deflate, br'\n      }\n    };\n\n    request.get(options, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200);\n      t.equal(res.headers['content-encoding'], undefined);\n    });\n  });\n  t.once('end', () => {\n    server.close();\n  });\n});\n"
  },
  {
    "path": "test/content-type.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst http = require('http');\nconst ecstatic = require('../lib/core');\nconst checkHeaders = require('./check-headers.js');\n\nconst root = `${__dirname}/public/`;\n\ntest('global default contentType', (t) => {\n  let server = null;\n  try {\n    server = http.createServer(ecstatic({\n      root,\n      contentType: 'text/plain',\n    }));\n  } catch (e) {\n    t.fail(e.message);\n    t.end();\n  }\n\n  t.plan(3);\n\n  \n  checkHeaders(t, server, 'f_f', (t, headers) => {\n    t.equal(headers['content-type'], 'text/plain; charset=UTF-8');\n  });\n});\n\ntest('content type text', (t) => {\n  t.plan(3);\n\n  const server = http.createServer(\n    ecstatic({root})\n  );\n\n  checkHeaders(t, server, 'subdir/e.html', (t, headers) => {\n    t.equal(headers['content-type'], 'text/html; charset=UTF-8');\n  });\n});\n\ntest('content type binary', (t) => {\n  t.plan(3);\n\n  const server = http.createServer(\n    ecstatic({root})\n  );\n\n  checkHeaders(t, server, 'subdir/app.wasm', (t, headers) => {\n    t.equal(headers['content-type'], 'application/wasm');\n  });\n});\n\ntest('charset arabic', (t) => {\n  t.plan(3);\n\n  const server = http.createServer(\n    ecstatic({root})\n  );\n\n  checkHeaders(t, server, 'charset/arabic.html', (t, headers) => {\n    t.equal(headers['content-type'], 'text/html; charset=ISO-8859-6');\n  });\n});\n\ntest('charset Shift_JIS', (t) => {\n  t.plan(3);\n\n  const server = http.createServer(\n    ecstatic({root})\n  );\n\n  checkHeaders(t, server, 'charset/shift_jis.html', (t, headers) => {\n    t.equal(headers['content-type'], 'text/html; charset=Shift_JIS');\n  });\n});\n"
  },
  {
    "path": "test/coop.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst server = require('../lib/core');\nconst http = require('http');\nconst path = require('path');\nconst request = require('request');\n\nconst root = path.join(__dirname, 'public');\n\ntest('coop defaults to false', (t) => {\n  t.plan(4);\n\n  const httpServer = http.createServer(\n    server({\n      root,\n      autoIndex: true,\n      defaultExt: 'html',\n    })\n  );\n\n  httpServer.listen(() => {\n    const port = httpServer.address().port;\n    const uri = `http://localhost:${port}/subdir/index.html`;\n\n    request.get({ uri }, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200);\n      t.type(res.headers['cross-origin-opener-policy'], 'undefined');\n      t.type(res.headers['cross-origin-embedder-policy'], 'undefined');\n    });\n  });\n  t.once('end', () => {\n    httpServer.close();\n  });\n});\n\ntest('coop set to false', (t) => {\n  t.plan(4);\n\n  const httpServer = http.createServer(\n    server({\n      root,\n      coop: false,\n      autoIndex: true,\n      defaultExt: 'html',\n    })\n  );\n\n  httpServer.listen(() => {\n    const port = httpServer.address().port;\n    const uri = `http://localhost:${port}/subdir/index.html`;\n\n    request.get({ uri }, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200);\n      t.type(res.headers['cross-origin-opener-policy'], 'undefined');\n      t.type(res.headers['cross-origin-embedder-policy'], 'undefined');\n    });\n  });\n  t.once('end', () => {\n    httpServer.close();\n  });\n});\n\ntest('coop set to true', (t) => {\n  t.plan(4);\n\n  const httpServer = http.createServer(\n    server({\n      root,\n      coop: true,\n      autoIndex: true,\n      defaultExt: 'html',\n    })\n  );\n\n  httpServer.listen(() => {\n    const port = httpServer.address().port;\n    const uri = `http://localhost:${port}/subdir/index.html`;\n    request.get({ uri }, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200);\n      t.equal(res.headers['cross-origin-opener-policy'], 'same-origin');\n      t.equal(res.headers['cross-origin-embedder-policy'], 'require-corp');\n    });\n  });\n  t.once('end', () => {\n    httpServer.close();\n  });\n});\n\ntest('COOP set to true', (t) => {\n  t.plan(4);\n\n  const httpServer = http.createServer(\n    server({\n      root,\n      COOP: true,\n      autoIndex: true,\n      defaultExt: 'html',\n    })\n  );\n\n  httpServer.listen(() => {\n    const port = httpServer.address().port;\n    const uri = `http://localhost:${port}/subdir/index.html`;\n    request.get({ uri }, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200);\n      t.equal(res.headers['cross-origin-opener-policy'], 'same-origin');\n      t.equal(res.headers['cross-origin-embedder-policy'], 'require-corp');\n    });\n  });\n  t.once('end', () => {\n    httpServer.close();\n  });\n});\n"
  },
  {
    "path": "test/core-error.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst ecstatic = require('../lib/core');\nconst http = require('http');\nconst request = require('request');\nconst path = require('path');\n\nconst root = `${__dirname}/public`;\nconst baseDir = 'base';\n\nrequire('fs').mkdirSync(`${root}/emptyDir`, {recursive: true});\n\nconst cases = require('./fixtures/common-cases-error');\n\ntest('core', (t) => {\n  require('portfinder').getPort((err, port) => {\n    const filenames = Object.keys(cases);\n\n    const server = http.createServer(\n      ecstatic({\n        root,\n        gzip: true,\n        baseDir,\n        autoIndex: true,\n        showDir: true,\n        handleError: false,\n      })\n    );\n\n    server.listen(port, () => {\n      let pending = filenames.length;\n      filenames.forEach((file) => {\n        const uri = `http://localhost:${port}${path.join('/', baseDir, file)}`;\n        const headers = cases[file].headers || {};\n\n        request.get({\n          uri,\n          followRedirect: false,\n          headers,\n        }, (err, res, body) => {\n          if (err) {\n            t.fail(err);\n          }\n          const r = cases[file];\n          t.equal(res.statusCode, r.code, `status code for \\`${file}\\``);\n\n          if (r.type !== undefined) {\n            t.equal(\n              res.headers['content-type'].split(';')[0], r.type,\n              `content-type for \\`${file}\\``\n            );\n          }\n\n          if (r.body !== undefined) {\n            t.equal(body, r.body, `body for \\`${file}\\``);\n          }\n\n          if (r.location !== undefined) {\n            t.equal(res.headers.location, path.join('/', baseDir, r.location), `location for \\`${file}\\``);\n          }\n\n          pending -= 1;\n          if (pending === 0) {\n            server.close();\n            t.end();\n          }\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/core.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst ecstatic = require('../lib/core');\nconst http = require('http');\nconst request = require('request');\nconst path = require('path');\nconst eol = require('eol');\n\nconst root = `${__dirname}/public`;\nconst baseDir = 'base';\n\nrequire('fs').mkdirSync(`${root}/emptyDir`, {recursive: true});\n\nconst cases = require('./fixtures/common-cases');\n\ntest('core', (t) => {\n  const filenames = Object.keys(cases);\n\n  const server = http.createServer(\n    ecstatic({\n      root,\n      gzip: true,\n      baseDir,\n      autoIndex: true,\n      showDir: true,\n      defaultExt: 'html',\n      handleError: true,\n    })\n  );\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    let pending = filenames.length;\n    filenames.forEach((file) => {\n      const uri = `http://localhost:${port}${path.join('/', baseDir, file)}`;\n      const headers = cases[file].headers || {};\n\n      request.get({\n        uri,\n        followRedirect: false,\n        headers,\n      }, (err, res, body) => {\n        if (err) {\n          t.fail(err);\n        }\n        const r = cases[file];\n        t.equal(res.statusCode, r.code, `status code for \\`${file}\\``);\n\n        if (r.type !== undefined) {\n          t.equal(\n            res.headers['content-type'].split(';')[0], r.type,\n            `content-type for \\`${file}\\``\n          );\n        }\n\n        if (r.body !== undefined) {\n          t.equal(eol.lf(body), r.body, `body for \\`${file}\\``);\n        }\n\n        if (r.location !== undefined) {\n          t.equal(path.normalize(res.headers.location), path.join('/', baseDir, r.location), `location for \\`${file}\\``);\n        }\n\n        pending -= 1;\n        if (pending === 0) {\n          server.close();\n          t.end();\n        }\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/cors.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst server = require('../lib/core');\nconst http = require('http');\nconst path = require('path');\nconst request = require('request');\n\nconst root = path.join(__dirname, 'public');\n\ntest('cors defaults to false', (t) => {\n  t.plan(4);\n\n  const httpServer = http.createServer(\n    server({\n      root,\n      autoIndex: true,\n      defaultExt: 'html',\n    })\n  );\n\n  httpServer.listen(() => {\n    const port = httpServer.address().port;\n    const uri = `http://localhost:${port}/subdir/index.html`;\n\n    request.get({ uri }, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200);\n      t.type(res.headers['access-control-allow-origin'], 'undefined');\n      t.type(res.headers['access-control-allow-headers'], 'undefined');\n    });\n  });\n  t.once('end', () => {\n    httpServer.close();\n  });\n});\n\ntest('cors set to false', (t) => {\n  t.plan(4);\n\n  const httpServer = http.createServer(\n    server({\n      root,\n      cors: false,\n      autoIndex: true,\n      defaultExt: 'html',\n    })\n  );\n\n  httpServer.listen(() => {\n    const port = httpServer.address().port;\n    const uri = `http://localhost:${port}/subdir/index.html`;\n\n    request.get({ uri }, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200);\n      t.type(res.headers['access-control-allow-origin'], 'undefined');\n      t.type(res.headers['access-control-allow-headers'], 'undefined');\n    });\n  });\n  t.once('end', () => {\n    httpServer.close();\n  });\n});\n\ntest('cors set to true', (t) => {\n  t.plan(4);\n\n  const httpServer = http.createServer(\n    server({\n      root,\n      cors: true,\n      autoIndex: true,\n      defaultExt: 'html',\n    })\n  );\n\n  httpServer.listen(() => {\n    const port = httpServer.address().port;\n    const uri = `http://localhost:${port}/subdir/index.html`;\n    request.get({ uri }, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200);\n      t.equal(res.headers['access-control-allow-origin'], '*');\n      t.equal(res.headers['access-control-allow-headers'], 'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since');\n    });\n  });\n  t.once('end', () => {\n    httpServer.close();\n  });\n});\n\ntest('CORS set to true', (t) => {\n  t.plan(4);\n\n  const httpServer = http.createServer(\n    server({\n      root,\n      CORS: true,\n      autoIndex: true,\n      defaultExt: 'html',\n    })\n  );\n\n  httpServer.listen(() => {\n    const port = httpServer.address().port;\n    const uri = `http://localhost:${port}/subdir/index.html`;\n    request.get({ uri }, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200);\n      t.equal(res.headers['access-control-allow-origin'], '*');\n      t.equal(res.headers['access-control-allow-headers'], 'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since');\n    });\n  });\n  t.once('end', () => {\n    httpServer.close();\n  });\n});\n"
  },
  {
    "path": "test/custom-content-type-file-secret.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst http = require('http');\nconst request = require('request');\nconst ecstatic = require('../lib/core');\n\ntest('custom contentType via .types file', (t) => {\n  let server = null;\n  try {\n    server = http.createServer(ecstatic({\n      root: `${__dirname}/public/`,\n      mimetypes: `${__dirname}/fixtures/custom_mime_type.types`,\n    }));\n  } catch (e) {\n    t.fail(e.message);\n    t.end();\n  }\n\n  t.plan(3);\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    request.get(`http://localhost:${port}/custom_mime_type.opml`, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200, 'custom_mime_type.opml should be found');\n      t.equal(res.headers['content-type'], 'application/secret');\n      server.close(() => { t.end(); });\n    });\n  });\n});\n"
  },
  {
    "path": "test/custom-content-type-file.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst http = require('http');\nconst request = require('request');\nconst ecstatic = require('../lib/core');\n\nfunction setup(opts) {\n  return http.createServer(ecstatic(opts));\n}\n\ntest('throws when custom contentType .types file does not exist', (t) => {\n  t.plan(1);\n\n  t.throws(\n    setup.bind(null, {\n      root: `${__dirname}/public/`,\n      mimeTypes: 'this_file_does_not_exist.types',\n    })\n  );\n});\n\ntest('custom contentType via .types file', (t) => {\n  let server = null;\n  try {\n    server = setup({\n      root: `${__dirname}/public`,\n      'mime-types': `${__dirname}/public/custom_mime_type.types`,\n    });\n  } catch (e) {\n    t.fail(e.message);\n    t.end();\n  }\n\n  t.plan(3);\n\n  server.listen(0, () => {\n    const port = server.address().port;\n\n    request.get(`http://localhost:${port}/custom_mime_type.opml`, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200, 'custom_mime_type.opml should be found');\n      t.equal(res.headers['content-type'], 'application/foo');\n      server.close(() => { t.end(); });\n    });\n  });\n});\n"
  },
  {
    "path": "test/custom-content-type.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst http = require('http');\nconst request = require('request');\nconst ecstatic = require('../lib/core');\n\ntest('custom contentType', (t) => {\n  let server = null;\n  try {\n    server = http.createServer(ecstatic({\n      root: `${__dirname}/public/`,\n      mimetype: {\n        'application/jon': ['opml'],\n      },\n    }));\n  } catch (e) {\n    t.fail(e.message);\n    t.end();\n  }\n\n  t.plan(3);\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    request.get(`http://localhost:${port}/custom_mime_type.opml`, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200, 'custom_mime_type.opml should be found');\n      t.equal(res.headers['content-type'], 'application/jon');\n      server.close(() => { t.end(); });\n    });\n  });\n});\n"
  },
  {
    "path": "test/default-default-ext.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst ecstatic = require('../lib/core');\nconst http = require('http');\nconst request = require('request');\nconst eol = require('eol');\n\ntest('default defaultExt', (t) => {\n  t.plan(3);\n  const server = http.createServer(ecstatic(`${__dirname}/public/subdir`));\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    request.get(`http://localhost:${port}`, (err, res, body) => {\n      t.error(err);\n      t.equal(res.statusCode, 200);\n      t.equal(eol.lf(body), 'index!!!\\n');\n      server.close(() => { t.end(); });\n    });\n  });\n});\n"
  },
  {
    "path": "test/dir-overrides-404.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst http = require('http');\nconst ecstatic = require('../lib/core');\nconst request = require('request');\nconst showDir = require('../lib/core/show-dir');\n\nconst root = `${__dirname}/public/dir-overrides-404`;\n\ntest('server should display directory if -d and --dir-overrides--404 flags are specified', (t) => {\n    // require('portfinder').getPort((err, port) => {\n    try {\n        const server = http.createServer(ecstatic({root, showDir: true, dirOverrides404: true}));\n        // t.plan(2);\n        // t.on('end', () => { server.close(); });\n        server.listen(0, () => {\n            const port = server.address().port;\n            request.get(`http://localhost:${port}/directory/`, (err, res, body) => {\n                if(err) {\n                    t.error(err);\n                }\n                // console.log(body);\n                // console.log(res.statusCode);\n                t.equal(res.statusCode, 200);\n                console.log(body);\n                t.equal(body.includes('Index of /directory/'), true);\n                server.close(() => { t.end(); });\n            });\n        })\n        console.log('d');\n        \n    } catch (e) {\n        t.fail(e.message);\n        t.end();\n    }\n    // });\n});\n\ntest('server should display 404.html if -d flag is specified but not --dir-overrides-404', (t) => {\n    try {\n        const server = http.createServer(ecstatic({root, showDir: true, dirOverrides404: false}));\n        // t.plan(2);\n        // t.on('end', () => { server.close(); });\n        server.listen(0, () => {\n            const port = server.address().port;\n            request.get(`http://localhost:${port}/directory/`, (err, res, body) => {\n                if(err) {\n                    t.error(err);\n                }\n                t.equal(res.statusCode, 404);\n                t.equal(body.includes('404file'), true);\n                server.close(() => { t.end(); });\n            });\n        })\n        console.log('d');\n        \n    } catch (e) {\n        t.fail(e.message);\n        t.end();\n    }\n    \n});"
  },
  {
    "path": "test/enotdir.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst ecstatic = require('../lib/core');\nconst http = require('http');\nconst request = require('request');\n\ntest('should handle ENOTDIR as 404', (t) => {\n  t.plan(3);\n  const server = http.createServer(ecstatic(`${__dirname}/public/subdir`));\n  t.on('end', () => { server.close(); });\n  server.listen(0, () => {\n    const port = server.address().port;\n    request.get(`http://localhost:${port}/index.html/hello`, (err, res, body) => {\n      t.error(err);\n      t.equal(res.statusCode, 404);\n      t.equal(body, 'File not found. :(');\n    });\n  });\n});\n"
  },
  {
    "path": "test/escaping.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst ecstatic = require('../lib/core');\nconst http = require('http');\nconst request = require('request');\nconst eol = require('eol');\n\ntest('escaping special characters', (t) => {\n  const server = http.createServer(ecstatic(`${__dirname}/public`));\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    request.get(`http://localhost:${port}/curimit%40gmail.com%20(40%25)`, (err, res, body) => {\n      t.error(err);\n      t.equal(res.statusCode, 200);\n      t.equal(eol.lf(body), 'index!!!\\n');\n      server.close(() => { t.end(); });\n    });\n  });\n});\n"
  },
  {
    "path": "test/express-error.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst ecstatic = require('../lib/core');\nconst http = require('http');\nconst express = require('express');\nconst request = require('request');\nconst path = require('path');\n\nconst root = `${__dirname}/public`;\nconst baseDir = 'base';\n\nrequire('fs').mkdirSync(`${root}/emptyDir`, {recursive: true});\n\nconst cases = require('./fixtures/common-cases-error');\n\ntest('express', (t) => {\n  require('portfinder').getPort((err, port) => {\n    const filenames = Object.keys(cases);\n    const app = express();\n\n    app.use(ecstatic({\n      root,\n      gzip: true,\n      baseDir,\n      autoIndex: true,\n      showDir: true,\n      cache: 'no-cache',\n      handleError: false,\n    }));\n\n    const server = http.createServer(app);\n\n    server.listen(port, () => {\n      let pending = filenames.length;\n      filenames.forEach((file) => {\n        const uri = `http://localhost:${port}${path.join('/', baseDir, file)}`;\n        const headers = cases[file].headers || {};\n\n        request.get({\n          uri,\n          followRedirect: false,\n          headers,\n        }, (err, res, body) => {\n          if (err) t.fail(err);\n          const r = cases[file];\n          t.equal(res.statusCode, r.code, `status code for \\`${file}\\``);\n\n          if (r.code === 200) {\n            t.equal(res.headers['cache-control'], 'no-cache', `cache control for \\`${file}\\``);\n          }\n\n          if (r.type !== undefined) {\n            t.equal(\n              res.headers['content-type'].split(';')[0], r.type,\n              `content-type for \\`${file}\\``\n            );\n          }\n\n          if (r.body !== undefined) {\n            t.equal(body, r.body, `body for \\`${file}\\``);\n          }\n\n          pending -= 1;\n          if (pending === 0) {\n            server.close();\n            t.end();\n          }\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/express.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst ecstatic = require('../lib/core');\nconst http = require('http');\nconst express = require('express');\nconst request = require('request');\nconst path = require('path');\nconst eol = require('eol');\n\nconst root = `${__dirname}/public`;\nconst baseDir = 'base';\n\nrequire('fs').mkdirSync(`${root}/emptyDir`, {recursive: true});\n\nconst cases = require('./fixtures/common-cases');\n\ntest('express', (t) => {\n  const filenames = Object.keys(cases);\n\n  const app = express();\n\n  app.use(ecstatic({\n    root,\n    gzip: true,\n    baseDir,\n    autoIndex: true,\n    showDir: true,\n    defaultExt: 'html',\n    cache: 'no-cache',\n    handleError: true,\n  }));\n\n  const server = http.createServer(app);\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    let pending = filenames.length;\n    filenames.forEach((file) => {\n      const uri = `http://localhost:${port}${path.join('/', baseDir, file)}`;\n      const headers = cases[file].headers || {};\n\n      request.get({\n        uri,\n        followRedirect: false,\n        headers,\n      }, (err, res, body) => {\n        if (err) t.fail(err);\n        const r = cases[file];\n        t.equal(res.statusCode, r.code, `status code for \\`${file}\\``);\n\n        if (r.code === 200) {\n          t.equal(res.headers['cache-control'], 'no-cache', `cache control for \\`${file}\\``);\n        }\n\n        if (r.type !== undefined) {\n          t.equal(\n            res.headers['content-type'].split(';')[0], r.type,\n            `content-type for \\`${file}\\``\n          );\n        }\n\n        if (r.body !== undefined) {\n          t.equal(eol.lf(body), r.body, `body for \\`${file}\\``);\n        }\n\n        pending -= 1;\n        if (pending === 0) {\n          server.close();\n          t.end();\n        }\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/fixtures/common-cases-error.js",
    "content": "'use strict';\n\nmodule.exports = {\n  404: {\n    code: 200,\n  },\n  'something non-existant': {\n    code: 404,\n  },\n};\n\nif (require.main === module) {\n  /* eslint-disable no-console */\n  console.log('ok 1 - test cases (error) included');\n}\n"
  },
  {
    "path": "test/fixtures/common-cases.js",
    "content": "'use strict';\n\nconst fs = require('fs');\nconst path = require('path');\nconst eol = require('eol');\n\nmodule.exports = {\n  'a.txt': {\n    code: 200,\n    type: 'text/plain',\n    body: 'A!!!\\n',\n  },\n  'b.txt': {\n    code: 200,\n    type: 'text/plain',\n    body: 'B!!!\\n',\n  },\n  'c.js': {\n    code: 200,\n    type: 'application/javascript',\n    body: 'console.log(\\'C!!!\\');\\n',\n  },\n  'd.js': {\n    code: 200,\n    type: 'application/javascript',\n    body: 'd.js\\n',\n  },\n  'e.js': {\n    code: 200,\n    type: 'application/javascript',\n    body: 'console.log(\\'π!!!\\');\\n',\n  },\n  'subdir/e.html': {\n    code: 200,\n    type: 'text/html',\n    body: '<b>e!!</b>\\n',\n  },\n  // test for defaultExt\n  'subdir/e?foo=bar': {\n    code: 200,\n    type: 'text/html',\n    body: '<b>e!!</b>\\n',\n  },\n  // test for defaultExt with noisy query param\n  'subdir/e?foo=bar.ext': {\n    code: 200,\n    type: 'text/html',\n    body: '<b>e!!</b>\\n',\n  },\n  'subdir/index.html': {\n    code: 200,\n    type: 'text/html',\n    body: 'index!!!\\n',\n  },\n  subdir: {\n    code: 302,\n    location: 'subdir/',\n  },\n  'subdir?foo=bar': {\n    code: 302,\n    location: 'subdir/?foo=bar',\n  },\n  // test for url-encoded paths\n  '%E4%B8%AD%E6%96%87': {  // '/中文'\n    code: 302,\n    location: '%E4%B8%AD%E6%96%87/',\n  },\n  '%E4%B8%AD%E6%96%87?%E5%A4%AB=%E5%B7%B4': {  // '中文?夫=巴'\n    code: 302,\n    location: '%E4%B8%AD%E6%96%87/?%E5%A4%AB=%E5%B7%B4',\n  },\n  'subdir/': {\n    code: 200,\n    type: 'text/html',\n    body: 'index!!!\\n',\n  },\n  404: {\n    code: 200,\n    type: 'text/html',\n    body: '<h1>404</h1>\\n',\n  },\n  'something-non-existant': {\n    code: 404,\n    type: 'text/html',\n    body: '<h1>404</h1>\\n',\n  },\n  'compress/foo.js': {\n    code: 200,\n    file: 'compress/foo.js.gz',\n    headers: { 'accept-encoding': 'compress, gzip' },\n    body: fs.readFileSync(path.join(__dirname, '../', 'public', 'compress', 'foo.js.gz'), 'utf8'),\n  },\n  // no accept-encoding of gzip, so serve regular file\n  'compress/foo_2.js': {\n    code: 200,\n    file: 'compress/foo_2.js',\n  },\n  'emptyDir/': {\n    code: 404,\n    body: '<h1>404</h1>\\n',\n  },\n  'subdir_with space': {\n    code: 302,\n    location: 'subdir_with%20space/',\n  },\n  'subdir_with space/index.html': {\n    code: 200,\n    type: 'text/html',\n    body: 'index :)\\n',\n  },\n  'containsSymlink/': {\n    code: 404,\n    body: '<h1>404</h1>\\n',\n  },\n  'gzip/': {\n    code: 200,\n    headers: { 'accept-encoding': 'compress, gzip' },\n    type: 'text/html',\n    body: fs.readFileSync(path.join(__dirname, '../', 'public', 'gzip', 'index.html.gz'), 'utf8'),\n  },\n  'gzip/a': {\n    code: 404,\n    headers: { 'accept-encoding': 'compress, gzip' },\n    type: 'text/html',\n    body: eol.lf(fs.readFileSync(path.join(__dirname, '../', 'public', '404.html.gz'), 'utf8')),\n  },\n  'gzip/real_ecstatic': {\n    code: 200,\n    file: 'gzip/real_ecstatic',\n    headers: { 'accept-encoding': 'compress, gzip' },\n    type: 'application/octet-stream',\n    body: fs.readFileSync(path.join(__dirname, '../', 'public', 'gzip', 'real_ecstatic.gz'), 'utf8'),\n  },\n  'gzip/real_ecstatic.gz': {\n    code: 200,\n    file: 'gzip/real_ecstatic.gz',\n    headers: { 'accept-encoding': 'compress, gzip' },\n    type: 'application/gzip',\n    body: fs.readFileSync(path.join(__dirname, '../', 'public', 'gzip', 'real_ecstatic.gz'), 'utf8'),\n  },\n  'gzip/fake_ecstatic': {\n    code: 200,\n    file: 'gzip/fake_ecstatic',\n    type: 'application/octet-stream',\n    headers: { 'accept-encoding': 'compress, gzip' },\n    body: 'ecstatic\\n',\n  },\n  'gzip/fake_ecstatic.gz': {\n    code: 200,\n    file: 'gzip/fake_ecstatic.gz',\n    type: 'application/gzip',\n    headers: { 'accept-encoding': 'compress, gzip' },\n    body: fs.readFileSync(path.join(__dirname, '../', 'public', 'gzip', 'fake_ecstatic.gz'), 'utf8'),\n  },\n};\n\nif (require.main === module) {\n  /* eslint-disable no-console */\n  console.log('ok 1 - test cases included');\n}\n"
  },
  {
    "path": "test/fixtures/custom_mime_type.types",
    "content": "# This file is an example of the Apache .types file format for describing mime-types.\n# Other example: http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types\napplication/secret                 opml\n"
  },
  {
    "path": "test/fixtures/https/agent2-cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEIDCCAggCCQChRDh/XiBF+zANBgkqhkiG9w0BAQsFADBUMQswCQYDVQQGEwJ1\nczETMBEGA1UECAwKV2FzaGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEeMBwGA1UE\nAwwVRHVtbXkgSW50ZXJtZWRpYXRlIENBMB4XDTE4MDYyMjIwMzEwNFoXDTMyMDIy\nOTIwMzEwNFowUDELMAkGA1UEBhMCdXMxEzARBgNVBAgMCldhc2hpbmd0b24xEDAO\nBgNVBAcMB1NlYXR0bGUxGjAYBgNVBAMMEWR1bW15LmV4YW1wbGUuY29tMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvSQq3d8AeZMTvtqZ13jWCckikyXJ\nSACvkGCQUCJqOceESbg6IHdRzQdoccE4P3sbvNsf9BlbdJKM+neCxabqKaU1PPje\n4P0tHT57t6yJrMuUh9NxEz3Bgh1srNHVS7saKvwHmcKm79jc+wxlioPmEQvQagjn\ny7oTkyLt0sn4LGxBjrcv2JoHOC9f1pxX7l47MaiN0/ctRau7Nr3PFn+pkB4Yf6Z0\nVyicVJbaUSz39Qo4HQWl1L2hiBP3CS1oKs2Yk0O1aOCMExWrhZQan+ZgHqL1rhgm\nkPpw2/zwwPt5Vf9CSakvHwg198EXuTTXtkzYduuIJAm8yp969iEIiG2xTwIDAQAB\nMA0GCSqGSIb3DQEBCwUAA4ICAQBnMSIo+kujkeXPh+iErFBmNtu/7EA+i/QnFPbN\nlSLngclYYBJAGQI+DhirJI8ghDi6vmlHB2THewDaOJXEKvC1czE8064wioIcA9HJ\nl3QJ3YYOFRctYdSHBU4TWdJbPgkLWDzYP5smjOfw8nDdr4WO/5jh9qRFcFpTFmQf\nDyU3xgWLsQnNK3qXLdJjWG75pEhHR+7TGo+Ob/RUho/1RX/P89Ux7/oVbzdKqqFu\nSErXAsjEIEFzWOM2uDOt6hrxDF6q+8/zudwQNEo422poEcTT9tDEFxMQ391CzZRi\nnozBm4igRn1f5S3YZzLI6VEUns0s76BNy2CzvFWn40DziTqNBExAMfFFj76wiMsX\n6fTIdcvkaTBa0S9SZB0vN99qahBdcG17rt4RssMHVRH1Wn7NXMwe476L0yXZ6gO7\nZ4uNAPxgaI3BRP75EPfslLutCLZ+BC4Zzu6MY0Salbpfl0Go462EhsKCxvYhE2Dg\nT477pICLfETZfA499Fd1tOaIsoLCrILAia/+Yd76uf94MuXUIqykDng/4H7xCc47\nBZhNFJiPC6XHaXzN7NYSEUNX9VOwY8ncxKwtP6TXga96PdMUy/p98KIM8RZlDoxB\nXy9dcZBFNn/zrqjW7R0CCWCUriDIFSmEP0wDZ91YYa6BVuJMb5uL/USkTLpjZS4/\nHNGvug==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "test/fixtures/https/agent2-key.pem",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEAvSQq3d8AeZMTvtqZ13jWCckikyXJSACvkGCQUCJqOceESbg6\nIHdRzQdoccE4P3sbvNsf9BlbdJKM+neCxabqKaU1PPje4P0tHT57t6yJrMuUh9Nx\nEz3Bgh1srNHVS7saKvwHmcKm79jc+wxlioPmEQvQagjny7oTkyLt0sn4LGxBjrcv\n2JoHOC9f1pxX7l47MaiN0/ctRau7Nr3PFn+pkB4Yf6Z0VyicVJbaUSz39Qo4HQWl\n1L2hiBP3CS1oKs2Yk0O1aOCMExWrhZQan+ZgHqL1rhgmkPpw2/zwwPt5Vf9CSakv\nHwg198EXuTTXtkzYduuIJAm8yp969iEIiG2xTwIDAQABAoIBAGPIw/C/qJF7HYyv\n6T+7GTiaa2o0IiehbP3/Y8NTFLWc49a8obXlHTvMr7Zr2I/tE+ojtIzkH9K1SjkN\neelqsNj9tsOPDI6oIvftsflpxkxqLtclnt8m0oMhoObf4OaONDT/N8dP4SBiSdsM\nZDmacnMFx5NZVWiup4sVf2CYexx7qks9FhyN2K5PArCQ4S9LHjFhSJVH4DSEpv7E\nYkbp30rhpqV7wSwjgUsm8ZYvI2NOlmffzLSiPdt3vy2K5Q25S/MVEAicg83rfDgK\n6EluHjeygRI1xU6DJ0hU7tnU7zE9KURoHPUycO3BKzZnzUH26AA36I58Pu4fXWw/\nCgmbv2ECgYEA+og9E4ziKCEi3p8gqjIfwTRgWZxDLjEzooB/K0UhEearn/xiX29A\nFiSzEHKfCB4uSrw5OENg2ckDs8uy08Qmxx7xFXL7AtufAl5fIYaWa0sNSqCaIk7p\nebbUmPcaYhKiLzIEd1EYEL38sXVZ62wvSVMRSWvEMq44g1qnoRlDa/8CgYEAwUTt\ntalYNwVmR9ZdkVEWm9ZxirdzoM6NaM6u4Tf34ygptpapdmIFSUhfq4iOiEnRGNg/\ntuNqhNCIb3LNpJbhRPEzqN7E7qiF/mp7AcJgbuxLZBm12QuLuJdG3nrisKPFXcY1\nlA4A7CFmNgH3E4THFfgwzyDXsBOxVLXleTqn+rECgYEA9up1P6J3dtOJuV2d5P/3\nugRz/X173LfTSxJXw36jZDAy8D/feG19/RT4gnplcKvGNhQiVOhbOOnbw0U8n2fQ\nTCmbs+cZqyxnH/+AxNsPvvk+RVHZ93xMsY/XIldP4l65B8jFDA+Zp06IESI2mEeM\npzi+bd1Phh+dRSCA2865W2MCgYEAlxYsgmQ1WyX0dFpHYU+zzfXRYzDQyrhOYc2Z\nduVK+yCto1iad7pfCY/zgmRJkI+sT7DV9kJIRjXDQuTLkEyHJF8vFGe6KhxCS8aw\nDIsI2g4NTd6vg1J8UryoIUqNpqsQoqNNxUVBQVdG0ReuMGsPO8R/W50AIFz0txVP\no/rP0LECgYEA7e/mOwCnR+ovmS/CAksmos3oIqvkRkXNKpKe513FVmp3TpTU38ex\ncBkFNU3hEO31FyrX1hGIKp3N5mHYSQ1lyODHM6teHW0OLWWTwIe8rIGvR2jfRLe0\nbbkdj40atYVkfeFmpz9uHHG24CUYxJdPc360jbXTVp4i3q8zqgL5aMY=\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "test/fixtures/proxy-all-local/does-not-exist",
    "content": "This file should never be served when proxyAll is enabled.\n"
  },
  {
    "path": "test/fixtures/proxy-all-local/file",
    "content": "Local proxy-all fixture file\n"
  },
  {
    "path": "test/fixtures/root/canYouSeeMe",
    "content": "I bet you can. I'm in your index.\n\n"
  },
  {
    "path": "test/fixtures/root/compression/index.html",
    "content": "I'm not compressed!\n"
  },
  {
    "path": "test/fixtures/root/compression/index.html.br",
    "content": "\u000b\n�im brotli compressed!!\n\u0003\n"
  },
  {
    "path": "test/fixtures/root/file",
    "content": "hello, I know nodejitsu\n\n"
  },
  {
    "path": "test/fixtures/root/htmlButNot",
    "content": "<div>\n  <h1>I am HTML?</h1>\n  <small>yeah i guess</small>\n</div>\n"
  },
  {
    "path": "test/force-content-encoding.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst ecstatic = require('../lib/core');\nconst http = require('http');\nconst request = require('request');\n\nconst root = `${__dirname}/public`;\n\ntest('--force-content-encoding flag: .br files served without Content-Encoding header when flag not set', (t) => {\n  t.plan(3);\n\n  const server = http.createServer(ecstatic({\n    root,\n    brotli: true,\n    autoIndex: true,\n    forceContentEncoding: false\n  }));\n\n  server.listen(() => {\n    const port = server.address().port;\n    const options = {\n      uri: `http://localhost:${port}/brotli/index.html.br`,\n      headers: {\n        'accept-encoding': 'gzip, deflate, br'\n      }\n    };\n\n    request.get(options, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200);\n      t.notOk(res.headers['content-encoding'], 'should not have content-encoding header when flag not set');\n    });\n  });\n\n  t.once('end', () => {\n    server.close();\n  });\n});\n\ntest('--force-content-encoding flag: .br files served with Content-Encoding header when flag is set', (t) => {\n  t.plan(3);\n\n  const server = http.createServer(ecstatic({\n    root,\n    brotli: true,\n    autoIndex: true,\n    forceContentEncoding: true\n  }));\n\n  server.listen(() => {\n    const port = server.address().port;\n    const options = {\n      uri: `http://localhost:${port}/brotli/index.html.br`,\n      headers: {\n        'accept-encoding': 'gzip, deflate, br'\n      }\n    };\n\n    request.get(options, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200);\n      t.equal(res.headers['content-encoding'], 'br', 'should have content-encoding: br header when flag is set');\n    });\n  });\n\n  t.once('end', () => {\n    server.close();\n  });\n});\n\ntest('--force-content-encoding flag: regular files served with Content-Encoding header when flag is set', (t) => {\n  t.plan(3);\n\n  const server = http.createServer(ecstatic({\n    root,\n    brotli: true,\n    autoIndex: true,\n    forceContentEncoding: true\n  }));\n\n  server.listen(() => {\n    const port = server.address().port;\n    const options = {\n      uri: `http://localhost:${port}/brotli/index.html`,\n      headers: {\n        'accept-encoding': 'gzip, deflate, br'\n      }\n    };\n\n    request.get(options, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200);\n      t.ok(res.headers['content-encoding'], 'regular files should have content-encoding header');\n    });\n  });\n\n  t.once('end', () => {\n    server.close();\n  });\n});\n"
  },
  {
    "path": "test/headers.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst ecstatic = require('../lib/core');\nconst http = require('http');\nconst checkHeaders = require('./check-headers.js');\n\nconst root = `${__dirname}/public`;\n\ntest('headers object', (t) => {\n  t.plan(4);\n\n  const server = http.createServer(\n    ecstatic({\n      root,\n      headers: {\n        Wow: 'sweet',\n        Cool: 'beans',\n      },\n      autoIndex: true,\n      defaultExt: 'html',\n    })\n  );\n\n  checkHeaders(t, server, 'subdir', (t, headers) => {\n    t.equal(headers.wow, 'sweet');\n    t.equal(headers.cool, 'beans');\n  });\n});\n\ntest('header string', (t) => {\n  t.plan(3);\n\n  const server = http.createServer(\n    ecstatic({\n      root,\n      header: 'beep: boop', // for command-line --header 'beep: boop'\n      autoIndex: true,\n      defaultExt: 'html',\n    })\n  );\n\n  checkHeaders(t, server, 'subdir', (t, headers) => {\n    t.equal(headers.beep, 'boop');\n  });\n});\n\ntest('header array', (t) => {\n  t.plan(3);\n\n  const server = http.createServer(\n    ecstatic({\n      root,\n      header: [\n        'beep: boop', // --header 'beep: boop'\n        'what: ever', // --header 'what: ever'\n      ],\n      autoIndex: true,\n      defaultExt: 'html',\n    })\n  );\n\n  checkHeaders(t, server, 'subdir', (t, headers) => {\n    t.equal(headers.beep, 'boop');\n  });\n});\n\ntest('H array', (t) => {\n  t.plan(3);\n\n  const server = http.createServer(\n    ecstatic({\n      root,\n      H: [\n        'beep: boop', // -H 'beep: boop'\n        'what: ever', // -H 'what: ever'\n      ],\n      autoIndex: true,\n      defaultExt: 'html',\n    })\n  );\n\n  checkHeaders(t, server, 'subdir', (t, headers) => {\n    t.equal(headers.beep, 'boop');\n  });\n});\n\n// CRLF injection prevention\ntest('CRLF injection prevention', (t) => {\n  t.plan(1);\n\n  t.throws(() => {\n    const server = http.createServer(\n      ecstatic({\n        root,\n        H: [\n          'X-CRLF-Injection: X\\r\\nContent-Type: text/html',\n        ],\n        autoIndex: true,\n        defaultExt: 'html',\n      })\n    );\n\n    server.close();\n  }, /Header is not a string or contains CRLF/);\n});"
  },
  {
    "path": "test/illegal-access-date.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst ecstatic = require('../lib/core');\nconst http = require('http');\nconst path = require('path');\nconst request = require('request');\n\ntest('if-modified-since illegal access date', (t) => {\n  const dir = path.join(__dirname, 'public');\n  const server = http.createServer(ecstatic(dir));\n\n  t.plan(2);\n\n  server.listen(0, () => {\n    const opts = {\n      url: `http://localhost:${server.address().port}/a.txt`,\n      headers: { 'if-modified-since': '275760-09-24' },\n    };\n    request.get(opts, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200);\n      server.close(() => { t.end(); });\n    });\n  });\n});\n"
  },
  {
    "path": "test/localhost.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst ecstatic = require('../lib/core');\nconst http = require('http');\nconst request = require('request');\n\ntest('can connect from all localhost addresses', t => {\n  const server = http.createServer(ecstatic(`${__dirname}/public/subdir`));\n  t.on('end', () => { server.close(); });\n  server.listen(0, () => {\n    const port = server.address().port;\n    const addresses = [\n      'localhost',\n      '127.0.0.1',\n      '::1',\n    ];\n\n    t.plan(addresses.length * 2);\n\n    for (const address of addresses) {\n      request.get(`http://[${address}]:${port}/index.html`, (err, res, body) => {\n        t.error(err);\n        t.equal(res.statusCode, 200);\n      });\n    }\n  });\n});\n"
  },
  {
    "path": "test/main.test.js",
    "content": "const test = require('tap').test;\nconst path = require('path');\nconst fs = require('fs');\nconst request = require('request');\nconst httpServer = require('../lib/http-server');\nconst promisify = require('util').promisify;\n\nconst requestAsync = promisify(request);\nconst fsReadFile = promisify(fs.readFile);\n\n// Prevent errors from being swallowed\nprocess.on('uncaughtException', console.error);\n\nconst root = path.join(__dirname, 'fixtures', 'root');\n\n// Tests are grouped into those which can run together. The groups are given\n// their own port to run on and live inside a Promise. Tests are done when all\n// Promise test groups complete.\ntest('http-server main', (t) => {\n  Promise.all([\n    new Promise((resolve) => {\n      const server = httpServer.createServer({\n        root,\n        robots: true,\n        headers: {\n          'Access-Control-Allow-Origin': '*',\n          'Access-Control-Allow-Credentials': 'true'\n        },\n        coop: true,\n        cors: true,\n        corsHeaders: 'X-Test',\n        ext: true,\n        brotli: true,\n        gzip: true\n      });\n      server.listen(0, async () => {\n        const port = server.address().port;\n        try {\n\n          // Since none of these depend on anything not already declared, they\n          // can run on the event loop at their own leisure\n          await Promise.all([\n            // request file from root\n            requestAsync(`http://localhost:${port}/file`).then(async (res) => {\n              // files should be served from the root\n              t.equal(res.statusCode, 200);\n\n              const fileData = await fsReadFile(path.join(root, 'file'), 'utf8');\n              t.equal(res.body.trim(), fileData.trim(), 'root file content matches');\n            }).catch(err => t.fail(err.toString())),\n\n            // Request non-existent file\n            requestAsync(`http://localhost:${port}/404`).then(res => {\n              t.ok(res);\n              t.equal(res.statusCode, 404);\n            }).catch(err => t.fail(err.toString())),\n\n            // Request root\n            requestAsync(`http://localhost:${port}/`).then(res => {\n              t.ok(res);\n              t.equal(res.statusCode, 200);\n              t.match(res.body, './file');\n              t.match(res.body, './canYouSeeMe');\n\n              // Custom headers\n              t.equal(res.headers['access-control-allow-origin'], '*');\n              t.equal(res.headers['access-control-allow-credentials'], 'true');\n              t.equal(res.headers['cross-origin-opener-policy'], 'same-origin');\n              t.equal(res.headers['cross-origin-embedder-policy'], 'require-corp');\n            }).catch(err => t.fail(err.toString())),\n\n            // Get robots\n            requestAsync(`http://localhost:${port}/robots.txt`).then(res => {\n              t.equal(res.statusCode, 200);\n            }).catch(err => t.fail(err.toString())),\n\n            // CORS time\n            requestAsync({\n              uri: `http://localhost:${port}`,\n              method: 'OPTIONS',\n              headers: {\n                'Access-Control-Request-Method': 'GET',\n                Origin: 'http://example.com',\n                'Access-Control-Request-Headers': 'Foobar'\n              }\n            }).then(res => {\n              t.equal(res.statusCode, 204);\n              t.ok(\n                res.headers['access-control-allow-headers']\n                  .split(/\\s*,\\s*/g)\n                  .indexOf('X-Test') >= 0, 204);\n            }).catch(err => t.fail(err.toString())),\n\n            t.test(\n              \"Regression: don't crash on control characters in query strings\",\n              {},\n              (t) => {\n                requestAsync({\n                  uri: encodeURI(`http://localhost:${port}/file?\\x0cfoo`),\n                }).then(res => {\n                  t.equal(res.statusCode, 200);\n                }).catch(err => t.fail(err.toString()))\n                  .finally(() => t.end());\n              }\n            ),\n\n            // Light compression testing. Heavier compression tests exist in\n            // compression.test.js\n            requestAsync({\n              uri: `http://localhost:${port}/compression/`,\n              headers: {\n                'accept-encoding': 'gzip'\n              }\n            }).then(res => {\n              t.equal(res.statusCode, 200);\n              t.equal(res.headers['content-encoding'], 'gzip');\n            }).catch(err => t.fail(err.toString())),\n\n            requestAsync({\n              uri: `http://localhost:${port}/compression/`,\n              headers: {\n                'accept-encoding': 'gzip, br'\n              }\n            }).then(res => {\n              t.equal(res.statusCode, 200);\n              t.equal(res.headers['content-encoding'], 'br');\n            }).catch(err => t.fail(err.toString())),\n\n            requestAsync(`http://localhost:${port}/htmlButNot`).then(res => {\n              t.equal(res.statusCode, 200);\n              t.match(res.headers['content-type'], /^text\\/html/);\n            }).catch(err => t.fail(err.toString()))\n          ]);\n\n          // Another server proxies to the main server\n          const proxyServer = httpServer.createServer({\n            proxy: `http://localhost:${port}`,\n            root: path.join(__dirname, 'fixtures')\n          });\n\n          await new Promise((resolve) => {\n            proxyServer.listen(0, async () => {\n              const proxyPort = proxyServer.address().port;\n              try {\n                // Serve files from proxy root\n                await requestAsync(`http://localhost:${proxyPort}/root/file`).then(async (res) => {\n                  t.ok(res);\n                  t.equal(res.statusCode, 200);\n\n                  // File content matches\n                  const fileData = await fsReadFile(path.join(root, 'file'), 'utf8');\n                  t.equal(res.body.trim(), fileData.trim(), 'proxied root file content matches');\n                }).catch(err => t.fail(err.toString()));\n\n                // Proxy fallback\n                await requestAsync(`http://localhost:${proxyPort}/file`).then(async (res) => {\n                  t.ok(res);\n                  t.equal(res.statusCode, 200);\n\n                  // File content matches\n                  const fileData = await fsReadFile(path.join(root, 'file'), 'utf8');\n                  t.equal(res.body.trim(), fileData.trim(), 'proxy fallback root file content matches');\n                }).catch(err => t.fail(err.toString()));\n              } catch (err) {\n                t.fail(err.toString());\n              } finally {\n                proxyServer.close();\n                resolve();\n              }\n            });\n          });\n\n        } catch (err) {\n          t.fail(err.toString());\n        } finally {\n          server.close();\n          resolve();\n        }\n      });\n    }),\n    new Promise((resolve) => {\n      const server = httpServer.createServer({\n        root,\n        username: 'correct_username',\n        password: 'correct_password'\n      });\n\n      server.listen(0, async () => {\n        const authPort1 = server.address().port;\n        try {\n          await Promise.all([\n            // Bad request with no auth\n            requestAsync(`http://localhost:${authPort1}/file`).then((res) => {\n              t.equal(res.statusCode, 401);\n              t.equal(res.body, 'Access denied', 'Bad auth returns expected body');\n            }).catch(err => t.fail(err.toString())),\n\n            // bad user\n            requestAsync(`http://localhost:${authPort1}/file`, {\n              auth: {\n                user: 'wrong_username',\n                pass: 'correct_password'\n              }\n            }).then((res) => {\n              t.equal(res.statusCode, 401);\n              t.equal(res.body, 'Access denied', 'Bad auth returns expected body');\n            }).catch(err => t.fail(err.toString())),\n\n            // bad password\n            requestAsync(`http://localhost:${authPort1}/file`, {\n              auth: {\n                user: 'correct_username',\n                pass: 'wrong_password'\n              }\n            }).then((res) => {\n              t.equal(res.statusCode, 401);\n              t.equal(res.body, 'Access denied', 'Bad auth returns expected body');\n            }).catch(err => t.fail(err.toString())),\n\n            // nonexistant file, and bad auth\n            requestAsync(`http://localhost:${authPort1}/404`, {\n              auth: {\n                user: 'correct_username',\n                pass: 'wrong_password'\n              }\n            }).then((res) => {\n              t.equal(res.statusCode, 401);\n              t.equal(res.body, 'Access denied', 'Bad auth returns expected body');\n            }).catch(err => t.fail(err.toString())),\n\n            // good path, good auth\n            requestAsync(`http://localhost:${authPort1}/file`, {\n              auth: {\n                user: 'correct_username',\n                pass: 'correct_password'\n              }\n            }).then(async (res) => {\n              t.equal(res.statusCode, 200);\n              const fileData = await fsReadFile(path.join(root, 'file'), 'utf8');\n              t.equal(res.body.trim(), fileData.trim(), 'auth-protected file with good auth has expected file content');\n            }).catch(err => t.fail(err.toString())),\n          ]);\n        } catch (err) {\n          t.fail(err.toString());\n        } finally {\n          server.close();\n          resolve();\n        }\n      });\n    }),\n\n    new Promise((resolve) => {\n      const server = httpServer.createServer({\n        root,\n        username: 'correct_username',\n        password: 123456\n      });\n\n      server.listen(0, async () => {\n        const authPort2 = server.address().port;\n        try {\n          await Promise.all([\n            // regression test\n            requestAsync(`http://localhost:${authPort2}/file`).then(res => {\n              t.equal(res.statusCode, 401);\n              t.equal(res.body, 'Access denied', 'Bad auth returns expected body');\n            }).catch(err => t.fail(err.toString())),\n\n            // regression test, bad username\n            requestAsync(`http://localhost:${authPort2}/file`, {\n              auth: {\n                user: 'wrong_username',\n                pass: '123456'\n              }\n            }).then(res => {\n              t.equal(res.statusCode, 401);\n              t.equal(res.body, 'Access denied', 'Bad auth returns expected body');\n            }).catch(err => t.fail(err.toString())),\n\n            // regression test, correct auth, even though the password is a\n            // different type.\n            requestAsync(`http://localhost:${authPort2}/file`, {\n              auth: {\n                user: 'correct_username',\n                pass: '123456'\n              }\n            }).then(async (res) => {\n              t.equal(res.statusCode, 200);\n              const fileData = await fsReadFile(path.join(root, 'file'), 'utf8');\n              t.equal(res.body.trim(), fileData.trim(), 'numeric auth with good auth has expected file content');\n            }).catch(err => t.fail(err.toString()))\n          ]);\n        } catch (err) {\n          t.fail(err.toString());\n        } finally {\n          server.close();\n          resolve();\n        }\n      });\n    }),\n\n    new Promise((resolve) => {\n      const server = httpServer.createServer({\n        root,\n        baseDir: '/test'\n      });\n\n      server.listen(0, async () => {\n        const baseDirPort = server.address().port;\n        try {\n          await Promise.all([\n            // should serve files at the specified baseDir\n            requestAsync(`http://localhost:${baseDirPort}/test/file`).then(async (res) => {\n              t.equal(res.statusCode, 200);\n              const fileData = await fsReadFile(path.join(root, 'file'), 'utf8');\n              t.equal(res.body.trim(), fileData.trim());\n            }).catch(err => t.fail(err.toString())),\n\n            // should not serve files at the root\n            requestAsync(`http://localhost:${baseDirPort}/file`).then((res) => {\n              t.equal(res.statusCode, 403);\n              t.equal(res.body, '');\n            }).catch(err => t.fail(err.toString())),\n          ]);\n        } catch (err) {\n          t.fail(err.toString());\n        } finally {\n          server.close();\n          resolve();\n        }\n      });\n    }),\n  ]).then(() => t.end())\n    .catch(err => {\n      t.fail(err.toString());\n      t.end();\n    });\n});\n"
  },
  {
    "path": "test/malformed-dir.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst ecstatic = require('../lib/core');\nconst http = require('http');\nconst request = require('request');\n\ntest('malformed showdir uri', (t) => {\n  const server = http.createServer(ecstatic(__dirname, { showDir: true }));\n\n  t.plan(2);\n\n  server.listen(0, () => {\n    request.get(`http://localhost:${server.address().port}/?%`, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 400);\n      server.close(() => { t.end(); });\n    });\n  });\n});\n"
  },
  {
    "path": "test/malformed.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst ecstatic = require('../lib/core');\nconst http = require('http');\nconst request = require('request');\n\ntest('malformed uri', (t) => {\n  const server = http.createServer(ecstatic(__dirname));\n\n  t.plan(2);\n\n  server.listen(0, () => {\n    request.get(`http://localhost:${server.address().port}/%`, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 400);\n      server.close(() => { t.end(); });\n    });\n  });\n});\n\n"
  },
  {
    "path": "test/mime.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst mime = require('mime');\n\ntest('mime package lookup', (t) => {\n  t.plan(7);\n\n  t.equal(mime.lookup('/path/to/file.css'), 'text/css');\n  t.equal(mime.lookup('/path/to/file.js'), 'application/javascript');\n  t.equal(mime.lookup('/path/to/file.mjs'), 'application/javascript');\n  t.equal(mime.lookup('/path/to/file.txt'), 'text/plain');\n  t.equal(mime.lookup('file.txt'), 'text/plain');\n  t.equal(mime.lookup('.TXT'), 'text/plain');\n  t.equal(mime.lookup('htm'), 'text/html');\n\n  t.end();\n});\n\ntest('custom definition of mime-type with the mime package', (t) => {\n  t.plan(1);\n\n  mime.define({\n    'application/xml': ['opml'],\n  });\n  t.equal(mime.lookup('.opml'), 'application/xml');\n\n  t.end();\n});\n\ntest('custom definition of mime-type with a .types file', (t) => {\n  t.plan(2);\n\n  try {\n    mime.load('test/public/custom_mime_type.types');\n  } catch (e) {\n    t.fail(e.message);\n    t.end();\n  }\n\n  t.equal(mime.lookup('.opml'), 'application/foo'); // see public/custom_mime_type.types\n\n  t.throws(mime.load.bind(mime, 'public/this_file_does_not_exist.types'));\n\n  t.end();\n});\n"
  },
  {
    "path": "test/network-interfaces.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst os = require('os');\n\ntest('network interface display handles both string and numeric family values', (t) => {\n  t.plan(4);\n\n  // Store original function to restore later\n  const originalNetworkInterfaces = os.networkInterfaces;\n\n  // Mock data with string family values (Node < 18)\n  const mockInterfacesString = {\n    'eth0': [\n      { family: 'IPv4', address: '192.168.1.100', internal: false },\n      { family: 'IPv6', address: '::1', internal: false }\n    ],\n    'lo': [\n      { family: 'IPv4', address: '127.0.0.1', internal: true }\n    ]\n  };\n\n  // Mock data with numeric family values (Node >= 18)\n  const mockInterfacesNumeric = {\n    'eth0': [\n      { family: 4, address: '192.168.1.100', internal: false },\n      { family: 6, address: '::1', internal: false }\n    ],\n    'lo': [\n      { family: 4, address: '127.0.0.1', internal: true }\n    ]\n  };\n\n  // Test the logic that filters IPv4 interfaces (extracted from bin/http-server)\n  function getIPv4Addresses(interfaces) {\n    const addresses = [];\n    Object.keys(interfaces).forEach(function (dev) {\n      interfaces[dev].forEach(function (details) {\n        // This is the fix: handle both string and numeric family values\n        if (details.family === 'IPv4' || details.family === 4) {\n          addresses.push(details.address);\n        }\n      });\n    });\n    return addresses;\n  }\n\n  // Test with string family values\n  let addresses = getIPv4Addresses(mockInterfacesString);\n  t.equal(addresses.length, 2, 'Should find 2 IPv4 addresses with string family');\n  t.ok(addresses.includes('192.168.1.100'), 'Should include external IPv4 address');\n\n  // Test with numeric family values  \n  addresses = getIPv4Addresses(mockInterfacesNumeric);\n  t.equal(addresses.length, 2, 'Should find 2 IPv4 addresses with numeric family');\n  t.ok(addresses.includes('192.168.1.100'), 'Should include external IPv4 address');\n\n  // Restore original function\n  os.networkInterfaces = originalNetworkInterfaces;\n});\n\ntest('network interface filtering excludes IPv6 link-local addresses', (t) => {\n  t.plan(2);\n\n  const mockInterfaces = {\n    'eth0': [\n      { family: 4, address: '192.168.1.100', internal: false },\n      { family: 6, address: '2001:db8::1', internal: false },\n      { family: 6, address: 'fe80::1', internal: false } // link-local, should be excluded\n    ]\n  };\n\n  // Extract the IPv6 filtering logic from bin/http-server\n  const ipv6Addresses = [];\n  const ipv4Addresses = [];\n  \n  Object.keys(mockInterfaces).forEach(function (dev) {\n    mockInterfaces[dev].forEach(function (details) {\n      if (details.family === 'IPv4' || details.family === 4) {\n        ipv4Addresses.push(details.address);\n      }\n      if ((details.family === 'IPv6' || details.family === 6) && \n          !details.address.startsWith(\"fe80\")) {\n        ipv6Addresses.push(details.address);\n      }\n    });\n  });\n\n  t.equal(ipv4Addresses.length, 1, 'Should find 1 IPv4 address');\n  t.equal(ipv6Addresses.length, 1, 'Should find 1 non-link-local IPv6 address');\n});"
  },
  {
    "path": "test/pathname-encoding.test.js",
    "content": "'use strict';\n\nconst tap = require('tap');\nconst ecstatic = require('../lib/core');\nconst http = require('http');\nconst request = require('request');\nconst path = require('path');\nconst portfinder = require('portfinder');\n\nconst test = tap.test;\n\nconst root = `${__dirname}/public`;\nconst baseDir = 'base';\n\nif (process.platform === 'win32') {\n  tap.plan(0, 'Windows is allergic to < in path names');\n  return;\n}\n\nconst fs = require('fs');\n\ntest('create test directory', (t) => {\n  fs.mkdirSync(`${root}/<dir>`, '0755');\n  t.end();\n});\n\ntest('directory listing with pathname including HTML characters', (t) => {\n  const server = http.createServer(\n    ecstatic({\n      root,\n      baseDir,\n      showDir: true,\n      autoIndex: false,\n    })\n  );\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    const uri = `http://localhost:${port}${path.join('/', baseDir, '/%3Cdir%3E')}`;\n      request.get({\n        uri,\n      }, (err, res, body) => {\n        t.notMatch(body, /<dir>/, 'We didn\\'t find the unencoded pathname');\n        t.match(body, /&#x3C;dir&#x3E;/, 'We found the encoded pathname');\n        server.close();\n        t.end();\n      });\n    });\n});\n\ntest('NULL byte in request path does not crash server', (t) => {\n  const server = http.createServer(\n    ecstatic({\n      root,\n      baseDir,\n    })\n  );\n\n  try {\n    server.listen(0, () => {\n      const port = server.address().port;\n      const uri = `http://localhost:${port}${path.join('/', baseDir, '/%00')}`;\n      request.get({uri}, (err, res, body) => {\n        t.pass('server did not crash')\n        server.close();\n        t.end();\n      });\n    });\n  } catch (err) {\n    t.fail(err.toString());\n  }\n});\n\ntest('remove test directory', (t) => {\n  fs.rmdirSync(`${root}/<dir>`);\n  t.end();\n});\n"
  },
  {
    "path": "test/private-network-access.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst server = require('../lib/core');\nconst http = require('http');\nconst path = require('path');\nconst request = require('request');\n\nconst root = path.join(__dirname, 'public');\n\ntest('private-network-access defaults to false', (t) => {\n  t.plan(3);\n\n  const httpServer = http.createServer(\n    server({\n      root,\n      autoIndex: true,\n      defaultExt: 'html',\n    })\n  );\n\n  httpServer.listen(() => {\n    const port = httpServer.address().port;\n    const uri = `http://localhost:${port}/subdir/index.html`;\n\n    request.get({ uri }, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200);\n      t.type(res.headers['access-control-allow-private-network'], 'undefined');\n    });\n  });\n  t.once('end', () => {\n    httpServer.close();\n  });\n});\n\ntest('privateNetworkAccess set to false', (t) => {\n  t.plan(3);\n\n  const httpServer = http.createServer(\n    server({\n      root,\n      privateNetworkAccess: false,\n      autoIndex: true,\n      defaultExt: 'html',\n    })\n  );\n\n  httpServer.listen(() => {\n    const port = httpServer.address().port;\n    const uri = `http://localhost:${port}/subdir/index.html`;\n\n    request.get({ uri }, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200);\n      t.type(res.headers['access-control-allow-private-network'], 'undefined');\n    });\n  });\n  t.once('end', () => {\n    httpServer.close();\n  });\n});\n\ntest('privateNetworkAccess set to true', (t) => {\n  t.plan(3);\n\n  const httpServer = http.createServer(\n    server({\n      root,\n      privateNetworkAccess: true,\n      autoIndex: true,\n      defaultExt: 'html',\n    })\n  );\n\n  httpServer.listen(() => {\n    const port = httpServer.address().port;\n    const uri = `http://localhost:${port}/subdir/index.html`;\n    request.get({ uri }, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 200);\n      t.equal(res.headers['access-control-allow-private-network'], 'true');\n    });\n  });\n  t.once('end', () => {\n    httpServer.close();\n  });\n});\n"
  },
  {
    "path": "test/process-env-port.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst request = require('request');\nconst spawn = require('child_process').spawn;\n\nfunction getRandomInt(min, max) {\n  return Math.floor((Math.random() * ((max - min) + 1))) + min;\n}\n\nconst sanePort = getRandomInt(1025, 65536);\nconst floatingPointPort = 9090.86;\nconst insanePorts = [-1, 65537];\n\nfunction checkServerIsRunning(url, msg, ps, t) {\n if (!msg.toString().match(/Starting up/)) {\n    return;\n  }\n  t.pass('http-server was started');\n  request(url, (err, res) => {\n    if (!err && res.statusCode !== 500) {\n      t.pass('a successful request from the server was made');\n    } else {\n      t.fail('the server could not be reached');\n    }\n    ps.kill('SIGTERM');\n  });\n}\n\nfunction startServer(url, port, t) {\n  const ecstatic = spawn(process.execPath, [require.resolve('../bin/http-server')], {\n    env: {\n      PORT: String(port),\n    },\n  });\n\n  if (!insanePorts.includes(port)) {\n    ecstatic.stdout.on('data', (msg) => {\n      checkServerIsRunning(url, msg, ecstatic, t);\n    });\n  } else {\n    ecstatic.on('exit', (evt) => {\n         t.not(evt.code, 0, 'err:Running on invalid port not allowed');\n    });\n  }\n}\n\ntest('sane port', (t) => {\n  t.plan(2);\n  startServer(`http://127.0.0.1:${sanePort}`, sanePort, t);\n});\n\ntest('floating point port', (t) => {\n  t.plan(2);\n  startServer('http://127.0.0.1:9090', floatingPointPort, t);\n});\n\ninsanePorts.forEach((port) => {\n  test(`insane port: ${port}`, (t) => {\n    t.plan(1);\n    startServer('http://127.0.0.1:8000', port, t);\n  });\n});\n\n"
  },
  {
    "path": "test/proxy-all.test.js",
    "content": "'use strict';\n\nconst http = require('http');\nconst test = require('tap').test;\nconst path = require('path');\nconst fs = require('fs');\nconst request = require('request');\nconst { promisify } = require('util');\n\nconst httpServer = require('../lib/http-server');\n\nconst requestAsync = promisify(request);\nconst REQUEST_TIMEOUT = 5000;\n\nfunction listen(server) {\n  return new Promise((resolve, reject) => {\n    const underlying = server.server || server;\n    const onError = (err) => {\n      underlying.removeListener('error', onError);\n      reject(err);\n    };\n    underlying.once('error', onError);\n    server.listen(0, () => {\n      underlying.removeListener('error', onError);\n      resolve((underlying.address() || {}).port);\n    });\n  });\n}\n\nfunction requestWithTimeout(url) {\n  return requestAsync({ url, timeout: REQUEST_TIMEOUT });\n}\n\ntest('proxyAll requires a proxy target', (t) => {\n  t.throws(() => {\n    httpServer.createServer({ proxyAll: true });\n  }, /proxy/i, 'proxyAll without proxy should throw');\n  t.end();\n});\n\ntest('proxyAll routes every request through the proxy target', async (t) => {\n  t.plan(4);\n\n  const localRoot = path.join(__dirname, 'fixtures', 'proxy-all-local');\n  const remoteContent = fs.readFileSync(path.join(__dirname, 'fixtures', 'root', 'file'), 'utf8').trim();\n  const localContent = fs.readFileSync(path.join(localRoot, 'file'), 'utf8').trim();\n\n  const remoteServer = http.createServer((req, res) => {\n    if (req.url === '/file') {\n      res.writeHead(200, { 'Content-Type': 'text/plain' });\n      res.end(remoteContent);\n      return;\n    }\n\n    res.writeHead(404, { 'Content-Type': 'text/plain' });\n    res.end('File not found. :(');\n  });\n  const remotePort = await listen(remoteServer);\n\n  const proxyServer = httpServer.createServer({\n    root: localRoot,\n    proxy: `http://localhost:${remotePort}`,\n    proxyAll: true\n  });\n  const proxyPort = await listen(proxyServer);\n\n  t.teardown(() => {\n    proxyServer.close();\n    remoteServer.close();\n  });\n\n  const proxied = await requestWithTimeout(`http://localhost:${proxyPort}/file`);\n  t.equal(proxied.body.trim(), remoteContent, 'response matches proxy target');\n  t.notSame(proxied.body.trim(), localContent, 'local files are ignored when proxyAll is set');\n\n  const missing = await requestWithTimeout(`http://localhost:${proxyPort}/does-not-exist`);\n  t.equal(missing.statusCode, 404, 'status code comes from proxy target');\n  t.match(missing.body, /file not found/i, 'body matches proxy response');\n});\n"
  },
  {
    "path": "test/proxy-config.test.js",
    "content": "const test = require('tap').test\nconst path = require('path')\nconst fs = require('fs')\nconst request = require('request')\nconst httpServer = require('../lib/http-server')\nconst promisify = require('util').promisify\n\nconst requestAsync = promisify(request)\nconst fsReadFile = promisify(fs.readFile)\n\n// Prevent errors from being swallowed\nprocess.on('uncaughtException', console.error)\n\nconst root = path.join(__dirname, 'fixtures', 'root')\nconst httpsOpts = {\n  key: path.join(__dirname, 'fixtures', 'https', 'agent2-key.pem'),\n  cert: path.join(__dirname, 'fixtures', 'https', 'agent2-cert.pem')\n}\n\nconst proxyConfigTest = {\n  \"/rewrite/**\": {\n    \"target\": \"http://localhost:8082\",\n    \"pathRewrite\": {\n      \"^/rewrite\": \"\"\n    }\n  }\n}\n\n// Tests are grouped into those which can run together. The groups are given\n// their own port to run on and live inside a Promise. Tests are done when all\n// Promise test groups complete.\ntest('proxy config', (t) => {\n  new Promise((resolve) => {\n    const server = httpServer.createServer({\n      root,\n      robots: true,\n      headers: {\n        'Access-Control-Allow-Origin': '*',\n        'Access-Control-Allow-Credentials': 'true'\n      },\n      cors: true,\n      corsHeaders: 'X-Test',\n      ext: true,\n      brotli: true,\n      gzip: true\n    })\n    // TODO #723 we should use portfinder\n    server.listen(8082, async () => {\n      try {\n\n        // Another server proxies 8083 to 8082\n        const proxyServer = httpServer.createServer({\n          root,\n          //tls: true,\n          //https: httpsOpts,\n          proxyConfig: proxyConfigTest\n        })\n\n        await new Promise((resolve) => {\n          proxyServer.listen(8083, async () => {\n            try {\n              // Serve files from proxy root\n              await requestAsync('http://localhost:8083/file', { rejectUnauthorized: false }).then(async (res) => {\n                t.ok(res)\n                t.equal(res.statusCode, 200)\n\n                // File content matches\n                const fileData = await fsReadFile(path.join(root, 'file'), 'utf8')\n                t.equal(res.body.trim(), fileData.trim(), 'none proxied file content matches')\n              }).catch(err => t.fail(err.toString()))\n\n              // Serve files from proxy with rewrite\n              await requestAsync('http://localhost:8083/rewrite/file', { rejectUnauthorized: false }).then(async (res) => {\n                t.ok(res)\n                t.equal(res.statusCode, 200)\n\n                // File content matches\n                const fileData = await fsReadFile(path.join(root, 'file'), 'utf8')\n                t.equal(res.body.trim(), fileData.trim(), 'proxied file content matches')\n              }).catch(err => t.fail(err.toString()))\n            } catch (err) {\n              t.fail(err.toString())\n            } finally {\n              proxyServer.close()\n              resolve()\n            }\n          })\n        })\n\n      } catch (err) {\n        t.fail(err.toString())\n      } finally {\n        server.close()\n        resolve()\n      }\n    })\n  })\n    .then(() => t.end())\n    .catch(err => {\n      t.fail(err.toString())\n      t.end()\n    })\n})\n"
  },
  {
    "path": "test/proxy-options.test.js",
    "content": "const test = require('tap').test\nconst path = require('path')\nconst fs = require('fs')\nconst request = require('request')\nconst httpServer = require('../lib/http-server')\nconst promisify = require('util').promisify\n\nconst requestAsync = promisify(request)\nconst fsReadFile = promisify(fs.readFile)\n\n// Prevent errors from being swallowed\nprocess.on('uncaughtException', console.error)\n\nconst root = path.join(__dirname, 'fixtures', 'root')\nconst httpsOpts = {\n  key: path.join(__dirname, 'fixtures', 'https', 'agent2-key.pem'),\n  cert: path.join(__dirname, 'fixtures', 'https', 'agent2-cert.pem')\n}\n\n// Tests are grouped into those which can run together. The groups are given\n// their own port to run on and live inside a Promise. Tests are done when all\n// Promise test groups complete.\ntest('proxy options', (t) => {\n  new Promise((resolve) => {\n    const server = httpServer.createServer({\n      root,\n      robots: true,\n      headers: {\n        'Access-Control-Allow-Origin': '*',\n        'Access-Control-Allow-Credentials': 'true'\n      },\n      cors: true,\n      corsHeaders: 'X-Test',\n      ext: true,\n      brotli: true,\n      gzip: true\n    })\n    server.listen(0, async () => {\n      const port = server.address().port;\n      try {\n\n        // Another server proxies to the main server\n        const proxyServer = httpServer.createServer({\n          proxy: `http://localhost:${port}`,\n          root: path.join(__dirname, 'fixtures'),\n          tls: true,\n          https: httpsOpts,\n          proxyOptions: {\n            secure: false\n          }\n        })\n\n        await new Promise((resolve) => {\n          proxyServer.listen(0, async () => {\n            const proxyPort = proxyServer.address().port;\n            try {\n              // Serve files from proxy root\n              await requestAsync(`https://localhost:${proxyPort}/root/file`, { rejectUnauthorized: false }).then(async (res) => {\n                t.ok(res)\n                t.equal(res.statusCode, 200)\n\n                // File content matches\n                const fileData = await fsReadFile(path.join(root, 'file'), 'utf8')\n                t.equal(res.body.trim(), fileData.trim(), 'proxied root file content matches')\n              }).catch(err => t.fail(err.toString()))\n\n              // Proxy fallback\n              await requestAsync(`https://localhost:${proxyPort}/file`, { rejectUnauthorized: false }).then(async (res) => {\n                t.ok(res)\n                t.equal(res.statusCode, 200)\n\n                // File content matches\n                const fileData = await fsReadFile(path.join(root, 'file'), 'utf8')\n                t.equal(res.body.trim(), fileData.trim(), 'proxy fallback root file content matches')\n              }).catch(err => t.fail(err.toString()))\n            } catch (err) {\n              t.fail(err.toString())\n            } finally {\n              proxyServer.close()\n              resolve()\n            }\n          })\n        })\n\n      } catch (err) {\n        t.fail(err.toString())\n      } finally {\n        server.close()\n        resolve()\n      }\n    })\n  })\n    .then(() => t.end())\n    .catch(err => {\n      t.fail(err.toString())\n      t.end()\n    })\n})\n"
  },
  {
    "path": "test/public/404.html",
    "content": "<h1>404</h1>\n"
  },
  {
    "path": "test/public/a.txt",
    "content": "A!!!\n"
  },
  {
    "path": "test/public/another-subdir/scripts.js",
    "content": "var foo = bar;\n"
  },
  {
    "path": "test/public/b.txt",
    "content": "B!!!\n"
  },
  {
    "path": "test/public/brotli/fake_ecstatic",
    "content": "ecstatic\n"
  },
  {
    "path": "test/public/brotli/index.html",
    "content": "brotli, but I'm not compressed!!!\n"
  },
  {
    "path": "test/public/brotli/index.html.br",
    "content": "\u000b\nbrotli, compressed!!\n\u0003"
  },
  {
    "path": "test/public/brotli/not_actually_brotli.br",
    "content": "You've been duped! This is not compressed!\n"
  },
  {
    "path": "test/public/brotli/real_ecstatic",
    "content": "ecstatic\n"
  },
  {
    "path": "test/public/brotli/real_ecstatic.br",
    "content": "\u000b\u0004ecstatic\n\u0003"
  },
  {
    "path": "test/public/c.js",
    "content": "console.log('C!!!');\n"
  },
  {
    "path": "test/public/charset/arabic.html",
    "content": "<!DOCTYPE html>\n<html lang=\"ar\">\n  <head>\n    <meta charset=\"ISO-8859-6\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title> HTML</title>\n  </head>\n\n  <body>\n    <h1>Kokoro - Natsume Soseki</h1>\n    <h2> </h2>\n    <h3></h3>\n    <p>\n           .        \n         .         .\n                     \n      \"\".         .    \n        .\n    </p>\n  </body>\n</html>\n"
  },
  {
    "path": "test/public/charset/shift_jis.html",
    "content": "<!DOCTYPE html>\n<html lang=\"ja\">\n  <head>\n    <meta charset=\"shift_jis\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>{HTMLt@C</title>\n  </head>\n\n  <body>\n    <h1> - Ėڟ</h1>\n    <h2>@搶Ǝ</h2>\n    <h3></h3>\n    <p>\n      ͂̐lɐ搶ƌĂłB炱ł搶ƏŖ{͑łȂB͐Ԃ݂鉓ƂA̕ɂƂĎRłB͂̐l̋LĂыNƂɁAu搶vƂȂBMĂS͓łB悻悻Ȃǂ͂ƂĂgCɂȂȂB\n    </p>\n  </body>\n</html>\n"
  },
  {
    "path": "test/public/compress/foo.js",
    "content": "exports.foo = \"baz\";\n"
  },
  {
    "path": "test/public/compress/foo_2.js",
    "content": "exports.foo = \"baz\";\n"
  },
  {
    "path": "test/public/curimit@gmail.com (40%)/index.html",
    "content": "index!!!\n"
  },
  {
    "path": "test/public/custom_mime_type.opml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<?xml-stylesheet type=\"text/xsl\" href=\"/hyperscope/src/client/lib/hs/xslt/hyperscope.xsl\"?>\n<opml xmlns:hs=\"http://www.hyperscope.org/hyperscope/opml/public/2006/05/09\" hs:version=\"1.0\" version=\"2.0\">\n  <head hs:left-label-delim=\"\" hs:right-label-delim=\"\" hs:sidCount=\"749\">\n    <dateCreated>Tue 9 May 2006 06:44:27 GMT+00:00</dateCreated>\n    <ownerName>BKN</ownerName>\n    <dateModified>Sat 20 May 2006 00:17:25 GMT+00:00</dateModified>\n  </head>\n  <body>\n    <outline text=\"This node is from transclude.opml; the node below is transcluded from another document to illustrate recursive transclusions; it is actually from a remote, third-party webserver:\"/>\n    <outline type=\"include\" url=\"http://radio.weblogs.com/0001000/gems/publications.opml#1D\" hs:include-type=\"branch\" text=\"INCLUDE BRANCH http://radio.weblogs.com/0001000/gems/publications.opml#1D\"/>\n  </body>\n</opml>\n"
  },
  {
    "path": "test/public/custom_mime_type.types",
    "content": "# This file is an example of the Apache .types file format for describing mime-types.\n# Other example: http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types\napplication/foo                 opml\n"
  },
  {
    "path": "test/public/d.js",
    "content": "d.js\n"
  },
  {
    "path": "test/public/dir-overrides-404/404.html",
    "content": "404file"
  },
  {
    "path": "test/public/dir-overrides-404/directory/file.txt",
    "content": ""
  },
  {
    "path": "test/public/e.js",
    "content": "console.log('π!!!');\n"
  },
  {
    "path": "test/public/f_f",
    "content": "f!!\n"
  },
  {
    "path": "test/public/gzip/fake_ecstatic",
    "content": "ecstatic\n"
  },
  {
    "path": "test/public/gzip/index.html",
    "content": "gzip!!\n"
  },
  {
    "path": "test/public/gzip/real_ecstatic",
    "content": "ecstatic\n"
  },
  {
    "path": "test/public/show-dir$$href_encoding$$/aname+aplus.txt",
    "content": "It should generate an href of <a href=\"/show-dir-href-encoding/aname+aplus.txt\">..\n\nhttp://stackoverflow.com/a/1006074/147079\nhttp://tools.ietf.org/html/rfc1738\n"
  },
  {
    "path": "test/public/subdir/app.wasm",
    "content": "Fake WebAssemble file\n"
  },
  {
    "path": "test/public/subdir/e.html",
    "content": "<b>e!!</b>\n"
  },
  {
    "path": "test/public/subdir/index.html",
    "content": "index!!!\n"
  },
  {
    "path": "test/public/subdir_with space/file_with space.html",
    "content": "<b>spacious!!</b>\n"
  },
  {
    "path": "test/public/subdir_with space/index.html",
    "content": "index :)\n"
  },
  {
    "path": "test/public/中文/檔案.html",
    "content": "<b>file!!</b>\n"
  },
  {
    "path": "test/range.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst ecstatic = require('../lib/core');\nconst http = require('http');\nconst request = require('request');\nconst eol = require('eol');\nconst fs = require('fs');\nconst path = require('path');\n\ntest('range', (t) => {\n  t.plan(4);\n  const server = http.createServer(ecstatic(`${__dirname}/public/subdir`));\n  t.on('end', () => { server.close(); });\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    const opts = {\n      uri: `http://localhost:${port}/e.html`,\n      headers: { range: '3-5' },\n    };\n    request.get(opts, (err, res, body) => {\n      t.error(err);\n      t.equal(res.statusCode, 206, 'partial content status code');\n      t.equal(body, 'e!!');\n      t.equal(parseInt(res.headers['content-length'], 10), body.length);\n    });\n  });\n});\n\ntest('range past the end', (t) => {\n  t.plan(4);\n  const server = http.createServer(ecstatic(`${__dirname}/public/subdir`));\n  t.on('end', () => { server.close(); });\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    const opts = {\n      uri: `http://localhost:${port}/e.html`,\n      headers: { range: '3-500' },\n    };\n    request.get(opts, (err, res, body) => {\n      t.error(err);\n      t.equal(res.statusCode, 206, 'partial content status code');\n      t.equal(eol.lf(body), 'e!!</b>\\n');\n      t.equal(parseInt(res.headers['content-length'], 10), body.length);\n    });\n  });\n});\n\ntest('range starts beyond the end', (t) => {\n  t.plan(4);\n  const server = http.createServer(ecstatic(`${__dirname}/public/subdir`));\n  t.on('end', () => { server.close(); });\n\n  const filePath = path.join(__dirname, 'public/subdir/e.html');\n  const fileSize = fs.statSync(filePath).size;\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    const opts = {\n      uri: `http://localhost:${port}/e.html`,\n      headers: { range: '500-' },\n    };\n    request.get(opts, (err, res, body) => {\n      t.error(err);\n      t.equal(res.statusCode, 416, 'range error status code');\n      t.equal(res.headers['content-range'], `bytes */${fileSize}`);\n      t.equal(body, 'Requested range not satisfiable');\n    });\n  });\n});\n\ntest('NaN range', (t) => {\n  t.plan(4);\n  const server = http.createServer(ecstatic(`${__dirname}/public/subdir`));\n  t.on('end', () => { server.close(); });\n\n  const filePath = path.join(__dirname, 'public/subdir/e.html');\n  const fileSize = fs.statSync(filePath).size;\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    const opts = {\n      uri: `http://localhost:${port}/e.html`,\n      headers: { range: 'abc-def' },\n    };\n    request.get(opts, (err, res, body) => {\n      t.error(err);\n      t.equal(res.statusCode, 416, 'range error status code');\n      t.equal(res.headers['content-range'], `bytes */${fileSize}`);\n      t.equal(body, 'Requested range not satisfiable');\n    });\n  });\n});\n\ntest('flipped range', (t) => {\n  t.plan(4);\n  const server = http.createServer(ecstatic(`${__dirname}/public/subdir`));\n  t.on('end', () => { server.close(); });\n\n  const filePath = path.join(__dirname, 'public/subdir/e.html');\n  const fileSize = fs.statSync(filePath).size;\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    const opts = {\n      uri: `http://localhost:${port}/e.html`,\n      headers: { range: '333-222' },\n    };\n    request.get(opts, (err, res, body) => {\n      t.error(err);\n      t.equal(res.statusCode, 416, 'range error status code');\n      t.equal(res.headers['content-range'], `bytes */${fileSize}`);\n      t.equal(body, 'Requested range not satisfiable');\n    });\n  });\n});\n\ntest('partial range', (t) => {\n  // 1 test is platform depedent \"res.headers['content-range']\"\n  t.plan(5);\n  const server = http.createServer(ecstatic(`${__dirname}/public/subdir`));\n  t.on('end', () => { server.close(); });\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    const opts = {\n      uri: `http://localhost:${port}/e.html`,\n      headers: { range: '3-' },\n    };\n    request.get(opts, (err, res, body) => {\n      t.error(err);\n      t.equal(res.statusCode, 206, 'partial content status code');\n      t.equal(eol.lf(body), 'e!!</b>\\n');\n      t.equal(parseInt(res.headers['content-length'], 10), body.length);\n      if (eol.lf(body) != body) { // on Windows, depending on Git settings\n        t.equal(res.headers['content-range'], 'bytes 3-11/12');\n      } else {\n        t.equal(res.headers['content-range'], 'bytes 3-10/11');\n      }\n    });\n  });\n});\n\ntest('include last-modified, etag and cache-control headers', (t) => {\n  t.plan(4);\n  const server = http.createServer(ecstatic(`${__dirname}/public/subdir`));\n  t.on('end', () => { server.close(); });\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    const opts = {\n      uri: `http://localhost:${port}/e.html`,\n      headers: { range: '3-5' },\n    };\n    request.get(opts, (err, res) => {\n      t.error(err);\n      t.ok(res.headers['cache-control']);\n      t.ok(res.headers['last-modified']);\n      t.ok(res.headers.etag);\n    });\n  });\n});\n"
  },
  {
    "path": "test/showdir-href-encoding.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst ecstatic = require('../lib/core');\nconst http = require('http');\nconst request = require('request');\nconst path = require('path');\n\nconst root = `${__dirname}/public`;\nconst baseDir = 'base';\n\ntest('url encoding in href', (t) => {\n  const server = http.createServer(\n    ecstatic({\n      root,\n      baseDir,\n      showDir: true,\n      autoIndex: false,\n    })\n  );\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    const uri = `http://localhost:${port}${path.join('/', baseDir, 'show-dir%24%24href_encoding%24%24')}`;\n    request.get({\n      uri,\n    }, (err, res, body) => {\n      t.match(body, /href=\"\\.\\/aname%2Baplus.txt\"/, 'We found the right href');\n      server.close();\n      t.end();\n    });\n  });\n});\n"
  },
  {
    "path": "test/showdir-search-encoding.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst ecstatic = require('../lib/core');\nconst http = require('http');\nconst request = require('request');\nconst path = require('path');\n\nconst root = `${__dirname}/public`;\nconst baseDir = 'base';\n\ntest('directory listing with query string specified', (t) => {\n  require('portfinder').getPort((err, port) => {\n    const uri = `http://localhost:${port}${path.join('/', baseDir, '?a=1&b=2')}`;\n\n    const server = http.createServer(\n      ecstatic({\n        root,\n        baseDir,\n        showDir: true,\n        autoIndex: false,\n      })\n    );\n\n    server.listen(port, () => {\n      request.get({\n        uri,\n      }, (err, res, body) => {\n        t.match(body, /href=\"\\.\\/subdir\\/\\?a=1&#x26;b=2\"/, 'We found the encoded href');\n        t.notMatch(body, /a=1&b=2/, 'We didn\\'t find the unencoded query string value');\n        server.close();\n        t.end();\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/showdir-with-spaces.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst ecstatic = require('../lib/core');\nconst http = require('http');\nconst request = require('request');\nconst path = require('path');\n\nconst root = `${__dirname}/public`;\nconst baseDir = 'base';\n\ntest('directory listing when directory name contains spaces', (t) => {\n  require('portfinder').getPort((err, port) => {\n    const uri = `http://localhost:${port}${path.join('/', baseDir, 'subdir_with%20space')}`;\n\n    const server = http.createServer(\n      ecstatic({\n        root,\n        baseDir,\n        showDir: true,\n        autoIndex: false,\n      })\n    );\n\n    server.listen(port, () => {\n      request.get({\n        uri,\n      }, (err, res, body) => {\n        t.ok(/href=\"\\.\\/index.html\"/.test(body), 'We found the right href');\n        server.close();\n        t.end();\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/timeout.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst http = require('http');\nconst httpServer = require('../lib/http-server');\nconst path = require('path');\n\nconst root = path.join(__dirname, 'fixtures', 'root');\n\ntest('timeout: default behavior (no timeout specified)', (t) => {\n  t.plan(2);\n\n  const server = httpServer.createServer({\n    root\n  });\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    // Verify server was created successfully\n    t.ok(server, 'server created without timeout option');\n    \n    // Check that server has a timeout property (Node.js default is usually 120000ms = 2 minutes)\n    // But we're not setting it, so it should use Node.js default\n    t.ok(server.server, 'server has underlying server instance');\n    \n    server.close();\n    t.end();\n  });\n});\n\ntest('timeout: custom timeout value in seconds', (t) => {\n  t.plan(2);\n\n  const timeoutSeconds = 60;\n  const server = httpServer.createServer({\n    root,\n    timeout: timeoutSeconds\n  });\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    t.ok(server, 'server created with custom timeout');\n    \n    // Verify timeout was set on the underlying server\n    // Note: Node.js setTimeout expects milliseconds, but we're passing seconds\n    // This test verifies the current behavior (may need adjustment after PR merge)\n    const underlyingServer = server.server;\n    t.ok(underlyingServer, 'server has underlying server instance');\n    \n    server.close();\n    t.end();\n  });\n});\n\ntest('timeout: disabled timeout (0)', (t) => {\n  t.plan(2);\n\n  const server = httpServer.createServer({\n    root,\n    timeout: 0\n  });\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    t.ok(server, 'server created with timeout disabled');\n    \n    const underlyingServer = server.server;\n    t.ok(underlyingServer, 'server has underlying server instance');\n    \n    server.close();\n    t.end();\n  });\n});\n\ntest('timeout: connection actually times out after specified duration', (t) => {\n  t.plan(2);\n\n  // Use a short timeout for testing (1 second = 1000ms)\n  // Note: This assumes timeout is in milliseconds. If PR converts to milliseconds,\n  // we may need to adjust this test\n  const timeoutMs = 1000;\n  const server = httpServer.createServer({\n    root,\n    timeout: timeoutMs\n  });\n\n  let timeoutFired = false;\n\n  server.server.on('timeout', (socket) => {\n    if (!timeoutFired) {\n      timeoutFired = true;\n      t.pass('timeout event fired');\n      socket.destroy();\n      server.close();\n      t.pass('timeout handling works');\n      t.end();\n    }\n  });\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    \n    // Create a connection but don't send any data\n    const socket = require('net').createConnection(port, 'localhost', () => {\n      // Don't send any data - keep connection idle to trigger timeout\n    });\n\n    socket.on('error', () => {\n      // Connection errors are expected when timeout fires\n    });\n\n    // Safety timeout in case the server timeout doesn't work\n    setTimeout(() => {\n      if (!timeoutFired) {\n        t.fail('server timeout did not fire within expected time');\n        socket.destroy();\n        server.close();\n        t.end();\n      }\n    }, timeoutMs + 2000);\n  });\n});\n\ntest('timeout: server handles requests normally with timeout set', (t) => {\n  t.plan(3);\n\n  const server = httpServer.createServer({\n    root,\n    timeout: 60 // 60 seconds\n  });\n\n  server.listen(0, () => {\n    const port = server.address().port;\n    t.ok(server, 'server created with timeout option');\n    \n    const req = http.get(`http://localhost:${port}/file`, (res) => {\n      t.equal(res.statusCode, 200, 'request succeeds with timeout set');\n      \n      let body = '';\n      res.on('data', (chunk) => {\n        body += chunk;\n      });\n      \n      res.on('end', () => {\n        t.ok(body.length > 0, 'response body received');\n        server.close();\n        t.end();\n      });\n    });\n\n    req.on('error', (err) => {\n      t.fail(`request failed: ${err.message}`);\n      server.close();\n      t.end();\n    });\n  });\n});\n\ntest('timeout: multiple timeout values', (t) => {\n  t.plan(4);\n\n  const testCases = [\n    { timeout: 30, description: '30 seconds' },\n    { timeout: 120, description: '120 seconds (default)' },\n    { timeout: 300, description: '300 seconds' },\n    { timeout: 0, description: 'disabled (0)' }\n  ];\n\n  let completed = 0;\n  const total = testCases.length;\n\n  testCases.forEach((testCase) => {\n    const server = httpServer.createServer({\n      root,\n      timeout: testCase.timeout\n    });\n\n    server.listen(0, () => {\n      const port = server.address().port;\n      t.ok(server, `server created with timeout ${testCase.description}`);\n      \n      server.close();\n      completed++;\n      if (completed === total) {\n        t.end();\n      }\n    });\n  });\n});\n\n"
  },
  {
    "path": "test/trailing-slash.test.js",
    "content": "'use strict';\n\nconst test = require('tap').test;\nconst ecstatic = require('../lib/core');\nconst http = require('http');\nconst request = require('request');\n\ntest('should not add trailing slash when showDir and autoIndex are off', (t) => {\n  t.plan(3);\n  const server = http.createServer(\n    ecstatic({\n      root: `${__dirname}/public`,\n      autoIndex: false,\n      showDir: false,\n    })\n  );\n  t.on('end', () => { server.close(); });\n  server.listen(0, () => {\n    const port = server.address().port;\n    request.get(`http://localhost:${port}/subdir`, (err, res) => {\n      t.error(err);\n      t.equal(res.statusCode, 404);\n      t.equal(res.body, 'File not found. :(');\n    });\n  });\n});\n"
  },
  {
    "path": "test/websocket-proxy.test.js",
    "content": "const test = require('tap').test\nconst path = require('path')\nconst http = require('http')\nconst httpServer = require('../lib/http-server')\nconst WebSocket = require('ws')\n\n// Prevent errors from being swallowed\nprocess.on('uncaughtException', console.error)\n\ntest('websocket proxy functionality', (t) => {\n    new Promise((resolve) => {\n        // Create a target server that will handle websocket connections\n        const targetServer = http.createServer()\n        const targetWss = new WebSocket.Server({ server: targetServer })\n\n        targetWss.on('connection', (ws) => {\n            ws.on('message', (message) => {\n                // Echo the message back\n                ws.send(`Echo: ${message}`)\n            })\n        })\n\n        targetServer.listen(0, () => {\n            const targetPort = targetServer.address().port\n            const targetUrl = `http://localhost:${targetPort}`\n\n            // Create http-server with websocket proxy enabled\n            const proxyServer = httpServer.createServer({\n                proxy: targetUrl,\n                websocket: true,\n                root: path.join(__dirname, 'fixtures')\n            })\n\n            proxyServer.listen(0, async () => {\n                const proxyPort = proxyServer.server.address().port\n                const proxyUrl = `http://localhost:${proxyPort}`\n\n                try {\n                    // Test 1: Verify websocket proxy is enabled when both proxy and websocket options are set\n                    t.ok(proxyServer.server.listeners('upgrade').length > 0, 'upgrade event listener should be registered')\n\n                    // Test 2: Test websocket connection through proxy\n                    await new Promise((resolve, reject) => {\n                        const ws = new WebSocket(`ws://localhost:${proxyPort}`)\n\n                        ws.on('open', () => {\n                            t.pass('websocket connection should be established through proxy')\n\n                            // Send a test message\n                            ws.send('Hello WebSocket!')\n                        })\n\n                        ws.on('message', (data) => {\n                            t.equal(data.toString(), 'Echo: Hello WebSocket!', 'should receive echoed message')\n                            ws.close()\n                        })\n\n                        ws.on('close', () => {\n                            t.pass('websocket connection should close properly')\n                            resolve()\n                        })\n\n                        ws.on('error', (err) => {\n                            t.fail(`websocket error: ${err.message}`)\n                            reject(err)\n                        })\n\n                        // Set timeout to prevent hanging\n                        setTimeout(() => {\n                            ws.close()\n                            reject(new Error('WebSocket test timeout'))\n                        }, 5000)\n                    })\n\n                } catch (err) {\n                    t.fail(`websocket proxy test failed: ${err.message}`)\n                } finally {\n                    proxyServer.close()\n                    targetServer.close()\n                    resolve()\n                }\n            })\n        })\n    })\n        .then(() => t.end())\n        .catch(err => {\n            t.fail(err.toString())\n            t.end()\n        })\n})\n\ntest('websocket proxy without proxy configuration', (t) => {\n    new Promise((resolve) => {\n        // Create http-server with websocket enabled but no proxy\n        const server = httpServer.createServer({\n            websocket: true,\n            root: path.join(__dirname, 'fixtures')\n        })\n\n        server.listen(0, () => {\n            try {\n                // Test: Verify no upgrade event listener is registered when proxy is not set\n                t.equal(server.server.listeners('upgrade').length, 0, 'no upgrade event listener should be registered when proxy is not set')\n                t.pass('websocket option should be ignored when proxy is not configured')\n            } catch (err) {\n                t.fail(`test failed: ${err.message}`)\n            } finally {\n                server.close()\n                resolve()\n            }\n        })\n    })\n        .then(() => t.end())\n        .catch(err => {\n            t.fail(err.toString())\n            t.end()\n        })\n})\n\ntest('ensure websocket proxy is not enabled when \\'websocket\\' is not set', (t) => {\n    new Promise((resolve) => {\n        // Create a target server that will handle websocket connections\n        const targetServer = http.createServer()\n        const targetWss = new WebSocket.Server({ server: targetServer })\n\n        targetWss.on('connection', (ws) => {\n            ws.on('message', (message) => {\n                // Echo the message back\n                ws.send(`Echo: ${message}`)\n            })\n        })\n\n        targetServer.listen(0, () => {\n            const targetPort = targetServer.address().port\n            const targetUrl = `http://localhost:${targetPort}`\n\n            const proxyServer = httpServer.createServer({\n                proxy: targetUrl,\n                root: path.join(__dirname, 'fixtures')\n            })\n            try {\n                t.equal(proxyServer.server.listeners('upgrade').length, 0, 'no upgrade event listener should be registered when websocket is not set')\n            } catch (err) {\n                t.fail(`test failed: ${err.message}`)\n            } finally {\n                proxyServer.close()\n                targetServer.close()\n                resolve()\n            }\n        })\n    })\n        .then(() => t.end())\n        .catch(err => {\n            t.fail(err.toString())\n            t.end()\n        })\n});\n\ntest('websocket proxy error handling', (t) => {\n    new Promise((resolve) => {\n        // Create http-server with invalid proxy target\n        const proxyServer = httpServer.createServer({\n            proxy: 'http://localhost:99999', // Invalid port\n            websocket: true,\n            root: path.join(__dirname, 'fixtures')\n        })\n\n        proxyServer.listen(0, async () => {\n            const proxyPort = proxyServer.server.address().port\n\n            try {\n                // Test: Verify websocket proxy handles connection errors gracefully\n                t.ok(proxyServer.server.listeners('upgrade').length > 0, 'upgrade event listener should be registered even with invalid proxy')\n\n                // Test websocket connection to invalid proxy target\n                await new Promise((resolve, reject) => {\n                    const ws = new WebSocket(`ws://localhost:${proxyPort}`)\n\n                    ws.on('open', () => {\n                        t.fail('websocket should not connect to invalid proxy target')\n                        ws.close()\n                        resolve()\n                    })\n\n                    ws.on('error', (err) => {\n                        t.pass('websocket should error when proxy target is invalid')\n                        resolve() // This is expected\n                    })\n\n                    ws.on('close', () => {\n                        t.pass('websocket should close on error')\n                        resolve()\n                    })\n\n                    setTimeout(() => {\n                        ws.close()\n                        resolve() // Timeout is acceptable for this test\n                    }, 2000)\n                })\n\n            } catch (err) {\n                t.fail(`websocket proxy error handling test failed: ${err.message}`)\n            } finally {\n                proxyServer.close()\n                resolve()\n            }\n        })\n    })\n        .then(() => t.end())\n        .catch(err => {\n            t.fail(err.toString())\n            t.end()\n        })\n})"
  }
]