Repository: http-party/http-server Branch: master Commit: 0d3b7bb5b6e8 Files: 125 Total size: 267.5 KB Directory structure: gitextract_g8irr2o3/ ├── .dockerignore ├── .eslintrc.json ├── .github/ │ ├── CONTRIBUTING.md │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug-report.md │ │ └── feature-request.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── release-drafter.yml │ └── workflows/ │ ├── node.js.yml │ ├── release-drafter.yml │ └── stale.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── bin/ │ └── http-server ├── doc/ │ └── http-server.1 ├── lib/ │ ├── core/ │ │ ├── aliases.json │ │ ├── etag.js │ │ ├── index.js │ │ ├── opts.js │ │ ├── show-dir/ │ │ │ ├── icons.json │ │ │ ├── index.js │ │ │ ├── last-modified-to-string.js │ │ │ ├── perms-to-string.js │ │ │ ├── size-to-string.js │ │ │ ├── sort-files.js │ │ │ └── styles.js │ │ └── status-handlers.js │ ├── http-server.js │ └── shims/ │ └── https-server-shim.js ├── package.json ├── public/ │ ├── 404.html │ └── index.html └── test/ ├── 304.test.js ├── accept-encoding.test.js ├── allowed-hosts.test.js ├── cache.test.js ├── check-headers.js ├── cli.test.js ├── compression.test.js ├── content-type.test.js ├── coop.test.js ├── core-error.test.js ├── core.test.js ├── cors.test.js ├── custom-content-type-file-secret.test.js ├── custom-content-type-file.test.js ├── custom-content-type.test.js ├── default-default-ext.test.js ├── dir-overrides-404.test.js ├── enotdir.test.js ├── escaping.test.js ├── express-error.test.js ├── express.test.js ├── fixtures/ │ ├── common-cases-error.js │ ├── common-cases.js │ ├── custom_mime_type.types │ ├── https/ │ │ ├── agent2-cert.pem │ │ └── agent2-key.pem │ ├── proxy-all-local/ │ │ ├── does-not-exist │ │ └── file │ └── root/ │ ├── canYouSeeMe │ ├── compression/ │ │ ├── index.html │ │ └── index.html.br │ ├── file │ └── htmlButNot ├── force-content-encoding.test.js ├── headers.test.js ├── illegal-access-date.test.js ├── localhost.test.js ├── main.test.js ├── malformed-dir.test.js ├── malformed.test.js ├── mime.test.js ├── network-interfaces.test.js ├── pathname-encoding.test.js ├── private-network-access.test.js ├── process-env-port.test.js ├── proxy-all.test.js ├── proxy-config.test.js ├── proxy-options.test.js ├── public/ │ ├── 404.html │ ├── a.txt │ ├── another-subdir/ │ │ └── scripts.js │ ├── b.txt │ ├── brotli/ │ │ ├── fake_ecstatic │ │ ├── fake_ecstatic.br │ │ ├── index.html │ │ ├── index.html.br │ │ ├── not_actually_brotli.br │ │ ├── real_ecstatic │ │ └── real_ecstatic.br │ ├── c.js │ ├── charset/ │ │ ├── arabic.html │ │ └── shift_jis.html │ ├── compress/ │ │ ├── foo.js │ │ └── foo_2.js │ ├── curimit@gmail.com (40%)/ │ │ └── index.html │ ├── custom_mime_type.opml │ ├── custom_mime_type.types │ ├── d.js │ ├── dir-overrides-404/ │ │ ├── 404.html │ │ └── directory/ │ │ └── file.txt │ ├── e.js │ ├── f_f │ ├── gzip/ │ │ ├── fake_ecstatic │ │ ├── index.html │ │ └── real_ecstatic │ ├── show-dir$$href_encoding$$/ │ │ └── aname+aplus.txt │ ├── subdir/ │ │ ├── app.wasm │ │ ├── e.html │ │ └── index.html │ ├── subdir_with space/ │ │ ├── file_with space.html │ │ └── index.html │ └── 中文/ │ └── 檔案.html ├── range.test.js ├── showdir-href-encoding.test.js ├── showdir-search-encoding.test.js ├── showdir-with-spaces.test.js ├── timeout.test.js ├── trailing-slash.test.js └── websocket-proxy.test.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ node_modules yarn-error.log npm-debug.log Dockerfile .dockerignore .git .gitignore testdir/privatefile ================================================ FILE: .eslintrc.json ================================================ { "extends": "eslint-config-populist", "rules": { "strict": "warn", "indent": ["warn", 2], "valid-jsdoc": "warn", "no-undefined": "warn", "comma-dangle": "warn", "callback-return": ["warn", ["next"]] } } ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributing to http-server > Please read these guidelines before submitting an issue, filing a feature request, or contributing code. ## :bug: I Found a Bug Sorry! 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. Please include _as much detail as possible_ to help us reproduce and diagnose the bug. Most importantly: - Make use of the issue template! - Let us know _how_ you're running http-server (options, flags, environment, etc.) - Include your test code or file(s). If large, please provide a link to a repository or [gist](https://gist.github.com). - Please show code in JavaScript only (any version) If 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. ## :exclamation: Propose a Change Before 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. ### :shoe: Contributing Code: Step-by-Step Follow these steps to get going. 1. [Install the latest version of Node.js](https://nodejs.org/en/download). - 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. 1. Follow [Github's documentation](https://help.github.com/articles/fork-a-repo/) on setting up Git, forking and cloning. 1. Create a new branch in your fork, giving it a descriptive name 1. Execute `npm install` to install the prod and dev dependencies - Do not use `yarn install` for development, as it may not get the same package versions as other developers. 1. Make your changes and add them via `git add`. - **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 - Keep your PR focused. Don't fix multiple things at once, and don't upgrade dependencies unless necessary. 1. Before committing, run `npm test` - Tests will also run on your PR, but running them locally will let you catch problems ahead-of-time. 1. Commit your changes. - See [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/). - **Please do not use "Conventional Commits" style** 1. Push your changes to your fork. 1. 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. 1. Describe your changes in detail here, following the template. Once you're satisfied, submit the form. 1. Be patient while your PR is reviewed. This can take a while. We may request changes, but don't be afraid to question them. 1. 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. 1. 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. - **Please do not rebase and force-push**, it ruins the git history ## :angel: I Just Want To Help _Excellent._ Here's how: - **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). - **Wait--you write unit tests for _fun_?** A PR which increases coverage is unlikely to ever be turned down. ================================================ FILE: .github/FUNDING.yml ================================================ github: thornjad ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report.md ================================================ --- name: Bug report about: Create a report to help us improve --- #### Environment Versions 1. OS Type 1. Node version: `$ node --version` 1. http-server version: `$ http-server --version` #### Steps to reproduce 1. ... 2. ... 3. ... #### Expected result ... #### Actual result ... #### Other information ================================================ FILE: .github/ISSUE_TEMPLATE/feature-request.md ================================================ --- name: Feature request about: Suggest an idea for this project --- #### What's the problem this feature will solve? #### Describe the solution you'd like #### Alternative Solutions #### Additional context ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ##### Relevant issues ##### Contributor checklist - [ ] Provide tests for the changes (unless documentation-only) - [ ] Documented any new features, CLI switches, etc. (if applicable) - [ ] Server `--help` output - [ ] README.md - [ ] doc/http-server.1 (use the same format as other entries) - [ ] The pull request is being made against the `master` branch ##### Maintainer checklist - [ ] Assign a version triage tag - [ ] Approve tests if applicable ================================================ FILE: .github/release-drafter.yml ================================================ name-template: 'v$RESOLVED_VERSION' tag-template: 'v$RESOLVED_VERSION' change-template: '- $TITLE @$AUTHOR (#$NUMBER)' change-title-escapes: '\<*_&' categories: - title: 'Breaking changes' labels: - 'major version' - title: 'Features and enhancements' labels: - 'feature' - 'enhancement' - title: 'Bug Fixes' labels: - 'fix' - 'bug' - title: 'Other changes' labels: - 'dependencies' - 'documentation' exclude-labels: - "skip-changelog" - "maintenance" - "trivial" version-resolver: major: labels: - 'major version' minor: labels: - 'minor version' patch: labels: - 'patch version' default: patch template: | $CHANGES ================================================ FILE: .github/workflows/node.js.yml ================================================ # 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 # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions name: Node.js CI on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: name: Test runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: node-version: [20.x, 22.x, 24.x] os: [ubuntu-latest, macOS-latest, windows-latest] steps: - uses: actions/checkout@v4.2.2 - name: Use Node.js ${{ matrix.node-version }} on ${{ matrix.os }} uses: actions/setup-node@v4.1.0 with: node-version: ${{ matrix.node-version }} cache: 'npm' - run: npx npm@7 ci - run: npm test ================================================ FILE: .github/workflows/release-drafter.yml ================================================ name: release-drafter on: push: # branches to consider in the event; optional, defaults to all branches: - master jobs: update_release_draft: permissions: contents: write pull-requests: read if: github.repository == 'http-party/http-server' runs-on: ubuntu-latest steps: # Drafts your next release notes as pull requests are merged into master - uses: release-drafter/release-drafter@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/stale.yml ================================================ name: Mark stale issues and pull requests on: schedule: - cron: '25 12 * * *' jobs: stale: runs-on: ubuntu-latest permissions: issues: write pull-requests: write steps: - uses: actions/stale@v4.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 360 days-before-issue-stale: 180 days-before-pr-stale: 360 stale-issue-message: 'This issue has been inactive for 180 days' stale-pr-message: 'This pull request has been inactive for 360 days' stale-issue-label: 'stale' stale-pr-label: 'stale' exempt-issue-labels: 'no-stale' exempt-pr-labels: 'no-stale' exempt-all-milestones: true days-before-close: -1 ================================================ FILE: .gitignore ================================================ node_modules/ npm-debug.log* .nyc_*/ .dir-locals.el .DS_Store .httpserver* .tap ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at jademichael @ jmthornton.net. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: Dockerfile ================================================ FROM node:16-alpine VOLUME /public WORKDIR /srv/http-server COPY package.json package-lock.json ./ RUN npm install --production COPY . . EXPOSE 8080 ENTRYPOINT ["node", "./bin/http-server"] ================================================ FILE: LICENSE ================================================ Copyright (c) 2011-2026 Charlie Robbins, Marak Squires, Jade Michael Thornton and the Contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ [![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) [![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) [![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) # http-server: a simple static HTTP server `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. ![Example of running http-server](https://github.com/http-party/http-server/raw/master/screenshots/public.png) ## Installation: #### Running on-demand: Using `npx` you can run the script without installing it first: npx http-server [path] [options] #### Globally via `npm` npm install --global http-server This will install `http-server` globally so that it may be run from the command line anywhere. #### Globally via Homebrew brew install http-server #### As a dependency in your `npm` package: npm install http-server #### Using Docker Note: a public image is not provided currently, but you can build one yourself with the provided Dockerfile. 1. Create an image ``` docker build -t my-image . ``` 2. Run a container ``` docker run -p 8080:8080 -v "${pwd}:/public" my-image ``` In the example above we're serving the directory `./` (working directory). If you wanted to serve `./test` you'd replace `${pwd}` with `${pwd}/test`. ## Usage: http-server [path] [options] `[path]` defaults to `./public` if the folder exists, and `./` otherwise. *Now you can visit http://localhost:8080 to view your server* **Note:** Caching is on by default. Add `-c-1` as an option to disable caching. ## Available Options: | Command | Description | Defaults | | ------------- |-------------|-------------| |`-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 | |`-a` |Address to use |0.0.0.0| |`--base-dir` | Base path to serve files from | `/` | |`-d` |Show directory listings |`true` | |`-dir-overrides-404` | Whether `-d` should override magic `404.html` | `false` |`-i` | Display autoIndex | `true` | |`-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`| |`-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`| |`-e` or `--ext` |Default file extension if none supplied |`html` | |`-s` or `--silent` |Suppress log messages from output | | |`--coop` |Enable COOP via the `Cross-Origin-Opener-Policy` header | | |`--cors` |Enable CORS via the `Access-Control-Allow-Origin` header | | |`--private-network-access` |Enable Private Network Access via the `Access-Control-Allow-Private-Network` header | | |`--cors` | Enable CORS via the `Access-Control-Allow-Origin: *` header. Optionally provide comma-separated values to add to `Access-Control-Allow-Headers` | | |`-H` or `--header` |Add an extra response header (can be used several times) | | |`-o [path]` |Open browser window after starting the server. Optionally provide a URL path to open. e.g.: -o /other/dir/ | | |`-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` | |`-t` |Connection timeout in seconds, e.g. `-t60` for 1 minute. To disable timeout, use `-t0`.|`120` | |`-T` or `--title` |Custom title suffix for the terminal window. The title will be "http-server PORT [TITLE]".| | |`-U` or `--utc` |Use UTC time format in log messages.| | |`--log-ip` |Enable logging of the client's IP address |`false` | |`-P` or `--proxy` |Proxies all requests which can't be resolved locally to the given url. e.g.: -P http://someurl.com | | |`--proxy-options` |Pass proxy [options](https://github.com/http-party/node-http-proxy#options) using nested dotted objects. e.g.: --proxy-options.secure false | | |`--proxy-config` |Pass in `.json` configuration file or stringified JSON. e.g.: `./path/to/config.json` | | |`--proxy-all` |Forward every request to the proxy target instead of serving local files|`false`| |`--proxy-options` |Pass proxy [options](https://github.com/http-party/node-http-proxy#options) using nested dotted objects. e.g.: --proxy-options.secure false | |`--user` or `--username` |Username for basic authentication | | |`--password` |Password for basic authentication | | |`-S`, `--tls` or `--ssl` |Enable secure request serving with TLS/SSL (HTTPS)|`false`| |`-C` or `--cert` |Path to ssl cert file |`cert.pem` | |`-K` or `--key` |Path to ssl key file |`key.pem` | |`-r` or `--robots` | Automatically provide a /robots.txt (The content of which defaults to `User-agent: *\nDisallow: /`) | `false` | |`--no-dotfiles` |Do not show dotfiles| | |`--mimetypes` |Path to a .types file for custom mimetype definition| | |`--hide-permissions` |Do not show file permissions| | |`--allowed-hosts` |Comma-separated list of hosts allowed to access the server. e.g.: `--allowed-hosts localhost,example.com`| | |`-h` or `--help` |Print this list and exit. | | |`-v` or `--version`|Print the version and exit. | | | `--no-panic` | Don't print error stack in the console, put it in a log file | `false`| ## Magic Files - `index.html` will be served as the default file to any directory requests. - `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. ## Catch-all redirect To implement a catch-all redirect, use the index page itself as the proxy with: ``` http-server --proxy http://localhost:8080? ``` Note the `?` at the end of the proxy URL. Thanks to [@houston3](https://github.com/houston3) for this clever hack! ## TLS/SSL First, 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: ``` sh openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out cert.pem ``` You 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. This generates a cert-key pair and it will be valid for 3650 days (about 10 years). Then you need to run the server with `-S` for enabling SSL and `-C` for your certificate file. ``` sh http-server -S -C cert.pem ``` If 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) e.g. `openssl req -newkey rsa:2048 -passout pass:foobar -keyout key.pem -x509 -days 365 -out cert.pem` For security reasons, the passphrase will only be read from the `NODE_HTTP_SERVER_SSL_PASSPHRASE` environment variable. This is what should be output if successful: ``` sh Starting up http-server, serving ./ through https http-server settings: COOP: disabled CORS: disabled Cache: 3600 seconds Connection Timeout: 120 seconds Directory Listings: visible AutoIndex: visible Serve GZIP Files: false Serve Brotli Files: false Default File Extension: none Available on: https://127.0.0.1:8080 https://192.168.1.101:8080 https://192.168.1.104:8080 Hit CTRL-C to stop the server ``` # Development Checkout this repository locally, then: ```sh $ npm i $ npm start ``` *Now you can visit http://localhost:8080 to view your server* You should see the turtle image in the screenshot above hosted at that URL. See the `./public` folder for demo content. ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions | Version | Supported | |---------|------------------------| | 14.x | ✔️ Yes | | 13.x | 🔐 Security updates until April 2022 | | <= 0.13.x | ❌ No | ## Reporting a Vulnerability In 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. ================================================ FILE: bin/http-server ================================================ #!/usr/bin/env node 'use strict'; var chalk = require('chalk'), os = require('os'), httpServer = require('../lib/http-server'), portfinder = require('portfinder'), opener = require('opener'), fs = require('fs'), url = require('url'); var argv = require('minimist')(process.argv.slice(2), { alias: { tls: 'ssl', header: 'H', user: 'username', }, boolean: ['proxy-all'] }); var ifaces = os.networkInterfaces(); if (argv.h || argv.help) { console.log([ 'usage: http-server [path] [options]', '', 'options:', ' -p --port Port to use. If 0, look for open port. [8080]', ' -a Address to use [0.0.0.0] or [::]', ' -d Show directory listings [true]', ' --dir-overrides-404 Whether -d should override magic 404.html [false]', ' --base-dir Base directory to serve files from [/]', ' -i Display autoIndex [true]', ' -g --gzip Serve gzip files when possible [false]', ' -b --brotli Serve brotli files when possible [false]', ' If both brotli and gzip are enabled, brotli takes precedence', '', ' --force-content-encoding', ' When using --gzip or --brotli, includes the content encoding', ' header even when the extension for the compressed file is', ' specified in the URL. "test.png.br" will be served the same', ' way as "test.png".', '', ' -e --ext Default file extension if none supplied [none]', ' -s --silent Suppress log messages from output', ' --coop[=mode] Enable COOP via the "Cross-Origin-Opener-Policy" header', ' Optionally provide COOP mode.', ' --content-type Default content type for unknown file types [application/octet-stream]', ' --cors[=headers] Enable CORS via the "Access-Control-Allow-Origin" header', ' Optionally provide CORS headers list separated by commas', ' --private-network-access Enable Private Network Access via the', ' "Access-Control-Allow-Private-Network" header', ' When enabled, sets Access-Control-Allow-Origin to "*"', ' Optional value adds to Access-Control-Allow-Headers', ' -H', ' --header', ' Add an extra response header (can be used several times)', ' -o [path] Open browser window after starting the server.', ' Optionally provide a URL path to open the browser window to.', ' -c Cache time (max-age) in seconds [3600], e.g. -c10 for 10 seconds.', ' To disable caching, use -c-1.', ' -t Connection timeout in seconds [120], e.g. -t60 for 1 minute.', ' To disable timeout, use -t0.', ' -T --title Custom title suffix for the terminal window [none]', ' The terminal title will be "http-server PORT [TITLE]"', ' -U --utc Use UTC time format in log messages.', ' --log-ip Enable logging of the client\'s IP address', '', ' -P --proxy Fallback proxy if the request cannot be resolved. e.g.: http://someurl.com', ' --proxy-all Send every request to the proxy target instead of serving local files', ' --proxy-options Pass options to proxy using nested dotted objects. e.g.: --proxy-options.secure false', ' --proxy-config Pass in .json configuration file. e.g.: ./path/to/config.json', ' --websocket Enable websocket proxy', '', ' --user --username Username for basic authentication [none]', ' Can also be specified with the env variable NODE_HTTP_SERVER_USERNAME', ' --password Password for basic authentication [none]', ' Can also be specified with the env variable NODE_HTTP_SERVER_PASSWORD', '', ' -S --tls --ssl Enable secure request serving with TLS/SSL (HTTPS)', ' -C --cert Path to TLS cert file (default: cert.pem)', ' -K --key Path to TLS key file (default: key.pem)', '', ' -r --robots Respond to /robots.txt [User-agent: *\\nDisallow: /]', ' --no-dotfiles Do not show dotfiles', ' --hide-permissions Do not show file permissions', ' --mimetypes Path to a .types file for custom mimetype definition', ' -h --help Print this list and exit.', ' -v --version Print the version and exit.', ' --no-panic If error occurs, gracefully shut down and create log file', ' Can also be specified with the env variable NODE_HTTP_SERVER_NO_PANIC', ' --allowed-hosts Comma-separated list of hosts allowed to access the server. e.g.: --allowed-hosts localhost,example.com', ].join('\n')); process.exit(); } var port = argv.p || argv.port || parseInt(process.env.PORT, 10), nopanic = !argv['panic'] || argv.n || process.env.NODE_HTTP_SERVER_NO_PANIC, host = argv.a || '::', tls = argv.S || argv.tls, title = argv.T || argv.title, sslPassphrase = process.env.NODE_HTTP_SERVER_SSL_PASSPHRASE, proxy = argv.P || argv.proxy, proxyOptions = argv['proxy-options'], proxyConfig = argv['proxy-config'], websocket = argv.websocket, proxyAll = Boolean(argv['proxy-all']), utc = argv.U || argv.utc, version = argv.v || argv.version, baseDir = argv['base-dir'], logger, allowedHosts = argv['allowed-hosts']; if (nopanic){ process.on('error', (e)=> { // Results in a string like "2021-12-27 14:56:31" const etime = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, ''); console.log(chalk.green(etime)); console.log(`${chalk.red('Fatal error: ')}${e.code}: ${e.message}`); const filename = `httpserver-${etime.split(' ').join('_')}.log`; console.log(chalk.bold(`Check ${filename} file in this folder.`)); fs.writeFileSync(filename, JSON.stringify(e)); process.exit(1); }); } var proxyOptionsBooleanProps = [ 'ws', 'xfwd', 'secure', 'toProxy', 'prependPath', 'ignorePath', 'changeOrigin', 'preserveHeaderKeyCase', 'followRedirects', 'selfHandleResponse' ]; if (proxyOptions) { Object.keys(proxyOptions).forEach(function (key) { if (proxyOptionsBooleanProps.indexOf(key) > -1) { proxyOptions[key] = proxyOptions[key].toLowerCase() === 'true'; } }); } if (!argv.s && !argv.silent) { logger = { info: console.log, warning: console.warn, request: function (req, res, error) { var date = utc ? new Date().toUTCString() : new Date(); var ip = argv['log-ip'] ? req.headers['x-forwarded-for'] || '' + req.connection.remoteAddress : ''; if (error) { logger.info( '[%s] %s "%s %s" Error (%s): "%s"', date, ip, chalk.red(req.method), chalk.red(req.url), chalk.red(error.status.toString()), chalk.red(error.message) ); } else if (req.proxy) { logger.info( '[%s] %s "%s" (%s)-> "%s"', date, ip, chalk.cyan(req.url), chalk.magenta('Proxy'), chalk.cyan(req.proxy.target) ); } else { logger.info( '[%s] %s "%s %s" "%s"', date, ip, chalk.cyan(req.method), chalk.cyan(req.url), req.headers['user-agent'] ); } } }; } else if (chalk) { logger = { info: function () {}, request: function () {} }; } if (version) { logger.info('v' + require('../package.json').version); process.exit(); } if (!port) { portfinder.basePort = 8080; portfinder.getPort(function (err, port) { if (err) { throw err; } listen(port); }); } else { listen(port); } if (allowedHosts && typeof allowedHosts === 'string') { allowedHosts = allowedHosts.split(',').map((host) => host.trim().toLowerCase()); } else { allowedHosts = undefined; } function listen(port) { var options = { root: argv._[0], cache: argv.c, timeout: argv.t, showDir: argv.d, dirOverrides404: argv['dir-overrides-404'], baseDir: baseDir, autoIndex: argv.i, gzip: argv.g || argv.gzip, brotli: argv.b || argv.brotli, robots: argv.r || argv.robots, ext: argv.e || argv.ext, logFn: logger.request, proxy: proxy, proxyOptions: proxyOptions, proxyConfig: proxyConfig, proxyAll: proxyAll, showDotfiles: argv.dotfiles, hidePermissions: argv['hide-permissions'], mimetypes: argv.mimetypes, contentType: argv['content-type'], username: argv.username || process.env.NODE_HTTP_SERVER_USERNAME, password: argv.password || process.env.NODE_HTTP_SERVER_PASSWORD, headers: {}, allowedHosts, }; function setHeader(str) { const m = /^(.+?)\s*(:\s*(.*))$/.exec(str); if (!m || m.length < 4) { options.headers[str] = ''; } else { options.headers[m[1]] = m[3]; } } if (argv.coop) { options.coop = true; if (typeof argv.coop === 'string') { options.coopHeader = argv.coop; } } if (websocket) { if (!proxy) { logger.warning(chalk.yellow('WebSocket proxy will not be enabled because proxy is not enabled')); } else { options.websocket = true; } } if (argv.cors) { options.cors = true; if (typeof argv.cors === 'string') { options.corsHeaders = argv.cors; } } if ( argv['force-content-encoding'] ) { options.forceContentEncoding = true; } if (argv.header) { if (Array.isArray(argv.header)) { argv.header.forEach(h => setHeader(h)); } else { setHeader(argv.header); } } if (argv['private-network-access']) { options.privateNetworkAccess = true; } if (proxy) { try { new url.URL(proxy); } catch (err) { logger.info(chalk.red('Error: Invalid proxy url')); process.exit(1); } } if (proxyConfig) { try { if (fs.existsSync(proxyConfig)) { proxyConfig = fs.readFileSync(proxyConfig, 'utf8'); } if (typeof proxyConfig === 'string') { proxyConfig = JSON.parse(proxyConfig); } if (typeof proxyConfig !== 'object') { throw new Error('Invalid proxy config'); } } catch (err) { logger.info(chalk.red('Error: Invalid proxy config or file')); process.exit(1); } // Proxy file overrides cli config proxy = undefined; proxyOptions = undefined; } if (proxyAll && proxyConfig) { logger.info(chalk.red('Error: --proxy-all cannot be used with --proxy-config')); logger.info( '%s\n%s\n%s', chalk.yellow('Hint: Use'), chalk.cyan('"/**": {\n "target": "your-proxy"\n}'), chalk.yellow('in the proxy config to achieve the same effect.') ); process.exit(1); } if (proxyAll && !proxy) { logger.info(chalk.red('Error: --proxy-all requires --proxy to be set')); process.exit(1); } if (tls) { options.https = { cert: argv.C || argv.cert || 'cert.pem', key: argv.K || argv.key || 'key.pem', passphrase: sslPassphrase }; try { fs.lstatSync(options.https.cert); } catch (err) { logger.info(chalk.red('Error: Could not find certificate ' + options.https.cert)); process.exit(1); } try { fs.lstatSync(options.https.key); } catch (err) { logger.info(chalk.red('Error: Could not find private key ' + options.https.key)); process.exit(1); } } var server = httpServer.createServer(options); server.listen(port, host, function () { // Set process title with port and optional custom suffix process.title = 'http-server ' + port + (title ? ' ' + title : ''); var protocol = tls ? 'https://' : 'http://', path = baseDir ? '/' + baseDir.replace(/^\//, '') : ''; logger.info([ chalk.yellow('Starting up http-server, serving '), chalk.cyan(server.root), tls ? (chalk.yellow(' through') + chalk.cyan(' https')) : '' ].join('')); logger.info([chalk.yellow('\nhttp-server version: '), chalk.cyan(require('../package.json').version)].join('')); logger.info([ chalk.yellow('\nhttp-server settings: '), ([chalk.yellow('COOP: '), argv.coop ? chalk.cyan(argv.coop) : chalk.red('disabled')].join('')), ([chalk.yellow('CORS: '), argv.cors ? chalk.cyan(argv.cors) : chalk.red('disabled')].join('')), ([chalk.yellow('Private Network Access: '), argv['private-network-access'] ? chalk.cyan(argv['private-network-access']) : chalk.red('disabled')].join('')), ([chalk.yellow('Cache: '), argv.c ? (argv.c === '-1' ? chalk.red('disabled') : chalk.cyan(argv.c + ' seconds')) : chalk.cyan('3600 seconds')].join('')), ([chalk.yellow('Connection Timeout: '), Math.max(0, argv.t) === 0 ? chalk.red('disabled') : ((!isNaN(argv.t) && !isNaN(parseFloat(argv.t))) ? chalk.cyan(Number(argv.t) + ' seconds') : chalk.cyan('120 seconds'))].join('')), ([chalk.yellow('Directory Listings: '), argv.d ? chalk.red('not visible') : chalk.cyan('visible')].join('')), ([chalk.yellow('AutoIndex: '), argv.i ? chalk.red('not visible') : chalk.cyan('visible')].join('')), ([chalk.yellow('Serve GZIP Files: '), argv.g || argv.gzip ? chalk.cyan('true') : chalk.red('false')].join('')), ([chalk.yellow('Serve Brotli Files: '), argv.b || argv.brotli ? chalk.cyan('true') : chalk.red('false')].join('')), ([chalk.yellow('Default File Extension: '), argv.e ? chalk.cyan(argv.e) : (argv.ext ? chalk.cyan(argv.ext) : chalk.red('none'))].join('')), ([chalk.yellow('Base directory: '), baseDir ? chalk.cyan(baseDir) : chalk.cyan('/')].join('')) ].join('\n')); if (options.headers) { logger.info(chalk.yellow('Additional Headers:')); for (let k in options.headers) { let v = options.headers[k]; logger.info(chalk.yellow(`\t${k}:`) + chalk.cyan(` ${v}`)); } } logger.info(chalk.yellow('\nAvailable on:')); if (allowedHosts) { for (const host of allowedHosts) { logger.info(` ${protocol}${host}:${chalk.green(port.toString())}${path}`); } } else if (argv.a && (host !== '::' || host !== '0.0.0.0')) { logger.info(` ${protocol}${host}:${chalk.green(port.toString())}${path}`); } else { Object.keys(ifaces).forEach(function (dev) { ifaces[dev].forEach(function (details) { if (details.family === 'IPv4' || details.family === 4) { logger.info((' ' + protocol + details.address + ':' + chalk.green(port.toString()) + path)); } if (details.family === 'IPv6' && !details.address.startsWith("fe80") ) { // Ignoring Ipv6-Link Local addresses logger.info((' ' + protocol + details.address + ':' + chalk.green(port.toString()))); } }); }); } if (typeof proxy === 'string') { if (proxyOptions) { logger.info('Unhandled requests will be served from: ' + proxy + '. Options: ' + JSON.stringify(proxyOptions)); } else { logger.info('Unhandled requests will be served from: ' + proxy); } } // Set up "CTRL-C" hook, before printing out "Hit CTRL-C to stop the server" function stopServer() { server.close(); logger.info(chalk.red('http-server stopped.')); process.exit(); } process.on('SIGINT', stopServer); process.on('SIGTERM', stopServer); if (process.platform === 'win32') { require('readline').createInterface({ input: process.stdin, output: process.stdout }).on('SIGINT', function () { process.emit('SIGINT'); }); } logger.info('Hit CTRL-C to stop the server'); if (argv.o) { let openHost = host if ('::' === host || '0.0.0.0'===host){ openHost = '127.0.0.1' } let openUrl = `${protocol}${openHost}:${port}`; if (typeof argv.o === 'string') { openUrl += argv.o[0] === '/' ? argv.o : '/' + argv.o; } logger.info('Open: ' + openUrl); opener(openUrl); } // Spacing before logs if (!argv.s) logger.info(); }); } ================================================ FILE: doc/http-server.1 ================================================ .TH http-server 1 "April 2020" GNU "http-server man page" .SH NAME http-server \- a simple zero-configuration command-line http server .SH SYNOPSIS .B http-server [\fIPATH\fR] [\fIOPTIONS\fR] .SH DESCRIPTION \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. .SH OPTIONS .TP .BI [\fIPATH\fR] The directory to serve. Defaults to ./public if it exists, and ./ otherwise. .TP .BI \-p ", " \-\-port " " \fIPORT\fR Port to use. If 0, look for the first available port, starting at 8080. Default is 8080. .TP .BI \-a " " \fIADDRESS\fR Address to use. Default is 0.0.0.0. .TP .BI \-d Show directory listings. Default is true. .TP .BI \-d Whether -d should override magic 404.html Default is false. .TP .BI \-i Display autoIndex. Default is true. .TP .BI \-g ", " \-\-gzip Serve gzip files when possible. Default is false. .TP .BI \-b ", " \-\-brotli Serve brotli files when possible. If both brotli and gzip are enabled, brotli takes precedence. Default is false. .TP .BI \-\-force\-content\-encoding When using --gzip or --brotli, includes the content encoding header even when the extension for the compressed file is specified in the URL. "test.png.br" will be served the same way as "test.png". .TP .BI \-e ", " \-\-ext " " \fIEXTENSION\fR Default file extension is none is provided. .TP .BI \-s ", " \-\-silent Suppress log messages from output. .TP .BI \-n ", " \-\-no-panic Gracefully shut down whenever a fatal error occurs, sending stack to log file, not console. .TP .BI \-\-coop " " [\fIMODE\fR] Enable COOP via the "Cross-Origin-Opener-Policy" header and sets the "Cross-Origin-Embedder-Policy" header to "require-corp". Optionally provide COOP mode which defaults to "same-origin". .TP .BI \-\-cors " " [\fIHEADERS\fR] Enable CORS by setting "Access-Control-Allow-Origin" to "*". Optional comma-separated headers list adds to "Access-Control-Allow-Headers". Default Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Range. .TP .BI \-H ", " \-\-header " " \fIHEADER\fR Add an extra response header (can be used several times) .TP .BI \-\-private-network-access Enable Private Network Access via the "Access-Control-Allow-Private-Network" header. .TP .BI \-o " " [\fIPATH\fR] Open default browser window after starting the server. Optionally provide a URL path to open the browser window to. .TP .BI \-c " " \fITIME\fR Cache time (max-age) in seconds. To disable caching, use \-c \-1. Default is 3600. .TP .BI \-t " " \fITIMEOUT\fR Connection timeout in seconds, e.g. -t60 for 1 minute. To disable timeout, use \-t0. Default is 120. .TP .BI \-T ", " \-\-title " " \fITITLE\fR Custom title suffix for the terminal window. The terminal title will be "http-server PORT [TITLE]". .TP .BI \-U ", " \-\-utc Use UTC time format in log messages. .TP .BI \-\-log\-ip Enable logging of the client IP address. .TP .BI \-P ", " \-\-proxy Fallback proxy if the request cannot be resolved. .TP .BI \-\-proxy\-all Forward every request to the proxy target and disable local file serving. Requires \-\-proxy. .TP .BI \-\-proxy\-options Pass proxy options using nested dotted objects. .TP .BI \-\-proxy\-config Pass in .json configuration file. .TP .BI \-\-user ", " \-\-username " " \fIUSERNAME\fR Username for basic authentication. Can also be specified with the environment variable NODE_HTTP_SERVER_USERNAME. Defaults to none. .TP .BI \-\-password " " \fIPASSWORD\fR Password for basic authentication. Can also be specified with the environment variable NODE_HTTP_SERVER_PASSWORD. Defaults to none. .TP .BI \-S ", " \-\-tls ", " \-\-ssl Enable https. .TP .BI \-C ", " \-\-cert " " [\fIFILE\fR] Path to SSL certificate file. If not specified, uses cert.pem. .TP .BI \-K ", " \-\-key " " [\fIFILE\fR] Path to SSL key file. If not specified, uses key.pem. Passphrase will be read from NODE_HTTP_SERVER_SSL_PASSPHRASE (if set) .TP .BI \-r ", " \-\-robots " " [\fIUSER\-AGENT\fR] Respond to /robots.txt request. If not specified, uses "User-agent: *\\nDisallow: /]" .TP .BI \-\-no\-dotfiles Do not show dotfiles. .TP .BI \-\-hide\-permissions Do not show file permissions. .TP .BI \-\-allowed\-hosts Comma-separated list of hosts allowed to access the server. e.g.: \-\-allowed\-hosts localhost,example.com .TP .BI \-h ", " \-\-help Print usage and exit. .TP .BI \-v ", " \-\-version Print version and exit. .SH FILES .B index.html will be served as the default file to any directory requests. .B 404.html will be served if a file is not found. This can be used for SPA hosting to serve the entry page. .SH COPYING Copyright (c) 2011-2022 Charlie Robbins, Marak Squires, and the Contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .SH VERSION Version 0.12.2 ================================================ FILE: lib/core/aliases.json ================================================ { "autoIndex": [ "autoIndex", "autoindex" ], "showDir": [ "showDir", "showdir" ], "dirOverrides404": [ "dirOverrides404", "diroverrides404", "dir-overrides-404", "listingsOverride404", "listings-override-404" ], "showDotfiles": ["showDotfiles", "showdotfiles"], "humanReadable": [ "humanReadable", "humanreadable", "human-readable" ], "hidePermissions": ["hidePermissions", "hidepermissions", "hide-permissions"], "si": [ "si", "index" ], "handleError": [ "handleError", "handleerror" ], "coop": [ "coop", "COOP" ], "cors": [ "cors", "CORS" ], "privateNetworkAccess": [ "privateNetworkAccess", "privatenetworkaccess", "private-network-access" ], "headers": [ "H", "header", "headers" ], "contentType": [ "contentType", "contenttype", "content-type" ], "mimeType": [ "mimetype", "mimetypes", "mimeType", "mimeTypes", "mime-type", "mime-types", "mime-Type", "mime-Types" ], "weakEtags": [ "weakEtags", "weaketags", "weak-etags" ], "weakCompare": [ "weakcompare", "weakCompare", "weak-compare", "weak-Compare" ], "handleOptionsMethod": [ "handleOptionsMethod", "handleoptionsmethod", "handle-options-method" ] } ================================================ FILE: lib/core/etag.js ================================================ 'use strict'; module.exports = (stat, weakEtag) => { let etag = `"${[stat.ino, stat.size, stat.mtime.toISOString()].join('-')}"`; if (weakEtag) { etag = `W/${etag}`; } return etag; }; ================================================ FILE: lib/core/index.js ================================================ #! /usr/bin/env node 'use strict'; const path = require('path'); const fs = require('fs'); const url = require('url'); const { Readable } = require('stream'); const buffer = require('buffer'); const mime = require('mime'); const urlJoin = require('url-join'); const showDir = require('./show-dir'); const version = require('../../package.json').version; const status = require('./status-handlers'); const generateEtag = require('./etag'); const optsParser = require('./opts'); const htmlEncodingSniffer = require('html-encoding-sniffer'); let httpServerCore = null; function decodePathname(pathname) { const pieces = pathname.replace(/\\/g, '/').split('/'); const normalized = path.normalize(pieces.map((rawPiece) => { const piece = decodeURIComponent(rawPiece); if (process.platform === 'win32' && /\\/.test(piece)) { throw new Error('Invalid forward slash character'); } return piece; }).join('/')); return process.platform === 'win32' ? normalized.replace(/\\/g, '/') : normalized; } const nonUrlSafeCharsRgx = /[\x00-\x1F\x20\x7F-\uFFFF]+/g; function ensureUriEncoded(text) { return text return String(text).replace(nonUrlSafeCharsRgx, encodeURIComponent); } // Check to see if we should try to compress a file with gzip. function shouldCompressGzip(req) { const headers = req.headers; return headers && headers['accept-encoding'] && headers['accept-encoding'] .split(',') .some(el => ['*', 'compress', 'gzip', 'deflate'].indexOf(el.trim()) !== -1) ; } function shouldCompressBrotli(req) { const headers = req.headers; return headers && headers['accept-encoding'] && headers['accept-encoding'] .split(',') .some(el => ['*', 'br'].indexOf(el.trim()) !== -1) ; } function hasGzipId12(gzipped, cb) { const stream = fs.createReadStream(gzipped, { start: 0, end: 1 }); let buffer = Buffer.from(''); let hasBeenCalled = false; stream.on('data', (chunk) => { buffer = Buffer.concat([buffer, chunk], 2); }); stream.on('error', (err) => { if (hasBeenCalled) { throw err; } hasBeenCalled = true; cb(err); }); stream.on('close', () => { if (hasBeenCalled) { return; } hasBeenCalled = true; cb(null, buffer[0] === 31 && buffer[1] === 139); }); } module.exports = function createMiddleware(_dir, _options) { let dir; let options; if (typeof _dir === 'string') { dir = _dir; options = _options; } else { options = _dir; dir = options.root; } const root = path.join(path.resolve(dir), '/'); const opts = optsParser(options); opts.root = dir; // Support hashes and .types files in mimeTypes @since 0.8 if (opts.mimeTypes) { try { // You can pass a JSON blob here---useful for CLI use opts.mimeTypes = JSON.parse(opts.mimeTypes); } catch (e) { // swallow parse errors, treat this as a string mimetype input } if (typeof opts.mimeTypes === 'string') { mime.load(opts.mimeTypes); } else if (typeof opts.mimeTypes === 'object') { mime.define(opts.mimeTypes); } } function shouldReturn304(req, serverLastModified, serverEtag) { if (!req || !req.headers) { return false; } const clientModifiedSince = req.headers['if-modified-since']; const clientEtag = req.headers['if-none-match']; let clientModifiedDate; if (!clientModifiedSince && !clientEtag) { // Client did not provide any conditional caching headers return false; } if (clientModifiedSince) { // Catch "illegal access" dates that will crash v8 try { clientModifiedDate = new Date(Date.parse(clientModifiedSince)); } catch (err) { return false; } if (clientModifiedDate.toString() === 'Invalid Date') { return false; } // If the client's copy is older than the server's, don't return 304 if (clientModifiedDate < new Date(serverLastModified)) { return false; } } if (clientEtag) { // Do a strong or weak etag comparison based on setting // https://www.ietf.org/rfc/rfc2616.txt Section 13.3.3 if (opts.weakCompare && clientEtag !== serverEtag && clientEtag !== `W/${serverEtag}` && `W/${clientEtag}` !== serverEtag) { return false; } if (!opts.weakCompare && (clientEtag !== serverEtag || clientEtag.indexOf('W/') === 0)) { return false; } } return true; } return function middleware(req, res, next) { // Figure out the path for the file from the given url const parsed = url.parse(req.url); let pathname = null; let file = null; let gzippedFile = null; let brotliFile = null; try { decodeURIComponent(req.url); // check validity of url pathname = decodePathname(parsed.pathname); } catch (err) { status[400](res, next, { error: err }); return; } file = path.normalize( path.join( root, path.relative(path.join('/', opts.baseDir), pathname) ) ); // 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" gzippedFile = `${file}.gz`; brotliFile = `${file}.br`; if ( opts.forceContentEncoding ) { if ( file.endsWith('.gz') ) gzippedFile = file; if ( file.endsWith('.br') ) brotliFile = file; } Object.keys(opts.headers).forEach((key) => { res.setHeader(key, opts.headers[key]); }); if (req.method === 'OPTIONS' && opts.handleOptionsMethod) { res.end(); return; } // TODO: This check is broken, which causes the 403 on the // expected 404. if (file.slice(0, root.length) !== root) { status[403](res, next); return; } if (req.method && (req.method !== 'GET' && req.method !== 'HEAD')) { status[405](res, next); return; } function serve(stat) { // Do a MIME lookup, fall back to octet-stream and handle gzip // and brotli special case. const defaultType = opts.contentType || 'application/octet-stream'; let contentType = mime.lookup(file, defaultType); const range = (req.headers && req.headers.range); const lastModified = (new Date(stat.mtime)).toUTCString(); const etag = generateEtag(stat, opts.weakEtags); let cacheControl = opts.cache; let stream = null; if (contentType && isTextFile(contentType)) { if (stat.size < buffer.constants.MAX_LENGTH) { const bytes = fs.readFileSync(file); const sniffedEncoding = htmlEncodingSniffer(bytes, { defaultEncoding: 'UTF-8' }); contentType += `; charset=${sniffedEncoding}`; stream = Readable.from(bytes) } else { // Assume text types are utf8 contentType += '; charset=UTF-8'; } } if (file === gzippedFile) { // is .gz picked up res.setHeader('Content-Encoding', 'gzip'); // strip gz ending and lookup mime type contentType = mime.lookup(path.basename(file, '.gz'), defaultType); } else if (file === brotliFile) { // is .br picked up res.setHeader('Content-Encoding', 'br'); // strip br ending and lookup mime type contentType = mime.lookup(path.basename(file, '.br'), defaultType); } if (typeof cacheControl === 'function') { cacheControl = opts.cache(pathname); } if (typeof cacheControl === 'number') { cacheControl = `max-age=${cacheControl}`; } if (range) { const total = stat.size; const parts = range.trim().replace(/bytes=/, '').split('-'); const partialstart = parts[0]; const partialend = parts[1]; const start = parseInt(partialstart, 10); const end = Math.min( total - 1, partialend ? parseInt(partialend, 10) : total - 1 ); const chunksize = (end - start) + 1; let fstream = null; if (start > end || isNaN(start) || isNaN(end)) { status['416'](res, next, { size: total }); return; } fstream = fs.createReadStream(file, { start, end }); fstream.on('error', (err) => { status['500'](res, next, { error: err }); }); res.on('close', () => { fstream.destroy(); }); res.writeHead(206, { 'Content-Range': `bytes ${start}-${end}/${total}`, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': contentType, 'cache-control': cacheControl, 'last-modified': lastModified, etag, }); fstream.pipe(res); return; } // TODO: Helper for this, with default headers. res.setHeader('cache-control', cacheControl); res.setHeader('last-modified', lastModified); res.setHeader('etag', etag); // Return a 304 if necessary if (shouldReturn304(req, lastModified, etag)) { status[304](res, next); return; } res.setHeader('content-length', stat.size); res.setHeader('content-type', contentType); // set the response statusCode if we have a request statusCode. // This only can happen if we have a 404 with some kind of 404.html // In all other cases where we have a file we serve the 200 res.statusCode = req.statusCode || 200; if (req.method === 'HEAD') { res.end(); return; } // stream may already have been assigned during encoding sniffing. if (stream === null) { stream = fs.createReadStream(file); } stream.pipe(res); stream.on('error', (err) => { status['500'](res, next, { error: err }); }); stream.on('close', () => { stream.destroy(); }) } function statWithAccess (file, cb) { fs.stat(file, (err, stat) => { if (err) { cb(err); return; } fs.access(file, fs.constants.R_OK, (err) => { stat.readable = !err; cb(err, stat); }); }); } function statFile() { try { statWithAccess(file, (err, stat) => { const effectively404 = (err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')) || (!stat || !stat.readable); if (effectively404) { if (req.statusCode === 404) { // This means we're already trying ./404.html and can not find it. // So send plain text response with 404 status code status[404](res, next); } else if (!path.extname(parsed.pathname).length && opts.defaultExt) { // If there is no file extension in the path and we have a default // extension try filename and default extension combination before rendering 404.html. middleware({ url: `${parsed.pathname}.${opts.defaultExt}${(parsed.search) ? parsed.search : ''}`, headers: req.headers, }, res, next); } else if (opts.showDir && opts.dirOverrides404) { // If showDir and dirOverrides404 are true, show the directory instead of 404.html req.url = path.dirname(req.url); showDir(opts, stat)(req, res); return; } else { // Try to serve default ./404.html const rawUrl = (opts.handleError ? `/${path.join(opts.baseDir, `404.${opts.defaultExt}`)}` : req.url); const encodedUrl = ensureUriEncoded(rawUrl); middleware({ url: encodedUrl, headers: req.headers, statusCode: 404, }, res, next); } } else if (err) { status[500](res, next, { error: err }); } else if (stat.isDirectory()) { if (!opts.autoIndex && !opts.showDir) { status[404](res, next); return; } // 302 to / if necessary if (!pathname.match(/\/$/)) { res.statusCode = 302; const q = parsed.query ? `?${parsed.query}` : ''; res.setHeader( 'location', ensureUriEncoded(`${parsed.pathname}/${q}`) ); res.end(); return; } if (opts.autoIndex) { middleware({ url: urlJoin( encodeURIComponent(pathname), `/index.${opts.defaultExt}` ), headers: req.headers, }, res, (autoIndexError) => { if (autoIndexError) { status[500](res, next, { error: autoIndexError }); return; } if (opts.showDir) { showDir(opts, stat)(req, res); return; } status[403](res, next); }); return; } if (opts.showDir) { showDir(opts, stat)(req, res); } } else { serve(stat); } }); } catch (err) { status[500](res, next, { error: err.message }); } } function isTextFile(mimeType) { return (/^text\/|^application\/(javascript|json)/).test(mimeType); } // serve gzip file if exists and is valid function tryServeWithGzip() { try { fs.stat(gzippedFile, (err, stat) => { if (!err && stat.isFile()) { hasGzipId12(gzippedFile, (gzipErr, isGzip) => { if (!gzipErr && isGzip) { file = gzippedFile; serve(stat); } else { statFile(); } }); } else { statFile(); } }); } catch (err) { status[500](res, next, { error: err.message }); } } // serve brotli file if exists, otherwise try gzip function tryServeWithBrotli(shouldTryGzip) { try { fs.stat(brotliFile, (err, stat) => { if (!err && stat.isFile()) { file = brotliFile; serve(stat); } else if (shouldTryGzip) { tryServeWithGzip(); } else { statFile(); } }); } catch (err) { status[500](res, next, { error: err.message }); } } const shouldTryBrotli = opts.brotli && shouldCompressBrotli(req); const shouldTryGzip = opts.gzip && shouldCompressGzip(req); // always try brotli first, next try gzip, finally serve without compression if (shouldTryBrotli) { tryServeWithBrotli(shouldTryGzip); } else if (shouldTryGzip) { tryServeWithGzip(); } else { statFile(); } }; }; httpServerCore = module.exports; httpServerCore.version = version; httpServerCore.showDir = showDir; ================================================ FILE: lib/core/opts.js ================================================ 'use strict'; const aliases = require('./aliases.json'); /** * @typedef {Object} ParsedOptions * @property {boolean} autoIndex * @property {boolean} showDir * @property {boolean} dirOverrides404 * @property {boolean} showDotfiles * @property {boolean} humanReadable * @property {boolean} hidePermissions * @property {boolean} si * @property {string|function} cache * @property {string} defaultExt * @property {string} baseDir * @property {boolean} gzip * @property {boolean} brotli * @property {boolean} forceContentEncoding * @property {function} handleError * @property {Object.} headers * @property {string} contentType * @property {Object|undefined} mimeTypes * @property {boolean} weakEtags * @property {boolean} weakCompare * @property {boolean} handleOptionsMethod */ /** * Converts a user-provided options object into a ParsedOptions object * @param {object} opts - User provided options * @returns {ParsedOptions} */ module.exports = (opts) => { /** @type {ParsedOptions} */ const options = { autoIndex: true, showDir: true, dirOverrides404: false, showDotfiles: true, humanReadable: true, hidePermissions: false, si: false, cache: "max-age=3600", coop: false, cors: false, privateNetworkAccess: false, gzip: true, brotli: false, forceContentEncoding: false, defaultExt: "html", baseDir: "/", handleError: true, contentType: "application/octet-stream", weakEtags: true, weakCompare: true, handleOptionsMethod: false, headers: {}, mimeTypes: undefined, }; function isDeclared(k) { return typeof opts[k] !== 'undefined' && opts[k] !== null; } function validateNoCRLF(str) { if (typeof str === 'string' && (str.includes('\r') || str.includes('\n'))) { throw new Error('Header is not a string or contains CRLF'); } } function addHeader(key, value) { validateNoCRLF(key); validateNoCRLF(value); options.headers[key] = value; } function setHeader(str) { validateNoCRLF(str); const m = /^(.+?)\s*:\s*(.*)$/.exec(str); if (!m) { addHeader(str, true); // Use addHeader instead of direct assignment } else { addHeader(m[1], m[2]); // Use addHeader instead of direct assignment } } if (opts) { aliases.autoIndex.some((k) => { if (isDeclared(k)) { options.autoIndex = opts[k]; return true; } return false; }); aliases.showDir.some((k) => { if (isDeclared(k)) { options.showDir = opts[k]; return true; } return false; }); aliases.dirOverrides404.some((k) => { if (isDeclared(k)) { options.dirOverrides404 = opts[k]; return true; } return false; }); aliases.showDotfiles.some((k) => { if (isDeclared(k)) { options.showDotfiles = opts[k]; return true; } return false; }); aliases.humanReadable.some((k) => { if (isDeclared(k)) { options.humanReadable = opts[k]; return true; } return false; }); aliases.hidePermissions.some((k) => { if (isDeclared(k)) { options.hidePermissions = opts[k]; return true; } return false; }); aliases.si.some((k) => { if (isDeclared(k)) { options.si = opts[k]; return true; } return false; }); if (opts.defaultExt && typeof opts.defaultExt === 'string') { let ext = opts.defaultExt; // Remove the leading dot if it exists if (/^\./.test(ext)) { ext = ext.replace(/^\./, ''); } options.defaultExt = ext; } if (typeof opts.cache !== 'undefined' && opts.cache !== null) { if (typeof opts.cache === 'string') { options.cache = opts.cache; } else if (typeof opts.cache === 'number') { options.cache = `max-age=${opts.cache}`; } else if (typeof opts.cache === 'function') { options.cache = opts.cache; } } if (typeof opts.gzip !== 'undefined' && opts.gzip !== null) { options.gzip = opts.gzip; } if (typeof opts.brotli !== 'undefined' && opts.brotli !== null) { options.brotli = opts.brotli; } if (typeof opts.forceContentEncoding !== 'undefined' && opts.forceContentEncoding !== null) { options.forceContentEncoding = opts.forceContentEncoding; } if (typeof opts.baseDir !== 'undefined' && opts.baseDir !== null) { options.baseDir = opts.baseDir; } aliases.handleError.some((k) => { if (isDeclared(k)) { options.handleError = opts[k]; return true; } return false; }); aliases.coop.forEach((k) => { if (isDeclared(k) && opts[k]) { options.handleOptionsMethod = true; options.headers['Cross-Origin-Opener-Policy'] = 'same-origin'; options.headers['Cross-Origin-Embedder-Policy'] = 'require-corp'; } }); aliases.cors.forEach((k) => { if (isDeclared(k) && opts[k]) { options.handleOptionsMethod = true; options.headers['Access-Control-Allow-Origin'] = '*'; options.headers['Access-Control-Allow-Headers'] = 'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since'; } }); aliases.privateNetworkAccess.forEach((k) => { if (isDeclared(k) && opts[k]) { options.headers['Access-Control-Allow-Private-Network'] = 'true'; } }); aliases.headers.forEach((k) => { if (isDeclared(k)) { if (Array.isArray(opts[k])) { opts[k].forEach(setHeader); } else if (opts[k] && typeof opts[k] === 'object') { Object.keys(opts[k]).forEach((key) => { addHeader(key, opts[k][key]); // Uses same validation path }); } else { setHeader(opts[k]); } } }); aliases.contentType.some((k) => { if (isDeclared(k)) { options.contentType = opts[k]; return true; } return false; }); aliases.mimeType.some((k) => { if (isDeclared(k)) { options.mimeTypes = opts[k]; return true; } return false; }); aliases.weakEtags.some((k) => { if (isDeclared(k)) { options.weakEtags = opts[k]; return true; } return false; }); aliases.weakCompare.some((k) => { if (isDeclared(k)) { options.weakCompare = opts[k]; return true; } return false; }); aliases.handleOptionsMethod.some((k) => { if (isDeclared(k)) { options.handleOptionsMethod = options.handleOptionsMethod || opts[k]; return true; } return false; }); } return options; }; ================================================ FILE: lib/core/show-dir/icons.json ================================================ { "_blank": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAWBJREFUeNqEUj1LxEAQnd1MVA4lyIEWx6UIKEGUExGsbC3tLfwJ/hT/g7VlCnubqxXBwg/Q4hQP/LhKL5nZuBsvuGfW5MGyuzM7jzdvVuR5DgYnZ+f99ai7Vt5t9K9unu4HLweI3qWYxI6PDosdy0fhcntxO44CcOBzPA7mfEyuHwf7ntQk4jcnywOxIlfxOCNYaLVgb6cXbkTdhJXq2SIlNMC0xIqhHczDbi8OVzpLSUa0WebRfmigLHqj1EcPZnwf7gbDIrYVRyEinurj6jTBHyI7pqVrFQqEbt6TEmZ9v1NRAJNC1xTYxIQh/MmRUlmFQE3qWOW1nqB2TWk1/3tgJV0waVvkFIEeZbHq4ElyKzAmEXOx6gnEVJuWBzmkRJBRPYGZBDsVaOlpSgVJE2yVaAe/0kx/3azBRO0VsbMFZE3CDSZKweZfYIVg+DZ6v7h9GDVOwZPw/PoxKu/fAgwALbDAXf7DdQkAAAAASUVORK5CYII=", "_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=", "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", "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", "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==", "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==", "bmp": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmZJREFUeNp0U+1rUlEY/13v9YV0vq2wttI5CdpL9aEGBZUDv0df668I6n+or0UQ/RuuD0EgVDAZrsKF4AR1a6COKW5qXvXec27PuVeda3bgcF6e8/ye5/d7niMZhgExnK9fbTrm5pbBGMZDkgCyq+VyhTUaT6Eo2ZHJePPWXJXRhez3B1yxmM/QdctXUSCgtV4Py4CvY3cky4e1x5DlLCaGbbzjXDcousG5OQe5HPRSCQPK4PpsEM/XH4WvhS4noeu3JwHGGRiULhsMoKZS4I0GtEIB9mgULJGA0+9DPBpBT7sffvf1W/Lg6OgJufw8C0CRGEXWazUwiiyFQjA8bsjVKjaJzovMD/Q5gxyJhG2cvyeXe2cAuADQNGBmBvLaGuTFRaDfh31lBTWi9pumjbK0B4JQul3vOQpM8JdskOLrdCvDcDjAsjtg5TIkoiKLaokMNR2cnZbqNAMycqG7XbHKR2fMzwO/dsxSwu0BiBJsNsv2LwAJAJCI5ux2gXYbqNetcz5PoORI1cDS0n8AxGW7A+zvEYBKZ2ZlcsEtJLbedMjePBaCTQMghx45ulyWkzxMVUQ2RMQhLfFO16YAqCrixPnm6iqKrRb2W23EfF4cUNSrHg90cr7hDyB33MTnSmUKALVs4uIlROjxg+AsPhGVl3fuIl2tIOB0Ya91gkOi9mxhAal0ekork1ic/kGLBORMxy2K1qS9V1ZQbNThIj2EGh+2tsyOnSai8r1UxMNIBB+LRTTULr4Uds0K1tU/uOLxIrmbNz8XXSrnASSpubG9fbKRyVh1n/zSw29t9oC1b47MfwUYAAUsLiWr4QUJAAAAAElFTkSuQmCC", "c": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAcxJREFUeNqEUk1rE0EYfmZnkgoJCaGNCehuJTalhJZSUZB66a0HwXsP/Qn+FM+9+hty0LNYCr2I7UVLIW0Fc0hpQpSS7O7MrO9MspuvVV8YMnk/nn2e5x0WRRFMvP/w6WSz5jbi/9NxfP693Wp3DrJCnMW5d28P7a+IE15lufR8o1ZEStwPhkWHsWbrZ+eNEPxsuubEF6m0TBv2Q4liPofXuzveulttSqW2UwH+GjqC0horpSL2njU89+FyMwjlTlxOJMTa9ZQHzDQIjgwdom9zLzfXPc75kbnOAswBJTlC2XrqQRMLxhi442DgB4UFBhgPpm3B5pgBHNUUxQKAHs8pHf3TEuFMetM9IKr/i2mWMwC0SnuSFTG2YKyppwKYVdGO7TFhzBqGIenVeLCUtfURgErucx5ECKREKBU4d3B718PHz6cICGT/1Qs8qpQtGOdyhtGEARWDQFqQJSeDL98u4VbLaKw9IRAJPwjtoJGlVAoDQ800+fRFTTYXcjlcXN2g++s36p5Lzzlve1iEROa8BGH1EbrSAeqrjxEqicHQt8/YSDHMpaNs7wJAp9vvfb287idboAVkRAa5fBYXP9rxO4Mgf0xvPPdHgAEA8OoGd40i1j0AAAAASUVORK5CYII=", "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==", "css": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAk1JREFUeNpsUktvUlEQ/u5DoCLl/RAKKKUvWmIxjYntQtcu3LvwJ/hTXLt16coFC2PsojEaMKZtCqFaTdGmjbS0CG3By+vei3OOBSGXSU7uzNyZ78z3zRF6vR6YvXzzPrMUCyf68bB9zO+VfpROn5hkOdfPPX/2lH/lfiLidztX5mN2jLGG0rKLENIE8liWpdzwP7HvqJqujmvudFU4bFY8Wk1FZsOBtKppd8YCDNu77CZevd3gflfTUFcUhP0ePLibiIR9rjSBpgwAfe4dVcV6dhtep4PH5msylGYLrzeybErcT85FYiH/CyPAf74gObC2vMhzsiRhPhpC6eQUM+EA1pJzILEnjRSuJsju7MJqsUCSRei6Dp3yXqcdGlHZ/rLPazQWGCn8+6YW4pAkEW0SjzUzanWlCa/LgcR0lNfovTEi6lcIkzesnM/R8RlN0INGp3h4DHoDsE5YRvQyiKiRSMzikRAOS2WoqoZWu41K7RwzlOOAVDMMMHhIGvFlRxJFrKYW0ep0IYgC3SDh4b1lTJjNfENsrazOAMAw680mPuW+8lFno1P4XDigRhOiwQAyJK7TbsNS/PaA7giAIAhYz2yRgBIfsVA8wIetPG6FAqhdNrC5u0f+TUyHgyMTDDToEt/ftQsEvW4EPG5OZcrvw0mlimarTXkPfpXPcNlQoGtjACgpryQXsPNtH/nvRXqBJpoKHMzGNkNB0Odls7LNyAYKpUq1dt1iuvB7fRDp9kr9D1xOFwkpoksXusmXaZWFn0coV89r/b6/AgwAkUENaQaRxswAAAAASUVORK5CYII=", "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==", "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==", "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==", "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=", "dwg": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAoFJREFUeNpsU0tPE2EUPfOg006hD4rQh8WgbCSwkKgbF2owujaCiQsXxpX+D6MmbtXEsHCLmIAbE6NLo8YlGIxREIshIqVl+mQ6j8/zFVCb4UtuZua795577rl3FCEE5Bl79vPd5LHYiOP7cH1AUWi85ytmvlas1bJ9E5ryBntH3BpuP/X9i7ovkluuiE8N9SDepaLpCcRCCqa/VDCaMuIjSWP25Upl6n+QDoCz6Yh7KKzh3sI2LuUimPtRRyaqodj0MDloYiITSTi+mH29Wu0AUf9CsZPJoW5czJl48LmCc5kIKo5Al67B9gUGYxrun+5NnMlFZ+GKiQADj2a7AquseLIvjMv5KMaSBu4sWVir+3i8VIVKYSby0UTdFU8Znu8AYBHQgVOJEN5uOXi4UsdawwU0FSf6TaSoyw6DRvukPkgGWpDKy4F8a3jImCrqFDFn6rhKPR4VGnhvOTAY3WLcjifcQAsqRfhUc/Gq1MKNbBh9nIAMDjEppocxs9HCMktfGTCwP/oOBkUKNk/qF3pDYC6Ktk8RfWzyaaoKrqdDaBDwya8W1m0/CPCR3kFy7CcnmWQRUJqcRJFUKtTnPCeR71LwoeYF92CYyVnCFZpCTrRtCv5to2St8SOrKxiPqEEA4fkYT+mI0rdoeUiH1XZVuQPpsIKqw2QmfifTsnOABiWySlH9uU0Hh2MqjsZV5LtpPSoGeN9rKnhBX7ehoOSLIIPfnGONXGMMWN7xUfVldYDbjM3mrh5HCDgS17DhHgDQcIU+XbBxnDTn1x1UuQcJ9iv7l5Q5e1zLGri92EDJFnoAgHtcfr6wbbVXUqq193+0z97n3UJt1+d51n7aHwEGAAHXJoAuZNlzAAAAAElFTkSuQmCC", "dxf": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAo5JREFUeNpsU0trE1EYPfNMmtdoH2kDNmJbaVFcaBVFpAsREQpFwY0bu3HjQnTj1mVd+ANcuC3qQixmry6E0kWFVIQ+bKy2tbFJm3emyXTujGca+4DkwsedfLnn3POd77uS67rw1vC79ek7fZEzpu3AYUqS9tKQGZPLpa3VXP0uFCmJ/8t9OLC3q/uJbcs5bkIybvdHoMsSbLKENRmvU2WcNnTjRFD7ML1WGSPJHI6sA4KRWMAWVDPxLYex3iCmfpuIh1QsFSyMxQO4GvXHHwOJ6XWSyIck8v6HQsnjAxFc7vTj2VwBg4aG78VdBHQFCk+dbVcxMdwev9gTSEC455sIBOu2KLsoJFzqasP9vjCeDBlYqzn4VXXwarGKZN7Crd5QfLDT/7KpBM84c9fFUFjFp2wdk6smflRsKKqMa7EgfJJ3Ac2OKlit2pEmBTQfngdpnupoU7BUtRGiiTe7fXiRqmK+KuDn6TpvYogmBRJcrOwIJLIWxmM+dOsyLKryQAaJpjJ1/AxrGO3SqdZt7kKZJrzJWBg5piHENuY8vV6e0UOye1TyftvC5l+gZB8SHJTwpSx4q4JeTUKaxhXoR57h7Rn+3iFolJ3xvPhab6HgJG/pJ7jsNP4sUX+jZiCgEsWd/DjH5IrSYpBUAr0yHpzSoXKOP25a6OBhndh0zcX1qIYM2RIbu6i0KiHD5B/GTMHG03kTGpEL7H80wHFOWwhqDZ+SpkBOtCDYJDhZE4gRcKNbYynAqbCMbXpwpVPFbEng0aKJGbYzK1p4wIegLlcEPmdt+DjXbzcsxFlCynRwwVAwW6hjqeg0Zt521SYCWCJvbe0Un29UDx7Hgrs3IEitHXkw3jOv2fl92D8BBgAJeyqBh90ENQAAAABJRU5ErkJggg==", "eps": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmlJREFUeNp0U01vElEUPfMFCEVArdoSqEA0KV246UJdUJM2Lo2JK/9FjXu3utJqTNz4D9worrsQExbFpAFT0TYp0CZ8pIAiyMfMvBnvm2Foa9uX3Lw7c98979x77hNM0wRf7ufPsq7Z2SQYw2QJAkDxQalUZa3WI8hy3gmZr15bu+z8kILBkCeRCJi6bufKMji0NhwiCQR6iitdatTvQ5LyOLLEiWcYukm3m4Zhmbq1BX13FyoxuH7xAlbvpqKRK1fT0PWbRwEmDEyiy1QVg/V1GO02tO1tKLEY2PIy3KEAlmJRDLXb0TeZL+n9g4MHlLJ5HIBuYnSzXq+DlcsQLk/D9Hoh1WrIUjlPcpsYGQzS3LWoaBhvKeXWMQCDA1D9pt8PaXERUjwOjEZQFhZQp9L2yERiqYRCkPt/z58ogTGqHQLE1BLgUmC6XGD5AlipBIFKkbhanKHGYLBDqQ4ZED0OAbfLlo8OIxwGvhVgyTHlA3xkomjH/gegBgDURMv6faDbBZpN+/tHkUApkdTA/PwZAPxntwdUyjYA/+ZMqJHjLgM9iv/6zRt2GgMaIE21aVIjnSm0DGPfmhzyde0UAE2Dj+p7urKCPvkZku9eJILOSMUnkvVhIo7GYIB3xSKYdhoA1erXGVKXpvFxZwdBonnD68PQ7YEwM4O4xwMPxc8RYE87g4FIcz+kvfmnA0YzIJIy77/m0OCqsTkkCTysKPjJG3viLei63Gm3kCO6UWqcMejjxecMPmxsoFKtYop6UNirYL9Wtc5OHqzznIXHq1na7OfMJROcK8a6O7MjW7nfzZdrd7jzT4ABACh3NGsh3GcdAAAAAElFTkSuQmCC", "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", "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=", "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=", "h": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAbRJREFUeNqMUk1Lw0AQnf0woK0ttVqp0hwqVCl+UBERT94F7x78Cf4Uz179DT14F8WbYHtRkBYRLNqDtdaPZLObuLs1NGlXcWDJZGbey+x7QUEQgIqT07PL5WKhHL5H46J+22q22vsWpbWwdnR4oJ80LNiz2czGUjENhvj4ctIE4Wrj8XmPUlKL9nCYcOFzE9j1OKSTCdjdrtiLdr7KhVgzEvwW6krC92E6k4Kd9bJt57JV5vFK2KfRQRV+RAMkzxglYI1RaDy2dW1rpWRjQo5VGicYIorWVooFvQVCCAjG8Omw1MgG8AM0uSBUDSnCfk/IGCHwf3DCD/7UhOLBrFkDuep/hDUSSCv1iYo4rIfqGwmUSNJjfYbBcQKhZw0aBMA4B48LwBhBt/cON80HmM9NQ6fXg/Wlku4TwmNWDzaQqzHG+0PSKod5cH5Vh2RiAhYKc8DlV1UPSyuFMGygVlMg1/P6BC6DqXQK8jNZDXAYA1f21V34wMXYFaiyVw0rJyzLgs3VMkxOjGtix/V0XWChZ0cI2i/dzvXdfTd0Qf91BMPrhyNzgKfOmxaWypqaDXHfAgwAtCL8XOfF47gAAAAASUVORK5CYII=", "hpp": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAehJREFUeNqEUk1v00AUHK/XKf1yZdESVRBXjRSRFqMQVBA5Ic5I3DnwE/gpnLnyG3LgXglx4UDDLZS0RWkDLiRxSusk9u6GXSembmLgWZbX7+2bnZl92mg0goo3b3ffO/ncdvyfjHef6q2Dlvs8Q2ktzr16+SL60jhhZ69bO8X8ClLC7w9XdKJVG8fuM0r1WrJG4gXjgqU1D0MGc2kBTytl+7a9XmWcl1IB/hZKEhccq5aJJ/e3bTu7Wg1CVo7rNLlRhUh4oMnXoDoyhoHGyWmUe+QUbELIa7W8CjAFlMzdzeckCwFN06ATAn8QmDMMMGlMuwWucpoCHNe4jBkAMenjYvRPTyi53JvuwX8AplleAeBcRFrH6rXIxLim9I/pi3QA1RhKaYxdjkN8IwalCMIwWs9ljMkh0wzk+9M7w179C3LZNXxve2h+c3Hu91HeKmD/6zHOLnw83ilB1/V0CeqU3Q81LC/O41b2Btx2N2JVP2riR8eTUxmi0TzBwrKZMsqMoz8MsDh/DWuWhUBKURLKxQIeOMWoptYPnS1c+INZBkwISomOSsmBZS7B+3WOzZvrKGzkMAiGqNy7g+LmRkRfekBnANy2163PZXrSbrQ6vch19Xz8fPDHyL39QzkHBKedXjfu+y3AAGU37INBJto1AAAAAElFTkSuQmCC", "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", "ics": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAhRJREFUeNqEUkFPE0EU/mZ2dra7bLNpi2AxQFKalkJrohICiYkXPagXrx78Df4K48GDBzmQePLMhUODNxQ5ciEkJVqDtJGmMWrCATRbd2ecoS5u3aovmezsvu9973vfPiKlhI4XL7c2r5YL81LIELEghLA3u/udxmHnPmfGW/Wuv+LpwwdneRYBx7PeWK0wOYYhcXxyckGV1fdbnbuMsXcklqPRJQxFMKz4RxDCtVO4s3xlRjWoB0FYjlQPEEBieChwKCRGMx5uLtaKs1P5ei8IKlGa/YkXMXYtlTEDlsnw/mMXhBJcqxSK6vlcpa4PEpCooUyIqs5M6hG1o2CUwqA091cFcYLf/sjzcX75EiQIojI9779CTYR4jwTBf+r7GAwh0AxCiL6JMT/04vQ79u8aI2O/7Jzg69o6Go8ewycUahtBpADhHKLnK/eVbkMdtROWIv80NQ2sPhncA9Htwn+9hZG0rY6DzFwJl+7dhs0ZstUy8rduwPS/wd/ehmi3kwq4zTHiWUgXp+EuL8FvNvFl5Rn4xAS86iyI2kY3n0Mv48ByrOQmancdi8I0Kcj3U5iuA29xAelKCUHrEIayzltagG2E4IwkFaQgSC6lYI09iN0d8It5uNV5nG5sgJdKYC0G8WoTOZvBISFNEBxnsuzD3GX4vfDsszzqAu0jkJQDedCGbB6AWg54pYbPo+NGVPdTgAEAqQq70PytIL0AAAAASUVORK5CYII=", "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", "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", "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", "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==", "key": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAlZJREFUeNpsU11PE0EUPbM7u/2AtJUWU6qiiSYYo5EmmPDCD9AH46sx8cEnja/+CB989z+Y+MKPgMiDsYQACcbaWBBogYD92t2Zud7ZlQZsbzKZ3bl3zj3n3IwgItjYeDO3MlWme0bjUth8e8/fO2tHzx3XqUEk50uft+Ndnhdmc3SlfNPkVZT8Cy600DoIISvVfKYtlvfX1p66XmoIYsMZdjJQWvEFbbsC/S5g2QhSkKUK7rx6OzvzqLpsovAhaAxA3DUBQn2TUFsl7KwTfm4Z9DoO5LW7uPXi9Wxpfn7ZKF09vyPxX2iWcNRkKGZz0mQWKoNs8AVB6x1yRY2pYnc2LLofuXTxMgAlmlXIfngCxNxEzM+DPv6NQa2BygLgZyX6JT83ngHTN5GAL0WSoUQkSQnXkyBh/k0GegTAaldM20sTKvet+yyhIZApECamL0jUSe3oFChx3TopM4TeEQP2gc6BgGIwb4KGNXRhCkMGxgg2kJeybRiZM45D8W61qEAknSmpHStBhywu0nFVupSCTAcM4ECwqapv+NQ6LS9JGALoMIIoPYDjZiEL1xHtbyO39AQUDaA7R1AH23DSeSA4hv5RG/VAhxomPYP8sw9A4TaC9iHkjUWmrtGvbyC18BLe3GP0m3WW4I5hEBEnPIStXzyuFIxb4EkMEJ79Qa/xHbKxCdM7xeCwzUZOjgEwnuzt7qLz6T3cySmQP43uzjeIiTJM6io6W19B/NLCKMVGCzkCoLR/0lrfOI2fNy/huKC1FTsK/rbGNeMRC8dHpHByfu+vAAMAL/0jvAVZQl0AAAAASUVORK5CYII=", "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=", "mid": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnhJREFUeNpsU01PE1EUPdOZKWUotKUKFLEWkQ1EASGGxGBi4sIVrt27IixN/Cn+CxfVnQsXJiz8IAoqRBGEaMUUWzofnXkz781436QDkjKTyXuZe96595x3rxJFEeTzaKW6dmdpfIoxjuRRFECGn7/4Utvarj/syWgflU5s891qvGoJePJasfBgeSpnW+yEIJVS4DEBx3FzGT2qfvh0tJxOE4mCU0yy8X3BLdODRQTJZ5oMzYaD0UuDePzkbnnx1mjV9/lMp+izBKEIwQMOzvnJGoYhhBDgFKtMjmBl9XZ54WapSjLnknMnEkQYgflCVhKXLt+/dRMy2d5OHdVnPoxeHUtLV8u2w5/S78UzBJwLMC8gAsosIqy9/ga37WNmvgKVKmEkb7JSwI3pIdRq1kBXBZJAUKkb6wd49fIzbJthdn6cIhE0XUWbyP4cmshmdZAE0eUBD6gCN0DtZwM7Xw+RUlVEJCui7CmyPaS94zC06ZMedREERNA6djBWHsS9+9fRS3p9AraOXbhELMlUQju2G2O7JAQENk0XhpHG3MIVlEZzaDbdOKO8jWy/TraGsMmL4L8KTgnIfcfy4JBWeQNp0j10MQtB4EJOg6qFMI/bEH3pGNtF4LOAjHMxO1dGvW4jXzDi7Iw60TB0jJRyONhv4MdunbDneMA6BMPDA6iMFzExcQH9AxkUiwby+QzevtnF2OU8lBT1i8fOa2UO1/FwdGTHE2STHM/14+vlPOz0RxibKPfn9AHXZHBzYx866ZdTKkuVndhHuqenS1h/v4ffvxqyvbUuAtPizZ0Dp7X1fTs+FA9cMnWd4ZG90NOjomVFzeTcPwEGACDGeYddZX86AAAAAElFTkSuQmCC", "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==", "mp4": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnBJREFUeNpsk99r01AUx79psrTrujVtbceabnZs4DYRHSoMh6Dgq77rn+AfoA/+If4Bok+C0CfxVRDBh+I2NqZzrpS1DVvbtU3SJPcm8SSlsJlecsn9dT73nO85V/B9H0H78OLdt/LDlQ1uMYybIAgI9n99OWxoe83nkiz9hDDae330JvxL48O51Xxm/enNtKPbVwAh0Ec6kYpXat9Pnl2GBC02HrjM5Y7h4P8+7FtIFVJ49OrxUnl7ucIdfhv+BIDv+fBcj7p/tXMPrs2RXVTw4OX2UnFTrXCbbY7tpMsA13FDSDAOQ4gJEGUJLs0PPh9CkESsPrmxxEz2lra3rnpAt3G6adgdQhBpmeLkFodNmsjpOPoXBrQTDcmFFNS7i3MRDzzPCw/vva8ikU+COQxm14BBhvJcHLGpGPTOAJxxeLbrRgAkYujBdH4G5oWJWXUW19YL4XqunAMFhnq1BqWYgaY1MAHASQOiU96zKzkU76mwehaOvx6h9uMv7KFN3RopL4oTAI4HRh4wSl399xla+00YbR3yrIzM9SzSqgJJnoKcklGrH08CcJjnBtLLCsSEGGpSWJvHtDKNoFippsJ0ulIsDDUCCATMlBQkNuahEyiZTcLsmFBKaQxaOk53TlHeKkM70AjAooCghBOk9sKtIvqtPqS4FBaRnJSRX8tj2DOh3lFB5Qw2ZNFK5LRo6w4sKt2ggAzywidAMN/9uIPSZglBLDO5FF3mRD3wHE9qVRvoHrUpfn+UEQK0/7ShtwboHJ6jdH8RZxSC57hSVETb7e5/2u0FxqPHJow+8iZ4lYY2QGu3idhIxO7Y7p8AAwALCGZKEPBGCgAAAABJRU5ErkJggg==", "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==", "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==", "ods": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAetJREFUeNqMUj1IHEEU/i7u7Z23e8tGgneGQPw3hZDkkhQiSuwMQREba4uUgpVlCrvEQhurkCoWqcQQ0oTAaYKNqJygGEwgHCSB6Knn7eXcdX/GmdHVPWYFP3gw78173/vmvYkQQsAwNvckq96UnyIEh7/d4t7uUd/8y+85P+bXSX4grkhI6nJYPW7LrXpBK2YxiSoShhu4Buq1NPofDeqdrZ3Z4cl7D4J3UtA5VyVAlmJoru9Af2ZAp1lcCQ3nqgiuKmbY3l/BH+MnHM9GVLP0Ww3KNA33CQoQQnL834Fj74PUGkANEIkCSSsa8gQqgYTIcB0PVsXB318GInRiCVWCkpRFAs+j5gKlA4t29Ggh4d0t04FKt9PQqF4UFgumSEA8ApeaElilWbYRVy/lsns/N1QBkxtENF4jxPxcgcB1CZVOrvMteK5IQDtJJIGh++PcX9iYwWjXK37+vP0WdYk0Ht99jtX8JywWFkQChw4tc+cZcvlF7rMze+ubbxN40fMalRMDP/6twaiUeK7wlZ0TD0a5hLTWxo2d45KKprqHKJslTsy209s2wnMFBTYNZjc/oLt9gPvLOx+hxVJIKS2YW5pCbSyJTGMK775O8VyBwDJd2LTDl/X5i8v3S7NVw9vJb51tITDEUwEGANCx2/rXEEFFAAAAAElFTkSuQmCC", "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=", "otp": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAcJJREFUeNqMkssvA1EUxr+ZjkdbrfFKVD12ErYSRELY2fkH+BMsLcQaSwsrSzZi47EjJEQkEhYkFlhYSVtFpdqOqpk717l3jKZmiC+5mZlzv/s795wzCuccQncz3YeRBj4KHz0/RrOZe2NsZPP20o255zQ3EAxzEAC+6uzTw13G4TFQAakA/CWtIYbY0KBOrx7IvwDQqlHV1o3YxKTOvyAUvfQCfqmA3e4ikyS/zRAKvOot7eoSHEgZIHrCfQAfBqBaKQQDKScQAExd8emBANg+2U2CvNMkkgSqBmrCxFB8mujeoJBWwEqARcssKTAJEGrmaGrjqK1zvNknH4BtyxKl2VUpRxmj5W+x73q9AEaZrR/ND1EJluIpS3i9JQiA+a+hSq8HwJjTsLrRaWitPTCOlhEZn5N75sM1qigmlN+dB3u++Qao5W4TtbEXXIsiszGL4PA00itTsu6XnQWo0TjMTAJqfMDx/ryBJcaVzSNSH4fW0Q+rkIf5rsjRiid7yyN7uoXS3Zn0egE0NiORAN9bQ017D1Lri7CLlP2EDr3Rf7C/itzV2bfXA/igLDaRixfngFhSCooH2xVPCWBlwKcAAwBX1suA6te+hAAAAABJRU5ErkJggg==", "ots": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAfZJREFUeNqMUk1rE1EUPS8zmabJdDKB2glEwY9ExJYiBUEQpV25qgtBXfgbpEtXuujKf+AfEKRddOdOGHClbYVCvyKWaijT2mhjphk7Sd7Me76ZONp0EsiBYWbOvfe88+69hHOOAE9f3zTVnDKNHvhlsfqPw/rM0ovyWsRFdXJEpDIyRnSlVz0KSkmvabaJeXSJBEhgAJzTDNybmtUnS5Pmg/lrN07H5NM/f13FoMgpXDSuhiIiK3Qi6LUugX7FAbaPPsJqfIHHKCStqRsXVFPQuZgD9BBxjikSiRq41AAkgCQBzVf0+BWEBX7GBm0xgHHUqk1UbBuEcIydzyCZlOI9YEGuDxwduCCitS3Xh3viCZ4jrcq4PJ6DLHd67tjtuAAXib54dCPVEfQ5XIcik/0/2iDeOYz3ceCxrisMi904y0XiMQFfkB7lg6xFHwFxEqUMV0anUNBLWKm8xd3i4zBWOzmASx0UsiW831mA59Xjm+h7HCOygduXHqJatzA7Poey9QnXjTuoVD/j/sRcmDOWLgqnLC5A2wwST+Pn8T629lahSCo291bwu9XA7vcy3m2+gTaUR14thrk9BXasbdiOjSe3nmPpwys0xSi/HpbDd3bIQC6dx/q3ZbRb/j8BEi3Po5cTJpHI9CBNDEa++GyDBN9/BBgAwfDlCVUQaNAAAAAASUVORK5CYII=", "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==", "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=", "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==", "png": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmtJREFUeNpsU9tOE1EUXXPp0CAUWmJbC04xBANNTF+kKhG8fID6aqL/gPEj9E0lIf6Dj30HL03wxQtVIC0QKrWxNG1Dk9Z2Oj1zxn1m0oIZTnIyZ8/ee+211z5Hsm0bYg29fLGpxWIJWBYGS5IA8ncKhT9Wvf4Yqprtu+w3q85X7f9QxseD/pmZMZsxN9fnc5JNw0ACGGv6tPSvyvEDKEoWZ5Y8OHHObKpucw4B0t3agnl4CJPs2YkQVu4s61ORaBqMJc8CDBiIRhhVM9bXYdVqYAcH8M3NgS0tQQsFcfdKHEbvlr6WyaR/V6uPKPy7B4DT7lUq4MUipMlJ2MPDUKtVfKZ2nn/5BoNbkONxXeb8LYXe/A9AJLNWCxgdhZJagDI9DZg9qIkEytRSkdqTSFQtGILSbgc8LViM+tc0yPfukzIyOJ359k9YR0eQdB2KmBbpwXoM3Dod1SkD+scpEapCI5DdpsJhIJcjajQZagcjI+5oLe4VkeQnyiZgdIH2X6BJ7dSqQLfrggjw0AQwP+/GegCIHppNoFAgEMO1RZKo7BQgRi3yN05cnwdA0BQMAgF3C6pnbuNg92M9AFT1diSCh6kb+FGvo2MxnBB9ocZxp4Mns1cde213B81e7xwAcl4jkaa0IUSjUdLJwkL0Ej6VSvArCt7l81iku6GrKnYEU89VJlSJRmR0Dax+fI9suYxSo4HlWIw6M3FBlnD9YhiXabyOsOeIqG7TzDeIYo6EDGp+ZPb2kKKqH8h+mkxiI5/D1/19J3bwYPvPWXq2skkiJVxesqt0XzghpKM8nRVV2Lv2q9eLIvSfAAMAaacnllcFBmYAAAAASUVORK5CYII=", "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", "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==", "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==", "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", "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=", "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=", "rtf": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAe5JREFUeNqEU01PE0EYfnZmd5FSvgLYFuwWt9EgHyEaox68eDJevHvwJ/hTPHv1N/QgZ2NC4g3kUAQKFKGhjVKqRrvbnRlnht262FHfy+y8877PPM8z71pCCKh4/ebt+rJfXEz26Vjf2mnsN5rPKKWbVpx7+eK5Xu2kyMtNTd5d8MdhiJ9BOO7atFI9ajy1UyAqSPIRMR6ZmoNehNHMMB7fX/UWvEKFMbYKE8DfQnAhwRmmJkbx6M6S5+WmK2Evup2c9yUk2nnKA0XVcSiGXAe1k5beP1i+4RFCXqnPywB/AKVzK34RjHNYlgVKCH50w7EBBogbTa/AVM5SgBdn0gc2AMDjPsbFPz2xye9asweS6n+NTbG8BCCfUtLjff2WoVnVpAH6z6hMUtJE3EykYfpF4vUiL3QNS7FMeSAQRBHW3r1Hq91B+VoBQRji4+ExFsvz6Hz7jm7Yw5OH92AcJKW9G4SoHhzhy/lXbB98Qmm2oCXN5WawsV2TACEoJXqwTKOsb3BtR2ucmZxANpPB8JUhyPnHWDaDpfJ1eZFALzJJ4MKO5MEtv4TSXB7V/br8iQLMz+almRZWbvoo5q9qRlxwewCgeXbe3qrVO5ZkUD/9jJGRLPaOm6COi92TU1DbxYe9umRD0DrrtJO+XwIMABWp9nS+FgaoAAAAAElFTkSuQmCC", "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", "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=", "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", "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==", "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", "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==", "txt": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAeJJREFUeNp8UrtOG1EQPfsyXiyzBguIJSyChZBBEFCKpKHLo6egpErNn8CHgH8gkZIiTSIXLhJAWCgkoMgRMSiRBSK29z4y9+I1d/HCrFb3MTPnnjkzlpQSynY+fP70fGF2gQuByCz6lfdd9Uurfvrrjes6762eb3tzQ69uFJwPsqOPC+MBEmxxphi4tlU5OGmsOzaBWLc+O9oIIVhScidkyGZ8vH62nHtSKlaI4cse6TjAfSaFBBcco0EWqyvzubmpyQrj/FXk75cQaSEMeMXU8xykPA/Hjd/6/LRcyjEpt2i7HAe4A2TeLZWKUOJaVLxj27j813EHGKCXaAJExu/4BOdiAED08riQD2riOrexyRoYc3CvsAbLGAAjZga7vgZG23WMCdBvoxKJc36TRBlMiaa2JByjNqqD8qkYc1pjDK7abey+/YhrWlfKswhpiCR96aEU9o5+QE3g2ovVWDm2Sc22bBQm8vrVpbkS9r+doPr1EOWZaQ0yFoxg2PcREosEAI4uvZhJpzFMP+cSXRbq+043RManez+tNWKMI6GN0g0Z04HFR+NoNC/0yx717efZOSbzY3AcR4Op2AGA5p/W31r9e0vNgSrh9OwCrpeCkqvZuqTybnpRqx/r2CjvvwADAJC/7lzAzQmwAAAAAElFTkSuQmCC", "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==", "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", "xlsx": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmlJREFUeNpsU8tqFEEUPVXdPY/ueWZIoiYZiSYKYhJc6EbduHOhgijo3t/wH1z6B0JAhOyMILhxo4kJGk1ASTAxwWF0Mpp5dHc9vFUzYwidaoqmq+8959xzbzGtNcx69PTS26ETmQtS9r4Hy/xv7MW7jV+th5yzVcaYPX/++It9u4NAv+CVR6tBUUTqMJsDcRzjZOZM8W9ZLKx+/XDb4e5/kH5In0lpIYWGUaC0YTZnBCAEKoVR3L36oDo7NbsglZwbqD6iQKOXFMcKUVfBkBAoQhlD5xxMDp/HrSv3q1JgYW3z0x0KXzkCYJaRZljru23aHWTzLiamAyytv0O9UYdf5PArqlppBfMUfu4oALErqZBKcUxMFRCHEp0DgW5Lo4N9NIN1dF0XXsVFOUyPJTzo+WBANDidjp8tgHGG3c0DnJ4uIRf4cOCBaW5KjY8xkZL72xpJ9QcFz5bVqHUJGHZL2YtNmKi06YCyiVFb4s/vEKMTAf1p4edOG6mMi1zR6wEpdUwX+vLDtkCzHoK7ptcM6ayLmGajvtex4PliyoIkFRjmUEASelB2rXQRSfjUCT9PlWpmW21iTGzCAyEkUixPRqXhe2V4zKczbdmybgkpJ0cGOuA6Y2MTCsKoi5HsNK7N3MN+uwYaWbxYfoLLkzdxcew6lrYWaZhm8PHHG3zffp1UwJSHz9vvkU8PodbcQYYYS5lxYkxTkGdVDQdV1Js1qPgYD6JIuIE7gsXVefIhIuM05k7dwMbeMmh87a18ufIMaVYyprrJLgje2Nr+1tzYXANnDnr3zRhHj37Vvy2wpXHtNAd5/wQYAD6WMuT2CwoVAAAAAElFTkSuQmCC", "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=", "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", "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" } ================================================ FILE: lib/core/show-dir/index.js ================================================ 'use strict'; const styles = require('./styles'); const lastModifiedToString = require('./last-modified-to-string'); const permsToString = require('./perms-to-string'); const sizeToString = require('./size-to-string'); const sortFiles = require('./sort-files'); const fs = require('fs'); const path = require('path'); const he = require('he'); const etag = require('../etag'); const url = require('url'); const status = require('../status-handlers'); const supportedIcons = styles.icons; const css = styles.css; module.exports = (opts) => { // opts are parsed by opts.js, defaults already applied const cache = opts.cache; const root = path.resolve(opts.root); const baseDir = opts.baseDir; const humanReadable = opts.humanReadable; const hidePermissions = opts.hidePermissions; const handleError = opts.handleError; const showDotfiles = opts.showDotfiles; const si = opts.si; const weakEtags = opts.weakEtags; return function middleware(req, res, next) { // Figure out the path for the file from the given url const parsed = url.parse(req.url); const pathname = decodeURIComponent(parsed.pathname); const dir = path.normalize( path.join( root, path.relative( path.join('/', baseDir), pathname ) ) ); fs.stat(dir, (statErr, stat) => { if (statErr) { if (handleError) { status[500](res, next, { error: statErr }); } else { next(); } return; } // files are the listing of dir fs.readdir(dir, (readErr, _files) => { let files = _files; if (readErr) { if (handleError) { status[500](res, next, { error: readErr }); } else { next(); } return; } // Optionally exclude dotfiles from directory listing. if (!showDotfiles) { files = files.filter(filename => filename.slice(0, 1) !== '.'); } res.setHeader('content-type', 'text/html'); res.setHeader('etag', etag(stat, weakEtags)); res.setHeader('last-modified', (new Date(stat.mtime)).toUTCString()); res.setHeader('cache-control', cache); // A step before render() is called to gives items additional // information so that render() can deliver the best user experience // possible. function prerender(dirs, renderFiles, errs) { const filenamesThatExist = new Set(); // Putting filenames in a set first keeps us in O(n) time complexity for (let i=0; i < renderFiles.length; i++) { const [name, stat] = renderFiles[i]; filenamesThatExist.add(name); const renderOptions = {}; renderFiles[i] = [name, stat, renderOptions]; } // Set render options for compressed files for (const [name, _stat, renderOptions] of renderFiles) { if ( opts.brotli && ! opts.forceContentEncoding && name.endsWith('.br') ) { const uncompressedName = name.slice(0, -'.br'.length); if (filenamesThatExist.has(uncompressedName)) { continue; } renderOptions.uncompressedName = uncompressedName; } } for (const [name, _stat, renderOptions] of renderFiles) { if ( opts.gzip && ! opts.forceContentEncoding && name.endsWith('.gz') ) { const uncompressedName = name.slice(0, -'.gz'.length); if (filenamesThatExist.has(uncompressedName)) { continue; } renderOptions.uncompressedName = uncompressedName; } } render(dirs, renderFiles, errs); } function render(dirs, renderFiles, errs) { // each entry in the array is a [name, stat] tuple let html = `${[ '', '', ' ', ' ', ' ', ` Index of ${he.encode(pathname)}`, ` `, ' ', ' ', `

Index of ${he.encode(pathname)}

`, ].join('\n')}\n`; html += ''; const failed = false; const writeRow = (file) => { // render a row given a [name, stat, renderOptions] tuple const isDir = file[1].isDirectory && file[1].isDirectory(); let href = `./${encodeURIComponent(file[0])}`; // append trailing slash and query for dir entry if (isDir) { href += `/${he.encode((parsed.search) ? parsed.search : '')}`; } // Handle compressed files with uncompressed names let displayNameHTML; let fileSize = sizeToString(file[1], humanReadable, si); if (file[2] && file[2].uncompressedName) { // This is a compressed file, show both names with separate links const uncompressedName = he.encode(file[2].uncompressedName); const compressedName = he.encode(file[0]); const uncompressedHref = `./${encodeURIComponent(file[2].uncompressedName)}`; const asterisk = `*`; displayNameHTML = `${uncompressedName}` + `${asterisk} (${compressedName})`; fileSize += '*'; } else { // Regular file or directory displayNameHTML = `${he.encode(file[0]) + ((isDir) ? '/' : '')}`; } const ext = file[0].split('.').pop(); const classForNonDir = supportedIcons[ext] ? ext : '_page'; const iconClass = `icon-${isDir ? '_blank' : classForNonDir}`; // TODO: use stylessheets? html += `${'' + '`; if (!hidePermissions) { html += ``; } html += `` + `` + `` + '\n'; }; dirs.sort((a, b) => a[0].toString().localeCompare(b[0].toString())).forEach(writeRow); renderFiles.sort((a, b) => a.toString().localeCompare(b.toString())).forEach(writeRow); errs.sort((a, b) => a[0].toString().localeCompare(b[0].toString())).forEach(writeRow); html += '
(${permsToString(file[1])})${lastModifiedToString(file[1])}${fileSize}${displayNameHTML}
\n'; html += `
Node.js ${ process.version }/ http-server ` + `server running @ ${ he.encode(req.headers.host || '')}
\n` + '' ; if (!failed) { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(html); } } sortFiles(dir, files, (errs, dirs, sortedFiles) => { // It's possible to get stat errors for all sorts of reasons here. // Unfortunately, our two choices are to either bail completely, // or just truck along as though everything's cool. In this case, // I decided to just tack them on as "??!?" items along with dirs // and files. // // Whatever. // if it makes sense to, add a .. link if (path.resolve(dir, '..').slice(0, root.length) === root) { fs.stat(path.join(dir, '..'), (err, s) => { if (err) { if (handleError) { status[500](res, next, { error: err }); } else { next(); } return; } dirs.unshift(['..', s]); prerender(dirs, sortedFiles, errs); }); } else { prerender(dirs, sortedFiles, errs); } }); }); }); }; }; ================================================ FILE: lib/core/show-dir/last-modified-to-string.js ================================================ 'use strict'; module.exports = function lastModifiedToString(stat) { if (!stat.mtime) { // stat error (eg, broken symlink) return 'Unknown Date'; } const t = new Date(stat.mtime); return (('0' + (t.getDate())).slice(-2) + '-' + t.toLocaleString('default', { month: 'short' }) + '-' + t.getFullYear() + ' ' + ('0' + t.getHours()).slice(-2) + ':' + ('0' + t.getMinutes()).slice(-2)); }; ================================================ FILE: lib/core/show-dir/perms-to-string.js ================================================ 'use strict'; module.exports = function permsToString(stat) { if (!stat.isDirectory || !stat.mode) { return '????!!!???'; } const dir = stat.isDirectory() ? 'd' : '-'; const mode = stat.mode.toString(8); return dir + mode.slice(-3).split('').map(n => [ '---', '--x', '-w-', '-wx', 'r--', 'r-x', 'rw-', 'rwx', ][parseInt(n, 10)]).join(''); }; ================================================ FILE: lib/core/show-dir/size-to-string.js ================================================ 'use strict'; // given a file's stat, return the size of it in string // humanReadable: (boolean) whether to result is human readable // si: (boolean) whether to use si (1k = 1000), otherwise 1k = 1024 // adopted from http://stackoverflow.com/a/14919494/665507 module.exports = function sizeToString(stat, humanReadable, si) { if (stat.isDirectory && stat.isDirectory()) { return ''; } let bytes = stat.size; const threshold = si ? 1000 : 1024; if (!humanReadable || bytes < threshold) { return `${bytes}B`; } const units = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; let u = -1; do { bytes /= threshold; u += 1; } while (bytes >= threshold); let b = bytes.toFixed(1); if (isNaN(b)) b = '??'; return b + units[u]; }; ================================================ FILE: lib/core/show-dir/sort-files.js ================================================ 'use strict'; const fs = require('fs'); const path = require('path'); module.exports = function sortByIsDirectory(dir, paths, cb) { // take the listing file names in `dir` // returns directory and file array, each entry is // of the array a [name, stat] tuple let pending = paths.length; const errs = []; const dirs = []; const files = []; if (!pending) { cb(errs, dirs, files); return; } paths.forEach((file) => { fs.stat(path.join(dir, file), (err, s) => { if (err) { errs.push([file, err]); } else if (s.isDirectory()) { dirs.push([file, s]); } else { files.push([file, s]); } pending -= 1; if (pending === 0) { cb(errs, dirs, files); } }); }); }; ================================================ FILE: lib/core/show-dir/styles.js ================================================ 'use strict'; const icons = require('./icons.json'); const IMG_SIZE = 16; let css = `i.icon { display: block; height: ${IMG_SIZE}px; width: ${IMG_SIZE}px; background: no-repeat center; }\n`; css += 'table tr { white-space: nowrap; }\n'; css += 'td.perms {}\n'; css += 'td.file-size { text-align: right; padding-left: 1em; }\n'; css += 'td.display-name { padding-left: 1em; }\n'; css += ` @media (prefers-color-scheme: dark) { body { background-color: #303030; color: #efefef; } a { color: #ffff11; } } `; Object.keys(icons).forEach((key) => { css += `i.icon-${key} {\n`; css += ` background-image: url("data:image/png;base64,${icons[key]}");\n`; css += '}\n\n'; }); exports.icons = icons; exports.css = css; ================================================ FILE: lib/core/status-handlers.js ================================================ 'use strict'; const he = require('he'); // not modified exports['304'] = (res) => { res.statusCode = 304; res.end(); }; // access denied exports['403'] = (res, next) => { res.statusCode = 403; if (typeof next === 'function') { next(); } else if (res.writable) { res.setHeader('content-type', 'text/plain'); res.end('ACCESS DENIED'); } }; // disallowed method exports['405'] = (res, next, opts) => { res.statusCode = 405; if (typeof next === 'function') { next(); } else { res.setHeader('allow', (opts && opts.allow) || 'GET, HEAD'); res.end(); } }; // not found exports['404'] = (res, next) => { res.statusCode = 404; if (typeof next === 'function') { next(); } else if (res.writable) { res.setHeader('content-type', 'text/plain'); res.end('File not found. :('); } }; exports['416'] = (res, next, opts) => { res.statusCode = 416; res.setHeader('content-range', 'bytes */' + opts.size) if (typeof next === 'function') { next(); } else if (res.writable) { res.setHeader('content-type', 'text/plain'); res.end('Requested range not satisfiable'); } }; // flagrant error exports['500'] = (res, next, opts) => { res.statusCode = 500; try { res.setHeader('content-type', 'text/html'); } catch (e) { // errors may have triggered headers being sent already, make sure we don't hide the underlying error } const error = String(opts.error.stack || opts.error || 'No specified error'); const html = `${[ '', '', ' ', ' ', ' 500 Internal Server Error', ' ', ' ', '

', ` ${he.encode(error)}`, '

', ' ', '', ].join('\n')}\n`; res.end(html); }; // bad request exports['400'] = (res, next, opts) => { res.statusCode = 400; res.setHeader('content-type', 'text/html'); const error = opts && opts.error ? String(opts.error) : 'Malformed request.'; const html = `${[ '', '', ' ', ' ', ' 400 Bad Request', ' ', ' ', '

', ` ${he.encode(error)}`, '

', ' ', '', ].join('\n')}\n`; res.end(html); }; ================================================ FILE: lib/http-server.js ================================================ 'use strict'; var fs = require('fs'), union = require('union'), httpServerCore = require('./core'), auth = require('basic-auth'), httpProxy = require('http-proxy'), corser = require('corser'), secureCompare = require('secure-compare'); var { minimatch } = require('minimatch'); // // Remark: backwards compatibility for previous // case convention of HTTP // exports.HttpServer = exports.HTTPServer = HttpServer; /** * Returns a new instance of HttpServer with the * specified `options`. */ exports.createServer = function (options) { return new HttpServer(options); }; /** * Constructor function for the HttpServer object * which is responsible for serving static files along * with other HTTP-related features. */ function HttpServer(options) { options = options || {}; var proxyAll = options.proxyAll === true || options.proxyAll === 'true'; if (proxyAll && typeof options.proxy !== 'string') { throw new Error('proxyAll option requires "proxy" to be configured'); } if (options.root) { this.root = options.root; } else { try { // eslint-disable-next-line no-sync fs.lstatSync('./public'); this.root = './public'; } catch (err) { this.root = './'; } } // CRLF injection prevention for ( const [key, value] of Object.entries(options.headers || {}) ) { if (typeof key !== 'string' || typeof value !== 'string') { throw new Error('Header is not a string or contains CRLF'); } if (key.includes('\r') || key.includes('\n') || value.includes('\r') || value.includes('\n')) { throw new Error('Header is not a string or contains CRLF'); } } this.headers = options.headers || {}; this.headers['Accept-Ranges'] = 'bytes'; this.cache = ( // eslint-disable-next-line no-nested-ternary options.cache === undefined ? 3600 : // -1 is a special case to turn off caching. // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Preventing_caching options.cache === -1 ? 'no-cache, no-store, must-revalidate' : options.cache // in seconds. ); this.showDir = options.showDir !== 'false'; this.dirOverrides404 = options.dirOverrides404; this.autoIndex = options.autoIndex !== 'false'; this.showDotfiles = options.showDotfiles; this.hidePermissions = options.hidePermissions; this.gzip = options.gzip === true; this.brotli = options.brotli === true; this.forceContentEncoding = options.forceContentEncoding === true; if (options.ext) { this.ext = options.ext === true ? 'html' : options.ext; } this.contentType = options.contentType || (this.ext === 'html' ? 'text/html' : 'application/octet-stream'); var before = options.before ? options.before.slice() : []; if (options.logFn) { before.push(function (req, res) { options.logFn(req, res); res.emit('next'); }); } if (options.username || options.password) { if (!options.username || !options.password) { throw new Error('Basic authentication requires both username and password to be specified'); } before.push(function (req, res) { var credentials = auth(req); // We perform these outside the if to avoid short-circuiting and giving // an attacker knowledge of whether the username is correct via a timing // attack. if (credentials) { // if credentials is defined, name and pass are guaranteed to be string // type var usernameEqual = secureCompare(options.username.toString(), credentials.name); var passwordEqual = secureCompare(options.password.toString(), credentials.pass); if (usernameEqual && passwordEqual) { return res.emit('next'); } } res.statusCode = 401; res.setHeader('WWW-Authenticate', 'Basic realm=""'); res.end('Access denied'); }); } if (options.allowedHosts) { before.push(function (req, res) { let host = req.headers && req.headers.host; if (host) { // don't include port number in host check host = host.split(':')[0]; } if (!host || !options.allowedHosts.includes(host)) { res.statusCode = 403; res.end('Access denied'); return; } return res.emit('next'); }); } if (options.coop) { this.headers['Cross-Origin-Opener-Policy'] = options.coopHeader || 'same-origin'; this.headers['Cross-Origin-Embedder-Policy'] = 'require-corp'; } // CORS configuration: // --cors enables CORS by setting Access-Control-Allow-Origin to '*' // --cors=header1,header2 also adds custom headers to Access-Control-Allow-Headers if (options.cors) { this.headers['Access-Control-Allow-Origin'] = '*'; this.headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, Range'; if (options.corsHeaders) { options.corsHeaders.split(/\s*,\s*/) .forEach(function (h) { this.headers['Access-Control-Allow-Headers'] += ', ' + h; }, this); } before.push(corser.create(options.corsHeaders ? { requestHeaders: this.headers['Access-Control-Allow-Headers'].split(/\s*,\s*/) } : null)); } if (options.privateNetworkAccess) { this.headers['Access-Control-Allow-Private-Network'] = true; } if (options.robots) { before.push(function (req, res) { if (req.url === '/robots.txt') { res.setHeader('Content-Type', 'text/plain'); var robots = options.robots === true ? 'User-agent: *\nDisallow: /' : options.robots.replace(/\\n/, '\n'); return res.end(robots); } res.emit('next'); }); } if (typeof options.proxyConfig === 'object') { var proxy = httpProxy.createProxyServer(); before.push(function (req, res, next) { for (var key of Object.keys(options.proxyConfig)) { if (!minimatch(req.url, key)) continue; req.proxy ??= {}; var matchConfig = options.proxyConfig[key]; if (matchConfig.pathRewrite) { Object.entries(matchConfig.pathRewrite).forEach(rewrite => { req.url = req.url.replace(new RegExp(rewrite[0]), rewrite[1]); }); } var configEntries = Object.entries(matchConfig).filter(entry => entry[0] !== "pathRewrite"); configEntries.forEach(entry => req.proxy[entry[0]] = entry[1]); break; } if (req.proxy) { if (options.logFn) { options.logFn(req, res); } proxy.web(req, res, req.proxy, function (err, req, res) { if (options.logFn) { options.logFn(req, res, { message: err.message, status: res.statusCode }); } res.emit('next'); }); } else { next(); } }); } if (!proxyAll) { before.push(httpServerCore({ root: this.root, baseDir: options.baseDir, cache: this.cache, showDir: this.showDir, showDotfiles: this.showDotfiles, hidePermissions: this.hidePermissions, autoIndex: this.autoIndex, defaultExt: this.ext, dirOverrides404: this.dirOverrides404, gzip: this.gzip, brotli: this.brotli, forceContentEncoding: this.forceContentEncoding, contentType: this.contentType, mimetypes: options.mimetypes, handleError: typeof options.proxy !== 'string' })); } if (typeof options.proxy === 'string') { var proxyOptions = options.proxyOptions || {}; if (proxyOptions.changeOrigin == null) { proxyOptions.changeOrigin = true; } var proxy = httpProxy.createProxyServer({ ...proxyOptions, target: options.proxy, }); before.push(function (req, res) { proxy.web(req, res, {}, function (err, req, res) { if (options.logFn) { options.logFn(req, res, { message: err.message, status: res.statusCode }); } res.emit('next'); }); }); } var serverOptions = { before: before, headers: this.headers, onError: function (err, req, res) { if (options.logFn) { options.logFn(req, res, err); } res.end(); } }; if (options.https) { serverOptions.https = options.https; } this.server = serverOptions.https && serverOptions.https.passphrase // if passphrase is set, shim must be used as union does not support ? require('./shims/https-server-shim')(serverOptions) : union.createServer(serverOptions); if (isNaN(options.timeout) || isNaN(parseFloat(options.timeout))) { this.server.setTimeout(120); } else { // set custom timeout only if options.timeout is a numeric string this.server.setTimeout(Math.max(0, Number(options.timeout))); } if (typeof options.proxy === 'string' && options.websocket) { this.server.on('upgrade', function (request, socket, head) { proxy.ws(request, socket, head, { target: options.proxy, changeOrigin: true }, function (err, req, res) { if (options.logFn) { options.logFn(req, res, { message: err?.message, status: res?.statusCode }); } res.emit('next'); }); }); } } HttpServer.prototype.listen = function () { this.server.listen.apply(this.server, arguments); }; HttpServer.prototype.close = function () { return this.server.close(); }; HttpServer.prototype.address = function () { return this.server.address(); }; ================================================ FILE: lib/shims/https-server-shim.js ================================================ /* eslint-disable no-process-env */ /* eslint-disable no-sync */ var https = require('https'); var fs = require('fs'); var core = require('union/lib/core'); var RoutingStream = require('union/lib/routing-stream'); module.exports = function (options) { var isArray = Array.isArray(options.after); var credentials; if (!options) { throw new Error('options is required to create a server'); } function requestHandler(req, res) { var routingStream = new RoutingStream({ before: options.before, buffer: options.buffer, after: isArray && options.after.map(function (After) { return new After(); }), request: req, response: res, limit: options.limit, headers: options.headers }); routingStream.on('error', function (err) { var fn = options.onError || core.errorHandler; fn(err, routingStream, routingStream.target, function () { routingStream.target.emit('next'); }); }); req.pipe(routingStream); } var serverOptions; serverOptions = options.https; if (!serverOptions.key || !serverOptions.cert) { throw new Error( 'Both options key and cert are required.' ); } credentials = { key: fs.readFileSync(serverOptions.key), cert: fs.readFileSync(serverOptions.cert), passphrase: process.env.NODE_HTTP_SERVER_SSL_PASSPHRASE }; if (serverOptions.ca) { serverOptions.ca = !Array.isArray(serverOptions.ca) ? [serverOptions.ca] : serverOptions.ca; credentials.ca = serverOptions.ca.map(function (ca) { return fs.readFileSync(ca); }); } return https.createServer(credentials, requestHandler); }; ================================================ FILE: package.json ================================================ { "name": "http-server", "version": "14.1.2", "description": "A simple zero-configuration command-line http server", "main": "./lib/http-server", "repository": { "type": "git", "url": "git://github.com/http-party/http-server.git" }, "keywords": [ "cli", "command", "static", "http", "https", "http-server", "https-server", "server" ], "scripts": { "start": "node ./bin/http-server", "test": "tap --reporter=terse --allow-incomplete-coverage test/*.test.js", "test-watch": "tap --reporter=terse --allow-incomplete-coverage --watch test/*.test.js" }, "files": [ "lib", "bin", "doc" ], "man": "./doc/http-server.1", "engines": { "node": ">=16.20.2" }, "contributors": [ { "name": "Charlie Robbins", "email": "charlie.robbins@gmail.com" }, { "name": "Marak Squires", "email": "marak.squires@gmail.com" }, { "name": "Charlie McConnell", "email": "charlie@charlieistheman.com" }, { "name": "Joshua Holbrook", "email": "josh.holbrook@gmail.com" }, { "name": "Maciej Małecki", "email": "maciej.malecki@notimplemented.org" }, { "name": "Matthew Bergman", "email": "mzbphoto@gmail.com" }, { "name": "brad dunbar", "email": "dunbarb2@gmail.com" }, { "name": "Dominic Tarr" }, { "name": "Travis Person", "email": "travis.person@gmail.com" }, { "name": "Jinkwon Lee", "email": "master@bdyne.net" }, { "name": "BigBlueHat", "email": "byoung@bigbluehat.com" }, { "name": "Daniel Dalton", "email": "daltond2@hawkmail.newpaltz.edu" }, { "name": "Jade Michael Thornton", "email": "jademichael@jmthornton.net" }, { "name": "Jorens Merenjanu", "email": "jorensmerenjanu@gmail.com" } ], "dependencies": { "basic-auth": "^2.0.1", "chalk": "^4.1.2", "corser": "^2.0.1", "he": "^1.2.0", "html-encoding-sniffer": "^3.0.0", "http-proxy": "^1.18.1", "mime": "^1.6.0", "minimatch": "^10.1.1", "minimist": "^1.2.6", "opener": "^1.5.1", "portfinder": "^1.0.28", "secure-compare": "3.0.1", "union": "~0.5.0", "url-join": "^4.0.1" }, "devDependencies": { "eol": "^0.9.1", "eslint": "^4.19.1", "eslint-config-populist": "^4.2.0", "express": "^4.17.1", "request": "^2.88.2", "tap": "^21.0.1" }, "bugs": { "url": "https://github.com/http-party/http-server/issues" }, "license": "MIT", "preferGlobal": true, "bin": { "http-server": "./bin/http-server" } } ================================================ FILE: public/404.html ================================================ 404

404

Were you just making up filenames or what?

================================================ FILE: public/index.html ================================================ node.js http server

Serving up static files like they were turtles strapped to rockets.

================================================ FILE: test/304.test.js ================================================ 'use strict'; const test = require('tap').test; const ecstatic = require('../lib/core'); const http = require('http'); const request = require('request'); const path = require('path'); const portfinder = require('portfinder'); const root = `${__dirname}/public`; const baseDir = 'base'; test('304_not_modified_strong', (t) => { const file = 'a.txt'; const server = http.createServer( ecstatic({ root, gzip: true, baseDir, autoIndex: true, showDir: true, weakEtags: false, weakCompare: false, }) ); server.listen(0, () => { const port = server.address().port; const uri = `http://localhost:${port}${path.join('/', baseDir, file)}`; request.get({ uri, followRedirect: false, }, (err, res) => { if (err) { t.fail(err); } t.equal(res.statusCode, 200, 'first request should be a 200'); request.get({ uri, followRedirect: false, headers: { 'if-modified-since': res.headers['last-modified'] }, }, (err2, res2) => { if (err2) { t.fail(err2); } t.equal(res2.statusCode, 304, 'second request should be a 304'); t.equal(res2.headers.etag.indexOf('"'), 0, 'should return a strong etag'); server.close(); setTimeout(() => { t.end(); }, 0); }); }); }); }); test('304_not_modified_weak', (t) => { const file = 'b.txt'; const server = http.createServer( ecstatic({ root, gzip: true, baseDir, autoIndex: true, showDir: true, weakEtags: true, weakCompare: false, }) ); server.listen(0, () => { const port = server.address().port; const uri = `http://localhost:${port}${path.join('/', baseDir, file)}`; const now = (new Date()).toString(); request.get({ uri, followRedirect: false, }, (err, res) => { if (err) { t.fail(err); } t.equal(res.statusCode, 200, 'first request should be a 200'); request.get({ uri, followRedirect: false, headers: { 'if-modified-since': now }, }, (err2, res2) => { if (err2) t.fail(err2); t.equal(res2.statusCode, 304, 'second request should be a 304'); t.equal(res2.headers.etag.indexOf('W/'), 0, 'should return a weak etag'); server.close(); setTimeout(() => { t.end(); }, 0); }); }); }); }); test('304_not_modified_strong_compare', (t) => { const file = 'b.txt'; const server = http.createServer( ecstatic({ root, gzip: true, baseDir, autoIndex: true, showDir: true, weakEtags: false, weakCompare: false, }) ); server.listen(0, () => { const port = server.address().port; const uri = `http://localhost:${port}${path.join('/', baseDir, file)}`; const now = (new Date()).toString(); let etag = null; request.get({ uri, followRedirect: false, }, (err, res) => { if (err) { t.fail(err); } t.equal(res.statusCode, 200, 'first request should be a 200'); etag = res.headers.etag; request.get({ uri, followRedirect: false, headers: { 'if-modified-since': now, 'if-none-match': etag }, }, (err2, res2) => { if (err2) { t.fail(err2); } t.equal(res2.statusCode, 304, 'second request with a strong etag should be 304'); request.get({ uri, followRedirect: false, headers: { 'if-modified-since': now, 'if-none-match': `W/${etag}` }, }, (err3, res3) => { if (err3) { t.fail(err3); } // Note that if both if-modified-since and if-none-match are // provided, the server MUST NOT return a response status of 304 // unless doing so is consistent with all of the conditional // header fields in the request // https://www.ietf.org/rfc/rfc2616.txt t.equal(res3.statusCode, 200, 'third request with a weak etag should be 200'); server.close(); setTimeout(() => { t.end(); }, 0); }); }); }); }); }); test('304_not_modified_weak_compare', (t) => { const file = 'c.js'; const server = http.createServer( ecstatic({ root, gzip: true, baseDir, autoIndex: true, showDir: true, weakEtags: false, }) ); server.listen(0, () => { const port = server.address().port; const uri = `http://localhost:${port}${path.join('/', baseDir, file)}`; const now = (new Date()).toString(); let etag = null; request.get({ uri, followRedirect: false, }, (err, res) => { if (err) { t.fail(err); } t.equal(res.statusCode, 200, 'first request should be a 200'); etag = res.headers.etag; request.get({ uri, followRedirect: false, headers: { 'if-modified-since': now, 'if-none-match': etag }, }, (err2, res2) => { if (err2) { t.fail(err2); } t.equal(res2.statusCode, 304, 'second request with a strong etag should be 304'); request.get({ uri, followRedirect: false, headers: { 'if-modified-since': now, 'if-none-match': `W/${etag}` }, }, (err3, res3) => { if (err3) { t.fail(err3); } t.equal(res3.statusCode, 304, 'third request with a weak etag should be 304'); server.close(); setTimeout(() => { t.end(); }, 0); }); }); }); }); }); ================================================ FILE: test/accept-encoding.test.js ================================================ 'use strict'; const test = require('tap').test; const ecstatic = require('../lib/core'); const http = require('http'); const request = require('request'); const root = `${__dirname}/public`; test('properly handles whitespace in accept-encoding', (t) => { t.plan(3); const server = http.createServer(ecstatic({ root, autoIndex: true, gzip: true })); server.listen(() => { const port = server.address().port; const options = { uri: `http://localhost:${port}/gzip`, headers: { 'accept-encoding': ' gzip, deflate' } }; request.get(options, (err, res) => { t.error(err); t.equal(res.statusCode, 200); t.equal(res.headers['content-encoding'], 'gzip'); }); }); t.once('end', () => { server.close(); }); }); test('properly handles single accept-encoding entry', (t) => { t.plan(3); const server = http.createServer(ecstatic({ root, autoIndex: true, gzip: true })); server.listen(() => { const port = server.address().port; const options = { uri: `http://localhost:${port}/gzip`, headers: { 'accept-encoding': 'gzip' } }; request.get(options, (err, res) => { t.error(err); t.equal(res.statusCode, 200); t.equal(res.headers['content-encoding'], 'gzip'); }); }); t.once('end', () => { server.close(); }); }); ================================================ FILE: test/allowed-hosts.test.js ================================================ const test = require('tap').test const path = require('path') const httpServer = require('../lib/http-server') const request = require('request'); // Prevent errors from being swallowed process.on('uncaughtException', console.error) test('allowed hosts functionality', (t) => { t.plan(4); new Promise((resolve) => { const server = httpServer.createServer({ root: path.join(__dirname, 'fixtures'), allowedHosts: ['localhost'], }) server.listen(0, async () => { console.log('server listening on port', server.address().port) const port = server.address().port const url = `http://localhost:${port}` try { await new Promise((resolve, reject) => { request.get({ url, headers: { Host: 'example.com' } }, (err, res) => { console.log('response', err) t.error(err); t.equal(res.statusCode, 403); resolve(); }) }) await new Promise((resolve, reject) => { request.get({ url, headers: { Host: 'localhost' } }, (err, res) => { console.log('response', err) t.error(err); t.equal(res.statusCode, 200); resolve(); }) }) } catch (err) { t.fail(`allowed hosts test failed: ${err.message}`) } finally { server.close() } }) }) }) ================================================ FILE: test/cache.test.js ================================================ 'use strict'; const test = require('tap').test; const http = require('http'); const request = require('request'); const ecstatic = require('../lib/core'); test('custom cache option number', (t) => { let server = null; try { server = http.createServer(ecstatic({ root: `${__dirname}/public/`, cache: 3600, })); } catch (e) { t.fail(e.message); t.end(); } t.plan(3); server.listen(0, () => { const port = server.address().port; request.get(`http://localhost:${port}/a.txt`, (err, res) => { t.error(err); t.equal(res.statusCode, 200, 'a.txt should be found'); t.equal(res.headers['cache-control'], 'max-age=3600'); server.close(() => { t.end(); }); }); }); }); test('custom cache option string', (t) => { let server = null; try { server = http.createServer(ecstatic({ root: `${__dirname}/public/`, cache: 'max-whatever=3600', })); } catch (e) { t.fail(e.message); t.end(); } t.plan(3); server.listen(0, () => { const port = server.address().port; request.get(`http://localhost:${port}/a.txt`, (err, res) => { t.error(err); t.equal(res.statusCode, 200, 'a.txt should be found'); t.equal(res.headers['cache-control'], 'max-whatever=3600'); server.close(() => { t.end(); }); }); }); }); test('custom cache option function returning a number', (t) => { let i = 0; let server = null; try { server = http.createServer(ecstatic({ root: `${__dirname}/public/`, cache() { i += 1; return i; }, })); } catch (e) { t.fail(e.message); t.end(); } t.plan(6); server.listen(0, () => { const port = server.address().port; request.get(`http://localhost:${port}/a.txt`, (err, res) => { t.error(err); t.equal(res.statusCode, 200, 'a.txt should be found'); t.equal(res.headers['cache-control'], 'max-age=1'); request.get(`http://localhost:${port}/a.txt`, (err2, res2) => { t.error(err2); t.equal(res2.statusCode, 200, 'a.txt should be found'); t.equal(res2.headers['cache-control'], 'max-age=2'); server.close(() => { t.end(); }); }); }); }); }); test('custom cache option function returning a string', (t) => { let i = 0; let server = null; try { server = http.createServer(ecstatic({ root: `${__dirname}/public/`, cache() { i += 1; return `max-meh=${i}`; }, })); } catch (e) { t.fail(e.message); t.end(); } t.plan(6); server.listen(0, () => { const port = server.address().port; request.get(`http://localhost:${port}/a.txt`, (err, res) => { t.error(err); t.equal(res.statusCode, 200, 'a.txt should be found'); t.equal(res.headers['cache-control'], 'max-meh=1'); request.get(`http://localhost:${port}/a.txt`, (err2, res2) => { t.error(err2); t.equal(res2.statusCode, 200, 'a.txt should be found'); t.equal(res2.headers['cache-control'], 'max-meh=2'); server.close(() => { t.end(); }); }); }); }); }); ================================================ FILE: test/check-headers.js ================================================ const request = require('request'); module.exports = (t, server, path, check) => { server.listen(() => { const port = server.address().port; const uri = `http://localhost:${port}/${path}`; request.get({ uri }, (err, res) => { t.error(err); t.equal(res.statusCode, 200); check(t, res.headers); }); }); t.once('end', () => { server.close(); }); } ================================================ FILE: test/cli.test.js ================================================ 'use strict'; /* this test suit is incomplete 2015-12-18 */ const test = require('tap').test; const request = require('request'); const spawn = require('child_process').spawn; const path = require('path'); const portfinder = require('portfinder'); const httpServer = require('../lib/http-server'); const node = process.execPath; function startServer(args) { return spawn(node, [require.resolve('../bin/http-server')].concat(args)); } function checkServerIsRunning(url, msg, t, _cb) { if (!msg.toString().match(/Starting up/)) { return; } t.pass('http-server started'); const cb = _cb || (() => {}); request(url, (err, res) => { if (!err && res.statusCode !== 500) { t.pass('a successful request from the server was made'); cb(null, res); } else { t.fail(`the server could not be reached @ ${url}`); cb(err); } }); } function tearDown(ps, t) { t.teardown(() => { ps.kill('SIGTERM'); }); } const getPort = () => new Promise((resolve, reject) => { portfinder.getPort((err, port) => { if (err) reject(err); resolve(port); }); }); const stripAnsi = (str) => str.replace(/\u001b\[[0-9;]*m/g, ''); test('setting port via cli - custom port', (t) => { t.plan(2); getPort().then((port) => { const options = ['.', '--port', port]; const server = startServer(options); tearDown(server, t); server.stdout.on('data', (msg) => { checkServerIsRunning(`http://localhost:${port}`, msg, t); }); }); }); test('setting mimeTypes via cli - .types file', (t) => { t.plan(4); getPort().then((port) => { const root = path.resolve(__dirname, 'public/'); const pathMimetypeFile = path.resolve(__dirname, 'fixtures/custom_mime_type.types'); const options = [root, '--port', port, '--mimetypes', pathMimetypeFile]; const server = startServer(options); tearDown(server, t); server.stdout.on('data', (msg) => { checkServerIsRunning(`http://localhost:${port}/custom_mime_type.opml`, msg, t, (err, res) => { t.error(err); t.equal(res.headers['content-type'], 'application/secret'); }); }); }); }); test('setting mimeTypes via cli - directly', (t) => { t.plan(4); getPort().then((port) => { const root = path.resolve(__dirname, 'public/'); const mimeType = ['--mimetypes', '{ "application/x-my-type": ["opml"] }']; const options = [root, '--port', port].concat(mimeType); const server = startServer(options); // TODO: remove error handler tearDown(server, t); server.stdout.on('data', (msg) => { checkServerIsRunning(`http://localhost:${port}/custom_mime_type.opml`, msg, t, (err, res) => { t.error(err); t.equal(res.headers['content-type'], 'application/x-my-type'); }); }); }); }); test('--proxy requires you to specify a protocol', (t) => { t.plan(1); const options = ['.', '--proxy', 'google.com']; const server = startServer(options); tearDown(server, t); server.on('exit', (code) => { t.equal(code, 1); }); }); test('--proxy-all requires --proxy', (t) => { t.plan(1); const options = ['.', '--proxy-all', 'true']; const server = startServer(options); tearDown(server, t); server.on('exit', (code) => { t.equal(code, 1); }); }); test('--proxy-all does not consume following positional args', (t) => { t.plan(4); const root = path.resolve(__dirname, 'fixtures', 'root'); const targetServer = httpServer.createServer({ root }); targetServer.listen(0, () => { const targetPort = targetServer.address().port; getPort().then((port) => { const options = [ '--proxy', `http://localhost:${targetPort}`, '--proxy-all', root, '--port', port ]; const server = startServer(options); tearDown(server, t); t.teardown(() => targetServer.close()); let sawRootLog = false; server.stdout.on('data', (msg) => { const text = stripAnsi(msg.toString()); if (text.includes(root)) { sawRootLog = true; } checkServerIsRunning(`http://localhost:${port}`, msg, t, (err, res) => { if (err) { t.fail(err.toString()); return; } t.ok(sawRootLog, 'root path should remain positional argument'); t.equal(res.statusCode, 200, 'proxied request should succeed'); }); }); }); }); }); function doHeaderOptionTest(t, argv, obj) { getPort().then((port) => { const options = ['.', '--port', port].concat(argv); const server = startServer(options); tearDown(server, t); server.stdout.on('data', (msg) => { checkServerIsRunning(`http://localhost:${port}`, msg, t, (err, res) => { t.error(err); for (const [k, v] of Object.entries(obj)) { t.equal(res.headers[k], v, 'expected header value matches in response'); } }); }); }); } test('single --header option is applied', (t) => { t.plan(4); doHeaderOptionTest(t, ['--header=X-http-server-test-A: hello'], { 'x-http-server-test-a': 'hello' } ); }); test('single -H option is applied', (t) => { t.plan(4); doHeaderOptionTest(t, ['-H', 'X-http-server-test-A: hello'], { 'x-http-server-test-a': 'hello' } ); }); test('mix of multiple --header and -H options are applied', (t) => { t.plan(7); doHeaderOptionTest(t, [ '--header=X-http-server-test-A: Lorem ipsum dolor sit amet', '-H', 'X-http-server-test-B: consectetur=adipiscing; elit', '-H', 'X-http-server-test-C: c', '--header=X-http-server-test-D: d' ], { 'x-http-server-test-a': 'Lorem ipsum dolor sit amet', 'x-http-server-test-b': 'consectetur=adipiscing; elit', 'x-http-server-test-c': 'c', 'x-http-server-test-d': 'd' } ); }); test('empty header value is allowed (RFC 7230)', (t) => { t.plan(5); doHeaderOptionTest(t, ['-H', 'X-http-server-test-empty-a:', '-H', 'X-http-server-test-empty-b'], { 'x-http-server-test-empty-a': '', 'x-http-server-test-empty-b': '' } ); }); test('setting default content-type via cli', (t) => { t.plan(4); getPort().then((port) => { const root = path.resolve(__dirname, 'public/'); const options = [root, '--port', port, '--content-type', 'text/custom']; const server = startServer(options); tearDown(server, t); server.stdout.on('data', (msg) => { checkServerIsRunning(`http://localhost:${port}/f_f`, msg, t, (err, res) => { t.error(err); t.equal(res.headers['content-type'], 'text/custom; charset=UTF-8'); }); }); }); }); ================================================ FILE: test/compression.test.js ================================================ 'use strict'; const test = require('tap').test; const ecstatic = require('../lib/core'); const http = require('http'); const request = require('request'); const root = `${__dirname}/public`; test('serves brotli-encoded file when available', (t) => { t.plan(3); const server = http.createServer(ecstatic({ root, brotli: true, autoIndex: true })); server.listen(() => { const port = server.address().port; const options = { uri: `http://localhost:${port}/brotli`, headers: { 'accept-encoding': 'gzip, deflate, br' } }; request.get(options, (err, res) => { t.error(err); t.equal(res.statusCode, 200); t.equal(res.headers['content-encoding'], 'br'); }); }); t.once('end', () => { server.close(); }); }); test('serves gzip-encoded file when brotli not available', (t) => { t.plan(3); const server = http.createServer(ecstatic({ root, brotli: true, gzip: true, autoIndex: true })); server.listen(() => { const port = server.address().port; const options = { uri: `http://localhost:${port}/gzip`, headers: { 'accept-encoding': 'gzip, deflate, br' } }; request.get(options, (err, res) => { t.error(err); t.equal(res.statusCode, 200); t.equal(res.headers['content-encoding'], 'gzip'); }); }); t.once('end', () => { server.close(); }); }); test('serves gzip-encoded file when brotli not accepted', (t) => { t.plan(3); const server = http.createServer(ecstatic({ root, brotli: true, gzip: true, autoIndex: true })); server.listen(() => { const port = server.address().port; const options = { uri: `http://localhost:${port}/brotli`, headers: { 'accept-encoding': 'gzip, deflate' } }; request.get(options, (err, res) => { t.error(err); t.equal(res.statusCode, 200); t.equal(res.headers['content-encoding'], 'gzip'); }); }); t.once('end', () => { server.close(); }); }); test('serves gzip-encoded file when brotli not enabled', (t) => { t.plan(3); const server = http.createServer(ecstatic({ root, brotli: false, gzip: true, autoIndex: true })); server.listen(() => { const port = server.address().port; const options = { uri: `http://localhost:${port}/brotli`, headers: { 'accept-encoding': 'gzip, deflate, br' } }; request.get(options, (err, res) => { t.error(err); t.equal(res.statusCode, 200); t.equal(res.headers['content-encoding'], 'gzip'); }); }); t.once('end', () => { server.close(); }); }); test('serves unencoded file when compression not accepted', (t) => { t.plan(3); const server = http.createServer(ecstatic({ root, brotli: true, gzip: true, autoIndex: true })); server.listen(() => { const port = server.address().port; const options = { uri: `http://localhost:${port}/brotli`, headers: { 'accept-encoding': '' } }; request.get(options, (err, res) => { t.error(err); t.equal(res.statusCode, 200); t.equal(res.headers['content-encoding'], undefined); }); }); t.once('end', () => { server.close(); }); }); test('serves unencoded file when compression not enabled', (t) => { t.plan(3); const server = http.createServer(ecstatic({ root, brotli: false, gzip: false, autoIndex: true })); server.listen(() => { const port = server.address().port; const options = { uri: `http://localhost:${port}/brotli`, headers: { 'accept-encoding': 'gzip, deflate, br' } }; request.get(options, (err, res) => { t.error(err); t.equal(res.statusCode, 200); t.equal(res.headers['content-encoding'], undefined); }); }); t.once('end', () => { server.close(); }); }); ================================================ FILE: test/content-type.test.js ================================================ 'use strict'; const test = require('tap').test; const http = require('http'); const ecstatic = require('../lib/core'); const checkHeaders = require('./check-headers.js'); const root = `${__dirname}/public/`; test('global default contentType', (t) => { let server = null; try { server = http.createServer(ecstatic({ root, contentType: 'text/plain', })); } catch (e) { t.fail(e.message); t.end(); } t.plan(3); checkHeaders(t, server, 'f_f', (t, headers) => { t.equal(headers['content-type'], 'text/plain; charset=UTF-8'); }); }); test('content type text', (t) => { t.plan(3); const server = http.createServer( ecstatic({root}) ); checkHeaders(t, server, 'subdir/e.html', (t, headers) => { t.equal(headers['content-type'], 'text/html; charset=UTF-8'); }); }); test('content type binary', (t) => { t.plan(3); const server = http.createServer( ecstatic({root}) ); checkHeaders(t, server, 'subdir/app.wasm', (t, headers) => { t.equal(headers['content-type'], 'application/wasm'); }); }); test('charset arabic', (t) => { t.plan(3); const server = http.createServer( ecstatic({root}) ); checkHeaders(t, server, 'charset/arabic.html', (t, headers) => { t.equal(headers['content-type'], 'text/html; charset=ISO-8859-6'); }); }); test('charset Shift_JIS', (t) => { t.plan(3); const server = http.createServer( ecstatic({root}) ); checkHeaders(t, server, 'charset/shift_jis.html', (t, headers) => { t.equal(headers['content-type'], 'text/html; charset=Shift_JIS'); }); }); ================================================ FILE: test/coop.test.js ================================================ 'use strict'; const test = require('tap').test; const server = require('../lib/core'); const http = require('http'); const path = require('path'); const request = require('request'); const root = path.join(__dirname, 'public'); test('coop defaults to false', (t) => { t.plan(4); const httpServer = http.createServer( server({ root, autoIndex: true, defaultExt: 'html', }) ); httpServer.listen(() => { const port = httpServer.address().port; const uri = `http://localhost:${port}/subdir/index.html`; request.get({ uri }, (err, res) => { t.error(err); t.equal(res.statusCode, 200); t.type(res.headers['cross-origin-opener-policy'], 'undefined'); t.type(res.headers['cross-origin-embedder-policy'], 'undefined'); }); }); t.once('end', () => { httpServer.close(); }); }); test('coop set to false', (t) => { t.plan(4); const httpServer = http.createServer( server({ root, coop: false, autoIndex: true, defaultExt: 'html', }) ); httpServer.listen(() => { const port = httpServer.address().port; const uri = `http://localhost:${port}/subdir/index.html`; request.get({ uri }, (err, res) => { t.error(err); t.equal(res.statusCode, 200); t.type(res.headers['cross-origin-opener-policy'], 'undefined'); t.type(res.headers['cross-origin-embedder-policy'], 'undefined'); }); }); t.once('end', () => { httpServer.close(); }); }); test('coop set to true', (t) => { t.plan(4); const httpServer = http.createServer( server({ root, coop: true, autoIndex: true, defaultExt: 'html', }) ); httpServer.listen(() => { const port = httpServer.address().port; const uri = `http://localhost:${port}/subdir/index.html`; request.get({ uri }, (err, res) => { t.error(err); t.equal(res.statusCode, 200); t.equal(res.headers['cross-origin-opener-policy'], 'same-origin'); t.equal(res.headers['cross-origin-embedder-policy'], 'require-corp'); }); }); t.once('end', () => { httpServer.close(); }); }); test('COOP set to true', (t) => { t.plan(4); const httpServer = http.createServer( server({ root, COOP: true, autoIndex: true, defaultExt: 'html', }) ); httpServer.listen(() => { const port = httpServer.address().port; const uri = `http://localhost:${port}/subdir/index.html`; request.get({ uri }, (err, res) => { t.error(err); t.equal(res.statusCode, 200); t.equal(res.headers['cross-origin-opener-policy'], 'same-origin'); t.equal(res.headers['cross-origin-embedder-policy'], 'require-corp'); }); }); t.once('end', () => { httpServer.close(); }); }); ================================================ FILE: test/core-error.test.js ================================================ 'use strict'; const test = require('tap').test; const ecstatic = require('../lib/core'); const http = require('http'); const request = require('request'); const path = require('path'); const root = `${__dirname}/public`; const baseDir = 'base'; require('fs').mkdirSync(`${root}/emptyDir`, {recursive: true}); const cases = require('./fixtures/common-cases-error'); test('core', (t) => { require('portfinder').getPort((err, port) => { const filenames = Object.keys(cases); const server = http.createServer( ecstatic({ root, gzip: true, baseDir, autoIndex: true, showDir: true, handleError: false, }) ); server.listen(port, () => { let pending = filenames.length; filenames.forEach((file) => { const uri = `http://localhost:${port}${path.join('/', baseDir, file)}`; const headers = cases[file].headers || {}; request.get({ uri, followRedirect: false, headers, }, (err, res, body) => { if (err) { t.fail(err); } const r = cases[file]; t.equal(res.statusCode, r.code, `status code for \`${file}\``); if (r.type !== undefined) { t.equal( res.headers['content-type'].split(';')[0], r.type, `content-type for \`${file}\`` ); } if (r.body !== undefined) { t.equal(body, r.body, `body for \`${file}\``); } if (r.location !== undefined) { t.equal(res.headers.location, path.join('/', baseDir, r.location), `location for \`${file}\``); } pending -= 1; if (pending === 0) { server.close(); t.end(); } }); }); }); }); }); ================================================ FILE: test/core.test.js ================================================ 'use strict'; const test = require('tap').test; const ecstatic = require('../lib/core'); const http = require('http'); const request = require('request'); const path = require('path'); const eol = require('eol'); const root = `${__dirname}/public`; const baseDir = 'base'; require('fs').mkdirSync(`${root}/emptyDir`, {recursive: true}); const cases = require('./fixtures/common-cases'); test('core', (t) => { const filenames = Object.keys(cases); const server = http.createServer( ecstatic({ root, gzip: true, baseDir, autoIndex: true, showDir: true, defaultExt: 'html', handleError: true, }) ); server.listen(0, () => { const port = server.address().port; let pending = filenames.length; filenames.forEach((file) => { const uri = `http://localhost:${port}${path.join('/', baseDir, file)}`; const headers = cases[file].headers || {}; request.get({ uri, followRedirect: false, headers, }, (err, res, body) => { if (err) { t.fail(err); } const r = cases[file]; t.equal(res.statusCode, r.code, `status code for \`${file}\``); if (r.type !== undefined) { t.equal( res.headers['content-type'].split(';')[0], r.type, `content-type for \`${file}\`` ); } if (r.body !== undefined) { t.equal(eol.lf(body), r.body, `body for \`${file}\``); } if (r.location !== undefined) { t.equal(path.normalize(res.headers.location), path.join('/', baseDir, r.location), `location for \`${file}\``); } pending -= 1; if (pending === 0) { server.close(); t.end(); } }); }); }); }); ================================================ FILE: test/cors.test.js ================================================ 'use strict'; const test = require('tap').test; const server = require('../lib/core'); const http = require('http'); const path = require('path'); const request = require('request'); const root = path.join(__dirname, 'public'); test('cors defaults to false', (t) => { t.plan(4); const httpServer = http.createServer( server({ root, autoIndex: true, defaultExt: 'html', }) ); httpServer.listen(() => { const port = httpServer.address().port; const uri = `http://localhost:${port}/subdir/index.html`; request.get({ uri }, (err, res) => { t.error(err); t.equal(res.statusCode, 200); t.type(res.headers['access-control-allow-origin'], 'undefined'); t.type(res.headers['access-control-allow-headers'], 'undefined'); }); }); t.once('end', () => { httpServer.close(); }); }); test('cors set to false', (t) => { t.plan(4); const httpServer = http.createServer( server({ root, cors: false, autoIndex: true, defaultExt: 'html', }) ); httpServer.listen(() => { const port = httpServer.address().port; const uri = `http://localhost:${port}/subdir/index.html`; request.get({ uri }, (err, res) => { t.error(err); t.equal(res.statusCode, 200); t.type(res.headers['access-control-allow-origin'], 'undefined'); t.type(res.headers['access-control-allow-headers'], 'undefined'); }); }); t.once('end', () => { httpServer.close(); }); }); test('cors set to true', (t) => { t.plan(4); const httpServer = http.createServer( server({ root, cors: true, autoIndex: true, defaultExt: 'html', }) ); httpServer.listen(() => { const port = httpServer.address().port; const uri = `http://localhost:${port}/subdir/index.html`; request.get({ uri }, (err, res) => { t.error(err); t.equal(res.statusCode, 200); t.equal(res.headers['access-control-allow-origin'], '*'); t.equal(res.headers['access-control-allow-headers'], 'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since'); }); }); t.once('end', () => { httpServer.close(); }); }); test('CORS set to true', (t) => { t.plan(4); const httpServer = http.createServer( server({ root, CORS: true, autoIndex: true, defaultExt: 'html', }) ); httpServer.listen(() => { const port = httpServer.address().port; const uri = `http://localhost:${port}/subdir/index.html`; request.get({ uri }, (err, res) => { t.error(err); t.equal(res.statusCode, 200); t.equal(res.headers['access-control-allow-origin'], '*'); t.equal(res.headers['access-control-allow-headers'], 'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since'); }); }); t.once('end', () => { httpServer.close(); }); }); ================================================ FILE: test/custom-content-type-file-secret.test.js ================================================ 'use strict'; const test = require('tap').test; const http = require('http'); const request = require('request'); const ecstatic = require('../lib/core'); test('custom contentType via .types file', (t) => { let server = null; try { server = http.createServer(ecstatic({ root: `${__dirname}/public/`, mimetypes: `${__dirname}/fixtures/custom_mime_type.types`, })); } catch (e) { t.fail(e.message); t.end(); } t.plan(3); server.listen(0, () => { const port = server.address().port; request.get(`http://localhost:${port}/custom_mime_type.opml`, (err, res) => { t.error(err); t.equal(res.statusCode, 200, 'custom_mime_type.opml should be found'); t.equal(res.headers['content-type'], 'application/secret'); server.close(() => { t.end(); }); }); }); }); ================================================ FILE: test/custom-content-type-file.test.js ================================================ 'use strict'; const test = require('tap').test; const http = require('http'); const request = require('request'); const ecstatic = require('../lib/core'); function setup(opts) { return http.createServer(ecstatic(opts)); } test('throws when custom contentType .types file does not exist', (t) => { t.plan(1); t.throws( setup.bind(null, { root: `${__dirname}/public/`, mimeTypes: 'this_file_does_not_exist.types', }) ); }); test('custom contentType via .types file', (t) => { let server = null; try { server = setup({ root: `${__dirname}/public`, 'mime-types': `${__dirname}/public/custom_mime_type.types`, }); } catch (e) { t.fail(e.message); t.end(); } t.plan(3); server.listen(0, () => { const port = server.address().port; request.get(`http://localhost:${port}/custom_mime_type.opml`, (err, res) => { t.error(err); t.equal(res.statusCode, 200, 'custom_mime_type.opml should be found'); t.equal(res.headers['content-type'], 'application/foo'); server.close(() => { t.end(); }); }); }); }); ================================================ FILE: test/custom-content-type.test.js ================================================ 'use strict'; const test = require('tap').test; const http = require('http'); const request = require('request'); const ecstatic = require('../lib/core'); test('custom contentType', (t) => { let server = null; try { server = http.createServer(ecstatic({ root: `${__dirname}/public/`, mimetype: { 'application/jon': ['opml'], }, })); } catch (e) { t.fail(e.message); t.end(); } t.plan(3); server.listen(0, () => { const port = server.address().port; request.get(`http://localhost:${port}/custom_mime_type.opml`, (err, res) => { t.error(err); t.equal(res.statusCode, 200, 'custom_mime_type.opml should be found'); t.equal(res.headers['content-type'], 'application/jon'); server.close(() => { t.end(); }); }); }); }); ================================================ FILE: test/default-default-ext.test.js ================================================ 'use strict'; const test = require('tap').test; const ecstatic = require('../lib/core'); const http = require('http'); const request = require('request'); const eol = require('eol'); test('default defaultExt', (t) => { t.plan(3); const server = http.createServer(ecstatic(`${__dirname}/public/subdir`)); server.listen(0, () => { const port = server.address().port; request.get(`http://localhost:${port}`, (err, res, body) => { t.error(err); t.equal(res.statusCode, 200); t.equal(eol.lf(body), 'index!!!\n'); server.close(() => { t.end(); }); }); }); }); ================================================ FILE: test/dir-overrides-404.test.js ================================================ 'use strict'; const test = require('tap').test; const http = require('http'); const ecstatic = require('../lib/core'); const request = require('request'); const showDir = require('../lib/core/show-dir'); const root = `${__dirname}/public/dir-overrides-404`; test('server should display directory if -d and --dir-overrides--404 flags are specified', (t) => { // require('portfinder').getPort((err, port) => { try { const server = http.createServer(ecstatic({root, showDir: true, dirOverrides404: true})); // t.plan(2); // t.on('end', () => { server.close(); }); server.listen(0, () => { const port = server.address().port; request.get(`http://localhost:${port}/directory/`, (err, res, body) => { if(err) { t.error(err); } // console.log(body); // console.log(res.statusCode); t.equal(res.statusCode, 200); console.log(body); t.equal(body.includes('Index of /directory/'), true); server.close(() => { t.end(); }); }); }) console.log('d'); } catch (e) { t.fail(e.message); t.end(); } // }); }); test('server should display 404.html if -d flag is specified but not --dir-overrides-404', (t) => { try { const server = http.createServer(ecstatic({root, showDir: true, dirOverrides404: false})); // t.plan(2); // t.on('end', () => { server.close(); }); server.listen(0, () => { const port = server.address().port; request.get(`http://localhost:${port}/directory/`, (err, res, body) => { if(err) { t.error(err); } t.equal(res.statusCode, 404); t.equal(body.includes('404file'), true); server.close(() => { t.end(); }); }); }) console.log('d'); } catch (e) { t.fail(e.message); t.end(); } }); ================================================ FILE: test/enotdir.test.js ================================================ 'use strict'; const test = require('tap').test; const ecstatic = require('../lib/core'); const http = require('http'); const request = require('request'); test('should handle ENOTDIR as 404', (t) => { t.plan(3); const server = http.createServer(ecstatic(`${__dirname}/public/subdir`)); t.on('end', () => { server.close(); }); server.listen(0, () => { const port = server.address().port; request.get(`http://localhost:${port}/index.html/hello`, (err, res, body) => { t.error(err); t.equal(res.statusCode, 404); t.equal(body, 'File not found. :('); }); }); }); ================================================ FILE: test/escaping.test.js ================================================ 'use strict'; const test = require('tap').test; const ecstatic = require('../lib/core'); const http = require('http'); const request = require('request'); const eol = require('eol'); test('escaping special characters', (t) => { const server = http.createServer(ecstatic(`${__dirname}/public`)); server.listen(0, () => { const port = server.address().port; request.get(`http://localhost:${port}/curimit%40gmail.com%20(40%25)`, (err, res, body) => { t.error(err); t.equal(res.statusCode, 200); t.equal(eol.lf(body), 'index!!!\n'); server.close(() => { t.end(); }); }); }); }); ================================================ FILE: test/express-error.test.js ================================================ 'use strict'; const test = require('tap').test; const ecstatic = require('../lib/core'); const http = require('http'); const express = require('express'); const request = require('request'); const path = require('path'); const root = `${__dirname}/public`; const baseDir = 'base'; require('fs').mkdirSync(`${root}/emptyDir`, {recursive: true}); const cases = require('./fixtures/common-cases-error'); test('express', (t) => { require('portfinder').getPort((err, port) => { const filenames = Object.keys(cases); const app = express(); app.use(ecstatic({ root, gzip: true, baseDir, autoIndex: true, showDir: true, cache: 'no-cache', handleError: false, })); const server = http.createServer(app); server.listen(port, () => { let pending = filenames.length; filenames.forEach((file) => { const uri = `http://localhost:${port}${path.join('/', baseDir, file)}`; const headers = cases[file].headers || {}; request.get({ uri, followRedirect: false, headers, }, (err, res, body) => { if (err) t.fail(err); const r = cases[file]; t.equal(res.statusCode, r.code, `status code for \`${file}\``); if (r.code === 200) { t.equal(res.headers['cache-control'], 'no-cache', `cache control for \`${file}\``); } if (r.type !== undefined) { t.equal( res.headers['content-type'].split(';')[0], r.type, `content-type for \`${file}\`` ); } if (r.body !== undefined) { t.equal(body, r.body, `body for \`${file}\``); } pending -= 1; if (pending === 0) { server.close(); t.end(); } }); }); }); }); }); ================================================ FILE: test/express.test.js ================================================ 'use strict'; const test = require('tap').test; const ecstatic = require('../lib/core'); const http = require('http'); const express = require('express'); const request = require('request'); const path = require('path'); const eol = require('eol'); const root = `${__dirname}/public`; const baseDir = 'base'; require('fs').mkdirSync(`${root}/emptyDir`, {recursive: true}); const cases = require('./fixtures/common-cases'); test('express', (t) => { const filenames = Object.keys(cases); const app = express(); app.use(ecstatic({ root, gzip: true, baseDir, autoIndex: true, showDir: true, defaultExt: 'html', cache: 'no-cache', handleError: true, })); const server = http.createServer(app); server.listen(0, () => { const port = server.address().port; let pending = filenames.length; filenames.forEach((file) => { const uri = `http://localhost:${port}${path.join('/', baseDir, file)}`; const headers = cases[file].headers || {}; request.get({ uri, followRedirect: false, headers, }, (err, res, body) => { if (err) t.fail(err); const r = cases[file]; t.equal(res.statusCode, r.code, `status code for \`${file}\``); if (r.code === 200) { t.equal(res.headers['cache-control'], 'no-cache', `cache control for \`${file}\``); } if (r.type !== undefined) { t.equal( res.headers['content-type'].split(';')[0], r.type, `content-type for \`${file}\`` ); } if (r.body !== undefined) { t.equal(eol.lf(body), r.body, `body for \`${file}\``); } pending -= 1; if (pending === 0) { server.close(); t.end(); } }); }); }); }); ================================================ FILE: test/fixtures/common-cases-error.js ================================================ 'use strict'; module.exports = { 404: { code: 200, }, 'something non-existant': { code: 404, }, }; if (require.main === module) { /* eslint-disable no-console */ console.log('ok 1 - test cases (error) included'); } ================================================ FILE: test/fixtures/common-cases.js ================================================ 'use strict'; const fs = require('fs'); const path = require('path'); const eol = require('eol'); module.exports = { 'a.txt': { code: 200, type: 'text/plain', body: 'A!!!\n', }, 'b.txt': { code: 200, type: 'text/plain', body: 'B!!!\n', }, 'c.js': { code: 200, type: 'application/javascript', body: 'console.log(\'C!!!\');\n', }, 'd.js': { code: 200, type: 'application/javascript', body: 'd.js\n', }, 'e.js': { code: 200, type: 'application/javascript', body: 'console.log(\'π!!!\');\n', }, 'subdir/e.html': { code: 200, type: 'text/html', body: 'e!!\n', }, // test for defaultExt 'subdir/e?foo=bar': { code: 200, type: 'text/html', body: 'e!!\n', }, // test for defaultExt with noisy query param 'subdir/e?foo=bar.ext': { code: 200, type: 'text/html', body: 'e!!\n', }, 'subdir/index.html': { code: 200, type: 'text/html', body: 'index!!!\n', }, subdir: { code: 302, location: 'subdir/', }, 'subdir?foo=bar': { code: 302, location: 'subdir/?foo=bar', }, // test for url-encoded paths '%E4%B8%AD%E6%96%87': { // '/中文' code: 302, location: '%E4%B8%AD%E6%96%87/', }, '%E4%B8%AD%E6%96%87?%E5%A4%AB=%E5%B7%B4': { // '中文?夫=巴' code: 302, location: '%E4%B8%AD%E6%96%87/?%E5%A4%AB=%E5%B7%B4', }, 'subdir/': { code: 200, type: 'text/html', body: 'index!!!\n', }, 404: { code: 200, type: 'text/html', body: '

404

\n', }, 'something-non-existant': { code: 404, type: 'text/html', body: '

404

\n', }, 'compress/foo.js': { code: 200, file: 'compress/foo.js.gz', headers: { 'accept-encoding': 'compress, gzip' }, body: fs.readFileSync(path.join(__dirname, '../', 'public', 'compress', 'foo.js.gz'), 'utf8'), }, // no accept-encoding of gzip, so serve regular file 'compress/foo_2.js': { code: 200, file: 'compress/foo_2.js', }, 'emptyDir/': { code: 404, body: '

404

\n', }, 'subdir_with space': { code: 302, location: 'subdir_with%20space/', }, 'subdir_with space/index.html': { code: 200, type: 'text/html', body: 'index :)\n', }, 'containsSymlink/': { code: 404, body: '

404

\n', }, 'gzip/': { code: 200, headers: { 'accept-encoding': 'compress, gzip' }, type: 'text/html', body: fs.readFileSync(path.join(__dirname, '../', 'public', 'gzip', 'index.html.gz'), 'utf8'), }, 'gzip/a': { code: 404, headers: { 'accept-encoding': 'compress, gzip' }, type: 'text/html', body: eol.lf(fs.readFileSync(path.join(__dirname, '../', 'public', '404.html.gz'), 'utf8')), }, 'gzip/real_ecstatic': { code: 200, file: 'gzip/real_ecstatic', headers: { 'accept-encoding': 'compress, gzip' }, type: 'application/octet-stream', body: fs.readFileSync(path.join(__dirname, '../', 'public', 'gzip', 'real_ecstatic.gz'), 'utf8'), }, 'gzip/real_ecstatic.gz': { code: 200, file: 'gzip/real_ecstatic.gz', headers: { 'accept-encoding': 'compress, gzip' }, type: 'application/gzip', body: fs.readFileSync(path.join(__dirname, '../', 'public', 'gzip', 'real_ecstatic.gz'), 'utf8'), }, 'gzip/fake_ecstatic': { code: 200, file: 'gzip/fake_ecstatic', type: 'application/octet-stream', headers: { 'accept-encoding': 'compress, gzip' }, body: 'ecstatic\n', }, 'gzip/fake_ecstatic.gz': { code: 200, file: 'gzip/fake_ecstatic.gz', type: 'application/gzip', headers: { 'accept-encoding': 'compress, gzip' }, body: fs.readFileSync(path.join(__dirname, '../', 'public', 'gzip', 'fake_ecstatic.gz'), 'utf8'), }, }; if (require.main === module) { /* eslint-disable no-console */ console.log('ok 1 - test cases included'); } ================================================ FILE: test/fixtures/custom_mime_type.types ================================================ # This file is an example of the Apache .types file format for describing mime-types. # Other example: http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types application/secret opml ================================================ FILE: test/fixtures/https/agent2-cert.pem ================================================ -----BEGIN CERTIFICATE----- MIIEIDCCAggCCQChRDh/XiBF+zANBgkqhkiG9w0BAQsFADBUMQswCQYDVQQGEwJ1 czETMBEGA1UECAwKV2FzaGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEeMBwGA1UE AwwVRHVtbXkgSW50ZXJtZWRpYXRlIENBMB4XDTE4MDYyMjIwMzEwNFoXDTMyMDIy OTIwMzEwNFowUDELMAkGA1UEBhMCdXMxEzARBgNVBAgMCldhc2hpbmd0b24xEDAO BgNVBAcMB1NlYXR0bGUxGjAYBgNVBAMMEWR1bW15LmV4YW1wbGUuY29tMIIBIjAN BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvSQq3d8AeZMTvtqZ13jWCckikyXJ SACvkGCQUCJqOceESbg6IHdRzQdoccE4P3sbvNsf9BlbdJKM+neCxabqKaU1PPje 4P0tHT57t6yJrMuUh9NxEz3Bgh1srNHVS7saKvwHmcKm79jc+wxlioPmEQvQagjn y7oTkyLt0sn4LGxBjrcv2JoHOC9f1pxX7l47MaiN0/ctRau7Nr3PFn+pkB4Yf6Z0 VyicVJbaUSz39Qo4HQWl1L2hiBP3CS1oKs2Yk0O1aOCMExWrhZQan+ZgHqL1rhgm kPpw2/zwwPt5Vf9CSakvHwg198EXuTTXtkzYduuIJAm8yp969iEIiG2xTwIDAQAB MA0GCSqGSIb3DQEBCwUAA4ICAQBnMSIo+kujkeXPh+iErFBmNtu/7EA+i/QnFPbN lSLngclYYBJAGQI+DhirJI8ghDi6vmlHB2THewDaOJXEKvC1czE8064wioIcA9HJ l3QJ3YYOFRctYdSHBU4TWdJbPgkLWDzYP5smjOfw8nDdr4WO/5jh9qRFcFpTFmQf DyU3xgWLsQnNK3qXLdJjWG75pEhHR+7TGo+Ob/RUho/1RX/P89Ux7/oVbzdKqqFu SErXAsjEIEFzWOM2uDOt6hrxDF6q+8/zudwQNEo422poEcTT9tDEFxMQ391CzZRi nozBm4igRn1f5S3YZzLI6VEUns0s76BNy2CzvFWn40DziTqNBExAMfFFj76wiMsX 6fTIdcvkaTBa0S9SZB0vN99qahBdcG17rt4RssMHVRH1Wn7NXMwe476L0yXZ6gO7 Z4uNAPxgaI3BRP75EPfslLutCLZ+BC4Zzu6MY0Salbpfl0Go462EhsKCxvYhE2Dg T477pICLfETZfA499Fd1tOaIsoLCrILAia/+Yd76uf94MuXUIqykDng/4H7xCc47 BZhNFJiPC6XHaXzN7NYSEUNX9VOwY8ncxKwtP6TXga96PdMUy/p98KIM8RZlDoxB Xy9dcZBFNn/zrqjW7R0CCWCUriDIFSmEP0wDZ91YYa6BVuJMb5uL/USkTLpjZS4/ HNGvug== -----END CERTIFICATE----- ================================================ FILE: test/fixtures/https/agent2-key.pem ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEpQIBAAKCAQEAvSQq3d8AeZMTvtqZ13jWCckikyXJSACvkGCQUCJqOceESbg6 IHdRzQdoccE4P3sbvNsf9BlbdJKM+neCxabqKaU1PPje4P0tHT57t6yJrMuUh9Nx Ez3Bgh1srNHVS7saKvwHmcKm79jc+wxlioPmEQvQagjny7oTkyLt0sn4LGxBjrcv 2JoHOC9f1pxX7l47MaiN0/ctRau7Nr3PFn+pkB4Yf6Z0VyicVJbaUSz39Qo4HQWl 1L2hiBP3CS1oKs2Yk0O1aOCMExWrhZQan+ZgHqL1rhgmkPpw2/zwwPt5Vf9CSakv Hwg198EXuTTXtkzYduuIJAm8yp969iEIiG2xTwIDAQABAoIBAGPIw/C/qJF7HYyv 6T+7GTiaa2o0IiehbP3/Y8NTFLWc49a8obXlHTvMr7Zr2I/tE+ojtIzkH9K1SjkN eelqsNj9tsOPDI6oIvftsflpxkxqLtclnt8m0oMhoObf4OaONDT/N8dP4SBiSdsM ZDmacnMFx5NZVWiup4sVf2CYexx7qks9FhyN2K5PArCQ4S9LHjFhSJVH4DSEpv7E Ykbp30rhpqV7wSwjgUsm8ZYvI2NOlmffzLSiPdt3vy2K5Q25S/MVEAicg83rfDgK 6EluHjeygRI1xU6DJ0hU7tnU7zE9KURoHPUycO3BKzZnzUH26AA36I58Pu4fXWw/ Cgmbv2ECgYEA+og9E4ziKCEi3p8gqjIfwTRgWZxDLjEzooB/K0UhEearn/xiX29A FiSzEHKfCB4uSrw5OENg2ckDs8uy08Qmxx7xFXL7AtufAl5fIYaWa0sNSqCaIk7p ebbUmPcaYhKiLzIEd1EYEL38sXVZ62wvSVMRSWvEMq44g1qnoRlDa/8CgYEAwUTt talYNwVmR9ZdkVEWm9ZxirdzoM6NaM6u4Tf34ygptpapdmIFSUhfq4iOiEnRGNg/ tuNqhNCIb3LNpJbhRPEzqN7E7qiF/mp7AcJgbuxLZBm12QuLuJdG3nrisKPFXcY1 lA4A7CFmNgH3E4THFfgwzyDXsBOxVLXleTqn+rECgYEA9up1P6J3dtOJuV2d5P/3 ugRz/X173LfTSxJXw36jZDAy8D/feG19/RT4gnplcKvGNhQiVOhbOOnbw0U8n2fQ TCmbs+cZqyxnH/+AxNsPvvk+RVHZ93xMsY/XIldP4l65B8jFDA+Zp06IESI2mEeM pzi+bd1Phh+dRSCA2865W2MCgYEAlxYsgmQ1WyX0dFpHYU+zzfXRYzDQyrhOYc2Z duVK+yCto1iad7pfCY/zgmRJkI+sT7DV9kJIRjXDQuTLkEyHJF8vFGe6KhxCS8aw DIsI2g4NTd6vg1J8UryoIUqNpqsQoqNNxUVBQVdG0ReuMGsPO8R/W50AIFz0txVP o/rP0LECgYEA7e/mOwCnR+ovmS/CAksmos3oIqvkRkXNKpKe513FVmp3TpTU38ex cBkFNU3hEO31FyrX1hGIKp3N5mHYSQ1lyODHM6teHW0OLWWTwIe8rIGvR2jfRLe0 bbkdj40atYVkfeFmpz9uHHG24CUYxJdPc360jbXTVp4i3q8zqgL5aMY= -----END RSA PRIVATE KEY----- ================================================ FILE: test/fixtures/proxy-all-local/does-not-exist ================================================ This file should never be served when proxyAll is enabled. ================================================ FILE: test/fixtures/proxy-all-local/file ================================================ Local proxy-all fixture file ================================================ FILE: test/fixtures/root/canYouSeeMe ================================================ I bet you can. I'm in your index. ================================================ FILE: test/fixtures/root/compression/index.html ================================================ I'm not compressed! ================================================ FILE: test/fixtures/root/compression/index.html.br ================================================ �im brotli compressed!!  ================================================ FILE: test/fixtures/root/file ================================================ hello, I know nodejitsu ================================================ FILE: test/fixtures/root/htmlButNot ================================================

I am HTML?

yeah i guess
================================================ FILE: test/force-content-encoding.test.js ================================================ 'use strict'; const test = require('tap').test; const ecstatic = require('../lib/core'); const http = require('http'); const request = require('request'); const root = `${__dirname}/public`; test('--force-content-encoding flag: .br files served without Content-Encoding header when flag not set', (t) => { t.plan(3); const server = http.createServer(ecstatic({ root, brotli: true, autoIndex: true, forceContentEncoding: false })); server.listen(() => { const port = server.address().port; const options = { uri: `http://localhost:${port}/brotli/index.html.br`, headers: { 'accept-encoding': 'gzip, deflate, br' } }; request.get(options, (err, res) => { t.error(err); t.equal(res.statusCode, 200); t.notOk(res.headers['content-encoding'], 'should not have content-encoding header when flag not set'); }); }); t.once('end', () => { server.close(); }); }); test('--force-content-encoding flag: .br files served with Content-Encoding header when flag is set', (t) => { t.plan(3); const server = http.createServer(ecstatic({ root, brotli: true, autoIndex: true, forceContentEncoding: true })); server.listen(() => { const port = server.address().port; const options = { uri: `http://localhost:${port}/brotli/index.html.br`, headers: { 'accept-encoding': 'gzip, deflate, br' } }; request.get(options, (err, res) => { t.error(err); t.equal(res.statusCode, 200); t.equal(res.headers['content-encoding'], 'br', 'should have content-encoding: br header when flag is set'); }); }); t.once('end', () => { server.close(); }); }); test('--force-content-encoding flag: regular files served with Content-Encoding header when flag is set', (t) => { t.plan(3); const server = http.createServer(ecstatic({ root, brotli: true, autoIndex: true, forceContentEncoding: true })); server.listen(() => { const port = server.address().port; const options = { uri: `http://localhost:${port}/brotli/index.html`, headers: { 'accept-encoding': 'gzip, deflate, br' } }; request.get(options, (err, res) => { t.error(err); t.equal(res.statusCode, 200); t.ok(res.headers['content-encoding'], 'regular files should have content-encoding header'); }); }); t.once('end', () => { server.close(); }); }); ================================================ FILE: test/headers.test.js ================================================ 'use strict'; const test = require('tap').test; const ecstatic = require('../lib/core'); const http = require('http'); const checkHeaders = require('./check-headers.js'); const root = `${__dirname}/public`; test('headers object', (t) => { t.plan(4); const server = http.createServer( ecstatic({ root, headers: { Wow: 'sweet', Cool: 'beans', }, autoIndex: true, defaultExt: 'html', }) ); checkHeaders(t, server, 'subdir', (t, headers) => { t.equal(headers.wow, 'sweet'); t.equal(headers.cool, 'beans'); }); }); test('header string', (t) => { t.plan(3); const server = http.createServer( ecstatic({ root, header: 'beep: boop', // for command-line --header 'beep: boop' autoIndex: true, defaultExt: 'html', }) ); checkHeaders(t, server, 'subdir', (t, headers) => { t.equal(headers.beep, 'boop'); }); }); test('header array', (t) => { t.plan(3); const server = http.createServer( ecstatic({ root, header: [ 'beep: boop', // --header 'beep: boop' 'what: ever', // --header 'what: ever' ], autoIndex: true, defaultExt: 'html', }) ); checkHeaders(t, server, 'subdir', (t, headers) => { t.equal(headers.beep, 'boop'); }); }); test('H array', (t) => { t.plan(3); const server = http.createServer( ecstatic({ root, H: [ 'beep: boop', // -H 'beep: boop' 'what: ever', // -H 'what: ever' ], autoIndex: true, defaultExt: 'html', }) ); checkHeaders(t, server, 'subdir', (t, headers) => { t.equal(headers.beep, 'boop'); }); }); // CRLF injection prevention test('CRLF injection prevention', (t) => { t.plan(1); t.throws(() => { const server = http.createServer( ecstatic({ root, H: [ 'X-CRLF-Injection: X\r\nContent-Type: text/html', ], autoIndex: true, defaultExt: 'html', }) ); server.close(); }, /Header is not a string or contains CRLF/); }); ================================================ FILE: test/illegal-access-date.test.js ================================================ 'use strict'; const test = require('tap').test; const ecstatic = require('../lib/core'); const http = require('http'); const path = require('path'); const request = require('request'); test('if-modified-since illegal access date', (t) => { const dir = path.join(__dirname, 'public'); const server = http.createServer(ecstatic(dir)); t.plan(2); server.listen(0, () => { const opts = { url: `http://localhost:${server.address().port}/a.txt`, headers: { 'if-modified-since': '275760-09-24' }, }; request.get(opts, (err, res) => { t.error(err); t.equal(res.statusCode, 200); server.close(() => { t.end(); }); }); }); }); ================================================ FILE: test/localhost.test.js ================================================ 'use strict'; const test = require('tap').test; const ecstatic = require('../lib/core'); const http = require('http'); const request = require('request'); test('can connect from all localhost addresses', t => { const server = http.createServer(ecstatic(`${__dirname}/public/subdir`)); t.on('end', () => { server.close(); }); server.listen(0, () => { const port = server.address().port; const addresses = [ 'localhost', '127.0.0.1', '::1', ]; t.plan(addresses.length * 2); for (const address of addresses) { request.get(`http://[${address}]:${port}/index.html`, (err, res, body) => { t.error(err); t.equal(res.statusCode, 200); }); } }); }); ================================================ FILE: test/main.test.js ================================================ const test = require('tap').test; const path = require('path'); const fs = require('fs'); const request = require('request'); const httpServer = require('../lib/http-server'); const promisify = require('util').promisify; const requestAsync = promisify(request); const fsReadFile = promisify(fs.readFile); // Prevent errors from being swallowed process.on('uncaughtException', console.error); const root = path.join(__dirname, 'fixtures', 'root'); // Tests are grouped into those which can run together. The groups are given // their own port to run on and live inside a Promise. Tests are done when all // Promise test groups complete. test('http-server main', (t) => { Promise.all([ new Promise((resolve) => { const server = httpServer.createServer({ root, robots: true, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': 'true' }, coop: true, cors: true, corsHeaders: 'X-Test', ext: true, brotli: true, gzip: true }); server.listen(0, async () => { const port = server.address().port; try { // Since none of these depend on anything not already declared, they // can run on the event loop at their own leisure await Promise.all([ // request file from root requestAsync(`http://localhost:${port}/file`).then(async (res) => { // files should be served from the root t.equal(res.statusCode, 200); const fileData = await fsReadFile(path.join(root, 'file'), 'utf8'); t.equal(res.body.trim(), fileData.trim(), 'root file content matches'); }).catch(err => t.fail(err.toString())), // Request non-existent file requestAsync(`http://localhost:${port}/404`).then(res => { t.ok(res); t.equal(res.statusCode, 404); }).catch(err => t.fail(err.toString())), // Request root requestAsync(`http://localhost:${port}/`).then(res => { t.ok(res); t.equal(res.statusCode, 200); t.match(res.body, './file'); t.match(res.body, './canYouSeeMe'); // Custom headers t.equal(res.headers['access-control-allow-origin'], '*'); t.equal(res.headers['access-control-allow-credentials'], 'true'); t.equal(res.headers['cross-origin-opener-policy'], 'same-origin'); t.equal(res.headers['cross-origin-embedder-policy'], 'require-corp'); }).catch(err => t.fail(err.toString())), // Get robots requestAsync(`http://localhost:${port}/robots.txt`).then(res => { t.equal(res.statusCode, 200); }).catch(err => t.fail(err.toString())), // CORS time requestAsync({ uri: `http://localhost:${port}`, method: 'OPTIONS', headers: { 'Access-Control-Request-Method': 'GET', Origin: 'http://example.com', 'Access-Control-Request-Headers': 'Foobar' } }).then(res => { t.equal(res.statusCode, 204); t.ok( res.headers['access-control-allow-headers'] .split(/\s*,\s*/g) .indexOf('X-Test') >= 0, 204); }).catch(err => t.fail(err.toString())), t.test( "Regression: don't crash on control characters in query strings", {}, (t) => { requestAsync({ uri: encodeURI(`http://localhost:${port}/file?\x0cfoo`), }).then(res => { t.equal(res.statusCode, 200); }).catch(err => t.fail(err.toString())) .finally(() => t.end()); } ), // Light compression testing. Heavier compression tests exist in // compression.test.js requestAsync({ uri: `http://localhost:${port}/compression/`, headers: { 'accept-encoding': 'gzip' } }).then(res => { t.equal(res.statusCode, 200); t.equal(res.headers['content-encoding'], 'gzip'); }).catch(err => t.fail(err.toString())), requestAsync({ uri: `http://localhost:${port}/compression/`, headers: { 'accept-encoding': 'gzip, br' } }).then(res => { t.equal(res.statusCode, 200); t.equal(res.headers['content-encoding'], 'br'); }).catch(err => t.fail(err.toString())), requestAsync(`http://localhost:${port}/htmlButNot`).then(res => { t.equal(res.statusCode, 200); t.match(res.headers['content-type'], /^text\/html/); }).catch(err => t.fail(err.toString())) ]); // Another server proxies to the main server const proxyServer = httpServer.createServer({ proxy: `http://localhost:${port}`, root: path.join(__dirname, 'fixtures') }); await new Promise((resolve) => { proxyServer.listen(0, async () => { const proxyPort = proxyServer.address().port; try { // Serve files from proxy root await requestAsync(`http://localhost:${proxyPort}/root/file`).then(async (res) => { t.ok(res); t.equal(res.statusCode, 200); // File content matches const fileData = await fsReadFile(path.join(root, 'file'), 'utf8'); t.equal(res.body.trim(), fileData.trim(), 'proxied root file content matches'); }).catch(err => t.fail(err.toString())); // Proxy fallback await requestAsync(`http://localhost:${proxyPort}/file`).then(async (res) => { t.ok(res); t.equal(res.statusCode, 200); // File content matches const fileData = await fsReadFile(path.join(root, 'file'), 'utf8'); t.equal(res.body.trim(), fileData.trim(), 'proxy fallback root file content matches'); }).catch(err => t.fail(err.toString())); } catch (err) { t.fail(err.toString()); } finally { proxyServer.close(); resolve(); } }); }); } catch (err) { t.fail(err.toString()); } finally { server.close(); resolve(); } }); }), new Promise((resolve) => { const server = httpServer.createServer({ root, username: 'correct_username', password: 'correct_password' }); server.listen(0, async () => { const authPort1 = server.address().port; try { await Promise.all([ // Bad request with no auth requestAsync(`http://localhost:${authPort1}/file`).then((res) => { t.equal(res.statusCode, 401); t.equal(res.body, 'Access denied', 'Bad auth returns expected body'); }).catch(err => t.fail(err.toString())), // bad user requestAsync(`http://localhost:${authPort1}/file`, { auth: { user: 'wrong_username', pass: 'correct_password' } }).then((res) => { t.equal(res.statusCode, 401); t.equal(res.body, 'Access denied', 'Bad auth returns expected body'); }).catch(err => t.fail(err.toString())), // bad password requestAsync(`http://localhost:${authPort1}/file`, { auth: { user: 'correct_username', pass: 'wrong_password' } }).then((res) => { t.equal(res.statusCode, 401); t.equal(res.body, 'Access denied', 'Bad auth returns expected body'); }).catch(err => t.fail(err.toString())), // nonexistant file, and bad auth requestAsync(`http://localhost:${authPort1}/404`, { auth: { user: 'correct_username', pass: 'wrong_password' } }).then((res) => { t.equal(res.statusCode, 401); t.equal(res.body, 'Access denied', 'Bad auth returns expected body'); }).catch(err => t.fail(err.toString())), // good path, good auth requestAsync(`http://localhost:${authPort1}/file`, { auth: { user: 'correct_username', pass: 'correct_password' } }).then(async (res) => { t.equal(res.statusCode, 200); const fileData = await fsReadFile(path.join(root, 'file'), 'utf8'); t.equal(res.body.trim(), fileData.trim(), 'auth-protected file with good auth has expected file content'); }).catch(err => t.fail(err.toString())), ]); } catch (err) { t.fail(err.toString()); } finally { server.close(); resolve(); } }); }), new Promise((resolve) => { const server = httpServer.createServer({ root, username: 'correct_username', password: 123456 }); server.listen(0, async () => { const authPort2 = server.address().port; try { await Promise.all([ // regression test requestAsync(`http://localhost:${authPort2}/file`).then(res => { t.equal(res.statusCode, 401); t.equal(res.body, 'Access denied', 'Bad auth returns expected body'); }).catch(err => t.fail(err.toString())), // regression test, bad username requestAsync(`http://localhost:${authPort2}/file`, { auth: { user: 'wrong_username', pass: '123456' } }).then(res => { t.equal(res.statusCode, 401); t.equal(res.body, 'Access denied', 'Bad auth returns expected body'); }).catch(err => t.fail(err.toString())), // regression test, correct auth, even though the password is a // different type. requestAsync(`http://localhost:${authPort2}/file`, { auth: { user: 'correct_username', pass: '123456' } }).then(async (res) => { t.equal(res.statusCode, 200); const fileData = await fsReadFile(path.join(root, 'file'), 'utf8'); t.equal(res.body.trim(), fileData.trim(), 'numeric auth with good auth has expected file content'); }).catch(err => t.fail(err.toString())) ]); } catch (err) { t.fail(err.toString()); } finally { server.close(); resolve(); } }); }), new Promise((resolve) => { const server = httpServer.createServer({ root, baseDir: '/test' }); server.listen(0, async () => { const baseDirPort = server.address().port; try { await Promise.all([ // should serve files at the specified baseDir requestAsync(`http://localhost:${baseDirPort}/test/file`).then(async (res) => { t.equal(res.statusCode, 200); const fileData = await fsReadFile(path.join(root, 'file'), 'utf8'); t.equal(res.body.trim(), fileData.trim()); }).catch(err => t.fail(err.toString())), // should not serve files at the root requestAsync(`http://localhost:${baseDirPort}/file`).then((res) => { t.equal(res.statusCode, 403); t.equal(res.body, ''); }).catch(err => t.fail(err.toString())), ]); } catch (err) { t.fail(err.toString()); } finally { server.close(); resolve(); } }); }), ]).then(() => t.end()) .catch(err => { t.fail(err.toString()); t.end(); }); }); ================================================ FILE: test/malformed-dir.test.js ================================================ 'use strict'; const test = require('tap').test; const ecstatic = require('../lib/core'); const http = require('http'); const request = require('request'); test('malformed showdir uri', (t) => { const server = http.createServer(ecstatic(__dirname, { showDir: true })); t.plan(2); server.listen(0, () => { request.get(`http://localhost:${server.address().port}/?%`, (err, res) => { t.error(err); t.equal(res.statusCode, 400); server.close(() => { t.end(); }); }); }); }); ================================================ FILE: test/malformed.test.js ================================================ 'use strict'; const test = require('tap').test; const ecstatic = require('../lib/core'); const http = require('http'); const request = require('request'); test('malformed uri', (t) => { const server = http.createServer(ecstatic(__dirname)); t.plan(2); server.listen(0, () => { request.get(`http://localhost:${server.address().port}/%`, (err, res) => { t.error(err); t.equal(res.statusCode, 400); server.close(() => { t.end(); }); }); }); }); ================================================ FILE: test/mime.test.js ================================================ 'use strict'; const test = require('tap').test; const mime = require('mime'); test('mime package lookup', (t) => { t.plan(7); t.equal(mime.lookup('/path/to/file.css'), 'text/css'); t.equal(mime.lookup('/path/to/file.js'), 'application/javascript'); t.equal(mime.lookup('/path/to/file.mjs'), 'application/javascript'); t.equal(mime.lookup('/path/to/file.txt'), 'text/plain'); t.equal(mime.lookup('file.txt'), 'text/plain'); t.equal(mime.lookup('.TXT'), 'text/plain'); t.equal(mime.lookup('htm'), 'text/html'); t.end(); }); test('custom definition of mime-type with the mime package', (t) => { t.plan(1); mime.define({ 'application/xml': ['opml'], }); t.equal(mime.lookup('.opml'), 'application/xml'); t.end(); }); test('custom definition of mime-type with a .types file', (t) => { t.plan(2); try { mime.load('test/public/custom_mime_type.types'); } catch (e) { t.fail(e.message); t.end(); } t.equal(mime.lookup('.opml'), 'application/foo'); // see public/custom_mime_type.types t.throws(mime.load.bind(mime, 'public/this_file_does_not_exist.types')); t.end(); }); ================================================ FILE: test/network-interfaces.test.js ================================================ 'use strict'; const test = require('tap').test; const os = require('os'); test('network interface display handles both string and numeric family values', (t) => { t.plan(4); // Store original function to restore later const originalNetworkInterfaces = os.networkInterfaces; // Mock data with string family values (Node < 18) const mockInterfacesString = { 'eth0': [ { family: 'IPv4', address: '192.168.1.100', internal: false }, { family: 'IPv6', address: '::1', internal: false } ], 'lo': [ { family: 'IPv4', address: '127.0.0.1', internal: true } ] }; // Mock data with numeric family values (Node >= 18) const mockInterfacesNumeric = { 'eth0': [ { family: 4, address: '192.168.1.100', internal: false }, { family: 6, address: '::1', internal: false } ], 'lo': [ { family: 4, address: '127.0.0.1', internal: true } ] }; // Test the logic that filters IPv4 interfaces (extracted from bin/http-server) function getIPv4Addresses(interfaces) { const addresses = []; Object.keys(interfaces).forEach(function (dev) { interfaces[dev].forEach(function (details) { // This is the fix: handle both string and numeric family values if (details.family === 'IPv4' || details.family === 4) { addresses.push(details.address); } }); }); return addresses; } // Test with string family values let addresses = getIPv4Addresses(mockInterfacesString); t.equal(addresses.length, 2, 'Should find 2 IPv4 addresses with string family'); t.ok(addresses.includes('192.168.1.100'), 'Should include external IPv4 address'); // Test with numeric family values addresses = getIPv4Addresses(mockInterfacesNumeric); t.equal(addresses.length, 2, 'Should find 2 IPv4 addresses with numeric family'); t.ok(addresses.includes('192.168.1.100'), 'Should include external IPv4 address'); // Restore original function os.networkInterfaces = originalNetworkInterfaces; }); test('network interface filtering excludes IPv6 link-local addresses', (t) => { t.plan(2); const mockInterfaces = { 'eth0': [ { family: 4, address: '192.168.1.100', internal: false }, { family: 6, address: '2001:db8::1', internal: false }, { family: 6, address: 'fe80::1', internal: false } // link-local, should be excluded ] }; // Extract the IPv6 filtering logic from bin/http-server const ipv6Addresses = []; const ipv4Addresses = []; Object.keys(mockInterfaces).forEach(function (dev) { mockInterfaces[dev].forEach(function (details) { if (details.family === 'IPv4' || details.family === 4) { ipv4Addresses.push(details.address); } if ((details.family === 'IPv6' || details.family === 6) && !details.address.startsWith("fe80")) { ipv6Addresses.push(details.address); } }); }); t.equal(ipv4Addresses.length, 1, 'Should find 1 IPv4 address'); t.equal(ipv6Addresses.length, 1, 'Should find 1 non-link-local IPv6 address'); }); ================================================ FILE: test/pathname-encoding.test.js ================================================ 'use strict'; const tap = require('tap'); const ecstatic = require('../lib/core'); const http = require('http'); const request = require('request'); const path = require('path'); const portfinder = require('portfinder'); const test = tap.test; const root = `${__dirname}/public`; const baseDir = 'base'; if (process.platform === 'win32') { tap.plan(0, 'Windows is allergic to < in path names'); return; } const fs = require('fs'); test('create test directory', (t) => { fs.mkdirSync(`${root}/`, '0755'); t.end(); }); test('directory listing with pathname including HTML characters', (t) => { const server = http.createServer( ecstatic({ root, baseDir, showDir: true, autoIndex: false, }) ); server.listen(0, () => { const port = server.address().port; const uri = `http://localhost:${port}${path.join('/', baseDir, '/%3Cdir%3E')}`; request.get({ uri, }, (err, res, body) => { t.notMatch(body, //, 'We didn\'t find the unencoded pathname'); t.match(body, /<dir>/, 'We found the encoded pathname'); server.close(); t.end(); }); }); }); test('NULL byte in request path does not crash server', (t) => { const server = http.createServer( ecstatic({ root, baseDir, }) ); try { server.listen(0, () => { const port = server.address().port; const uri = `http://localhost:${port}${path.join('/', baseDir, '/%00')}`; request.get({uri}, (err, res, body) => { t.pass('server did not crash') server.close(); t.end(); }); }); } catch (err) { t.fail(err.toString()); } }); test('remove test directory', (t) => { fs.rmdirSync(`${root}/`); t.end(); }); ================================================ FILE: test/private-network-access.test.js ================================================ 'use strict'; const test = require('tap').test; const server = require('../lib/core'); const http = require('http'); const path = require('path'); const request = require('request'); const root = path.join(__dirname, 'public'); test('private-network-access defaults to false', (t) => { t.plan(3); const httpServer = http.createServer( server({ root, autoIndex: true, defaultExt: 'html', }) ); httpServer.listen(() => { const port = httpServer.address().port; const uri = `http://localhost:${port}/subdir/index.html`; request.get({ uri }, (err, res) => { t.error(err); t.equal(res.statusCode, 200); t.type(res.headers['access-control-allow-private-network'], 'undefined'); }); }); t.once('end', () => { httpServer.close(); }); }); test('privateNetworkAccess set to false', (t) => { t.plan(3); const httpServer = http.createServer( server({ root, privateNetworkAccess: false, autoIndex: true, defaultExt: 'html', }) ); httpServer.listen(() => { const port = httpServer.address().port; const uri = `http://localhost:${port}/subdir/index.html`; request.get({ uri }, (err, res) => { t.error(err); t.equal(res.statusCode, 200); t.type(res.headers['access-control-allow-private-network'], 'undefined'); }); }); t.once('end', () => { httpServer.close(); }); }); test('privateNetworkAccess set to true', (t) => { t.plan(3); const httpServer = http.createServer( server({ root, privateNetworkAccess: true, autoIndex: true, defaultExt: 'html', }) ); httpServer.listen(() => { const port = httpServer.address().port; const uri = `http://localhost:${port}/subdir/index.html`; request.get({ uri }, (err, res) => { t.error(err); t.equal(res.statusCode, 200); t.equal(res.headers['access-control-allow-private-network'], 'true'); }); }); t.once('end', () => { httpServer.close(); }); }); ================================================ FILE: test/process-env-port.test.js ================================================ 'use strict'; const test = require('tap').test; const request = require('request'); const spawn = require('child_process').spawn; function getRandomInt(min, max) { return Math.floor((Math.random() * ((max - min) + 1))) + min; } const sanePort = getRandomInt(1025, 65536); const floatingPointPort = 9090.86; const insanePorts = [-1, 65537]; function checkServerIsRunning(url, msg, ps, t) { if (!msg.toString().match(/Starting up/)) { return; } t.pass('http-server was started'); request(url, (err, res) => { if (!err && res.statusCode !== 500) { t.pass('a successful request from the server was made'); } else { t.fail('the server could not be reached'); } ps.kill('SIGTERM'); }); } function startServer(url, port, t) { const ecstatic = spawn(process.execPath, [require.resolve('../bin/http-server')], { env: { PORT: String(port), }, }); if (!insanePorts.includes(port)) { ecstatic.stdout.on('data', (msg) => { checkServerIsRunning(url, msg, ecstatic, t); }); } else { ecstatic.on('exit', (evt) => { t.not(evt.code, 0, 'err:Running on invalid port not allowed'); }); } } test('sane port', (t) => { t.plan(2); startServer(`http://127.0.0.1:${sanePort}`, sanePort, t); }); test('floating point port', (t) => { t.plan(2); startServer('http://127.0.0.1:9090', floatingPointPort, t); }); insanePorts.forEach((port) => { test(`insane port: ${port}`, (t) => { t.plan(1); startServer('http://127.0.0.1:8000', port, t); }); }); ================================================ FILE: test/proxy-all.test.js ================================================ 'use strict'; const http = require('http'); const test = require('tap').test; const path = require('path'); const fs = require('fs'); const request = require('request'); const { promisify } = require('util'); const httpServer = require('../lib/http-server'); const requestAsync = promisify(request); const REQUEST_TIMEOUT = 5000; function listen(server) { return new Promise((resolve, reject) => { const underlying = server.server || server; const onError = (err) => { underlying.removeListener('error', onError); reject(err); }; underlying.once('error', onError); server.listen(0, () => { underlying.removeListener('error', onError); resolve((underlying.address() || {}).port); }); }); } function requestWithTimeout(url) { return requestAsync({ url, timeout: REQUEST_TIMEOUT }); } test('proxyAll requires a proxy target', (t) => { t.throws(() => { httpServer.createServer({ proxyAll: true }); }, /proxy/i, 'proxyAll without proxy should throw'); t.end(); }); test('proxyAll routes every request through the proxy target', async (t) => { t.plan(4); const localRoot = path.join(__dirname, 'fixtures', 'proxy-all-local'); const remoteContent = fs.readFileSync(path.join(__dirname, 'fixtures', 'root', 'file'), 'utf8').trim(); const localContent = fs.readFileSync(path.join(localRoot, 'file'), 'utf8').trim(); const remoteServer = http.createServer((req, res) => { if (req.url === '/file') { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(remoteContent); return; } res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('File not found. :('); }); const remotePort = await listen(remoteServer); const proxyServer = httpServer.createServer({ root: localRoot, proxy: `http://localhost:${remotePort}`, proxyAll: true }); const proxyPort = await listen(proxyServer); t.teardown(() => { proxyServer.close(); remoteServer.close(); }); const proxied = await requestWithTimeout(`http://localhost:${proxyPort}/file`); t.equal(proxied.body.trim(), remoteContent, 'response matches proxy target'); t.notSame(proxied.body.trim(), localContent, 'local files are ignored when proxyAll is set'); const missing = await requestWithTimeout(`http://localhost:${proxyPort}/does-not-exist`); t.equal(missing.statusCode, 404, 'status code comes from proxy target'); t.match(missing.body, /file not found/i, 'body matches proxy response'); }); ================================================ FILE: test/proxy-config.test.js ================================================ const test = require('tap').test const path = require('path') const fs = require('fs') const request = require('request') const httpServer = require('../lib/http-server') const promisify = require('util').promisify const requestAsync = promisify(request) const fsReadFile = promisify(fs.readFile) // Prevent errors from being swallowed process.on('uncaughtException', console.error) const root = path.join(__dirname, 'fixtures', 'root') const httpsOpts = { key: path.join(__dirname, 'fixtures', 'https', 'agent2-key.pem'), cert: path.join(__dirname, 'fixtures', 'https', 'agent2-cert.pem') } const proxyConfigTest = { "/rewrite/**": { "target": "http://localhost:8082", "pathRewrite": { "^/rewrite": "" } } } // Tests are grouped into those which can run together. The groups are given // their own port to run on and live inside a Promise. Tests are done when all // Promise test groups complete. test('proxy config', (t) => { new Promise((resolve) => { const server = httpServer.createServer({ root, robots: true, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': 'true' }, cors: true, corsHeaders: 'X-Test', ext: true, brotli: true, gzip: true }) // TODO #723 we should use portfinder server.listen(8082, async () => { try { // Another server proxies 8083 to 8082 const proxyServer = httpServer.createServer({ root, //tls: true, //https: httpsOpts, proxyConfig: proxyConfigTest }) await new Promise((resolve) => { proxyServer.listen(8083, async () => { try { // Serve files from proxy root await requestAsync('http://localhost:8083/file', { rejectUnauthorized: false }).then(async (res) => { t.ok(res) t.equal(res.statusCode, 200) // File content matches const fileData = await fsReadFile(path.join(root, 'file'), 'utf8') t.equal(res.body.trim(), fileData.trim(), 'none proxied file content matches') }).catch(err => t.fail(err.toString())) // Serve files from proxy with rewrite await requestAsync('http://localhost:8083/rewrite/file', { rejectUnauthorized: false }).then(async (res) => { t.ok(res) t.equal(res.statusCode, 200) // File content matches const fileData = await fsReadFile(path.join(root, 'file'), 'utf8') t.equal(res.body.trim(), fileData.trim(), 'proxied file content matches') }).catch(err => t.fail(err.toString())) } catch (err) { t.fail(err.toString()) } finally { proxyServer.close() resolve() } }) }) } catch (err) { t.fail(err.toString()) } finally { server.close() resolve() } }) }) .then(() => t.end()) .catch(err => { t.fail(err.toString()) t.end() }) }) ================================================ FILE: test/proxy-options.test.js ================================================ const test = require('tap').test const path = require('path') const fs = require('fs') const request = require('request') const httpServer = require('../lib/http-server') const promisify = require('util').promisify const requestAsync = promisify(request) const fsReadFile = promisify(fs.readFile) // Prevent errors from being swallowed process.on('uncaughtException', console.error) const root = path.join(__dirname, 'fixtures', 'root') const httpsOpts = { key: path.join(__dirname, 'fixtures', 'https', 'agent2-key.pem'), cert: path.join(__dirname, 'fixtures', 'https', 'agent2-cert.pem') } // Tests are grouped into those which can run together. The groups are given // their own port to run on and live inside a Promise. Tests are done when all // Promise test groups complete. test('proxy options', (t) => { new Promise((resolve) => { const server = httpServer.createServer({ root, robots: true, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': 'true' }, cors: true, corsHeaders: 'X-Test', ext: true, brotli: true, gzip: true }) server.listen(0, async () => { const port = server.address().port; try { // Another server proxies to the main server const proxyServer = httpServer.createServer({ proxy: `http://localhost:${port}`, root: path.join(__dirname, 'fixtures'), tls: true, https: httpsOpts, proxyOptions: { secure: false } }) await new Promise((resolve) => { proxyServer.listen(0, async () => { const proxyPort = proxyServer.address().port; try { // Serve files from proxy root await requestAsync(`https://localhost:${proxyPort}/root/file`, { rejectUnauthorized: false }).then(async (res) => { t.ok(res) t.equal(res.statusCode, 200) // File content matches const fileData = await fsReadFile(path.join(root, 'file'), 'utf8') t.equal(res.body.trim(), fileData.trim(), 'proxied root file content matches') }).catch(err => t.fail(err.toString())) // Proxy fallback await requestAsync(`https://localhost:${proxyPort}/file`, { rejectUnauthorized: false }).then(async (res) => { t.ok(res) t.equal(res.statusCode, 200) // File content matches const fileData = await fsReadFile(path.join(root, 'file'), 'utf8') t.equal(res.body.trim(), fileData.trim(), 'proxy fallback root file content matches') }).catch(err => t.fail(err.toString())) } catch (err) { t.fail(err.toString()) } finally { proxyServer.close() resolve() } }) }) } catch (err) { t.fail(err.toString()) } finally { server.close() resolve() } }) }) .then(() => t.end()) .catch(err => { t.fail(err.toString()) t.end() }) }) ================================================ FILE: test/public/404.html ================================================

404

================================================ FILE: test/public/a.txt ================================================ A!!! ================================================ FILE: test/public/another-subdir/scripts.js ================================================ var foo = bar; ================================================ FILE: test/public/b.txt ================================================ B!!! ================================================ FILE: test/public/brotli/fake_ecstatic ================================================ ecstatic ================================================ FILE: test/public/brotli/index.html ================================================ brotli, but I'm not compressed!!! ================================================ FILE: test/public/brotli/index.html.br ================================================ brotli, compressed!!  ================================================ FILE: test/public/brotli/not_actually_brotli.br ================================================ You've been duped! This is not compressed! ================================================ FILE: test/public/brotli/real_ecstatic ================================================ ecstatic ================================================ FILE: test/public/brotli/real_ecstatic.br ================================================ ecstatic  ================================================ FILE: test/public/c.js ================================================ console.log('C!!!'); ================================================ FILE: test/public/charset/arabic.html ================================================ HTML

Kokoro - Natsume Soseki

. . . "". . .

================================================ FILE: test/public/charset/shift_jis.html ================================================ {HTMLt@C

- Ėڟ

@搶Ǝ

͂̐lɐ搶ƌĂłB炱ł搶ƏŖ{͑łȂB͐Ԃ݂鉓ƂA̕ɂƂĎRłB͂̐l̋LĂыNƂɁAu搶vƂȂBMĂS͓łB悻悻Ȃǂ͂ƂĂgCɂȂȂB

================================================ FILE: test/public/compress/foo.js ================================================ exports.foo = "baz"; ================================================ FILE: test/public/compress/foo_2.js ================================================ exports.foo = "baz"; ================================================ FILE: test/public/curimit@gmail.com (40%)/index.html ================================================ index!!! ================================================ FILE: test/public/custom_mime_type.opml ================================================ Tue 9 May 2006 06:44:27 GMT+00:00 BKN Sat 20 May 2006 00:17:25 GMT+00:00 ================================================ FILE: test/public/custom_mime_type.types ================================================ # This file is an example of the Apache .types file format for describing mime-types. # Other example: http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types application/foo opml ================================================ FILE: test/public/d.js ================================================ d.js ================================================ FILE: test/public/dir-overrides-404/404.html ================================================ 404file ================================================ FILE: test/public/dir-overrides-404/directory/file.txt ================================================ ================================================ FILE: test/public/e.js ================================================ console.log('π!!!'); ================================================ FILE: test/public/f_f ================================================ f!! ================================================ FILE: test/public/gzip/fake_ecstatic ================================================ ecstatic ================================================ FILE: test/public/gzip/index.html ================================================ gzip!! ================================================ FILE: test/public/gzip/real_ecstatic ================================================ ecstatic ================================================ FILE: test/public/show-dir$$href_encoding$$/aname+aplus.txt ================================================ It should generate an href of .. http://stackoverflow.com/a/1006074/147079 http://tools.ietf.org/html/rfc1738 ================================================ FILE: test/public/subdir/app.wasm ================================================ Fake WebAssemble file ================================================ FILE: test/public/subdir/e.html ================================================ e!! ================================================ FILE: test/public/subdir/index.html ================================================ index!!! ================================================ FILE: test/public/subdir_with space/file_with space.html ================================================ spacious!! ================================================ FILE: test/public/subdir_with space/index.html ================================================ index :) ================================================ FILE: test/public/中文/檔案.html ================================================ file!! ================================================ FILE: test/range.test.js ================================================ 'use strict'; const test = require('tap').test; const ecstatic = require('../lib/core'); const http = require('http'); const request = require('request'); const eol = require('eol'); const fs = require('fs'); const path = require('path'); test('range', (t) => { t.plan(4); const server = http.createServer(ecstatic(`${__dirname}/public/subdir`)); t.on('end', () => { server.close(); }); server.listen(0, () => { const port = server.address().port; const opts = { uri: `http://localhost:${port}/e.html`, headers: { range: '3-5' }, }; request.get(opts, (err, res, body) => { t.error(err); t.equal(res.statusCode, 206, 'partial content status code'); t.equal(body, 'e!!'); t.equal(parseInt(res.headers['content-length'], 10), body.length); }); }); }); test('range past the end', (t) => { t.plan(4); const server = http.createServer(ecstatic(`${__dirname}/public/subdir`)); t.on('end', () => { server.close(); }); server.listen(0, () => { const port = server.address().port; const opts = { uri: `http://localhost:${port}/e.html`, headers: { range: '3-500' }, }; request.get(opts, (err, res, body) => { t.error(err); t.equal(res.statusCode, 206, 'partial content status code'); t.equal(eol.lf(body), 'e!!\n'); t.equal(parseInt(res.headers['content-length'], 10), body.length); }); }); }); test('range starts beyond the end', (t) => { t.plan(4); const server = http.createServer(ecstatic(`${__dirname}/public/subdir`)); t.on('end', () => { server.close(); }); const filePath = path.join(__dirname, 'public/subdir/e.html'); const fileSize = fs.statSync(filePath).size; server.listen(0, () => { const port = server.address().port; const opts = { uri: `http://localhost:${port}/e.html`, headers: { range: '500-' }, }; request.get(opts, (err, res, body) => { t.error(err); t.equal(res.statusCode, 416, 'range error status code'); t.equal(res.headers['content-range'], `bytes */${fileSize}`); t.equal(body, 'Requested range not satisfiable'); }); }); }); test('NaN range', (t) => { t.plan(4); const server = http.createServer(ecstatic(`${__dirname}/public/subdir`)); t.on('end', () => { server.close(); }); const filePath = path.join(__dirname, 'public/subdir/e.html'); const fileSize = fs.statSync(filePath).size; server.listen(0, () => { const port = server.address().port; const opts = { uri: `http://localhost:${port}/e.html`, headers: { range: 'abc-def' }, }; request.get(opts, (err, res, body) => { t.error(err); t.equal(res.statusCode, 416, 'range error status code'); t.equal(res.headers['content-range'], `bytes */${fileSize}`); t.equal(body, 'Requested range not satisfiable'); }); }); }); test('flipped range', (t) => { t.plan(4); const server = http.createServer(ecstatic(`${__dirname}/public/subdir`)); t.on('end', () => { server.close(); }); const filePath = path.join(__dirname, 'public/subdir/e.html'); const fileSize = fs.statSync(filePath).size; server.listen(0, () => { const port = server.address().port; const opts = { uri: `http://localhost:${port}/e.html`, headers: { range: '333-222' }, }; request.get(opts, (err, res, body) => { t.error(err); t.equal(res.statusCode, 416, 'range error status code'); t.equal(res.headers['content-range'], `bytes */${fileSize}`); t.equal(body, 'Requested range not satisfiable'); }); }); }); test('partial range', (t) => { // 1 test is platform depedent "res.headers['content-range']" t.plan(5); const server = http.createServer(ecstatic(`${__dirname}/public/subdir`)); t.on('end', () => { server.close(); }); server.listen(0, () => { const port = server.address().port; const opts = { uri: `http://localhost:${port}/e.html`, headers: { range: '3-' }, }; request.get(opts, (err, res, body) => { t.error(err); t.equal(res.statusCode, 206, 'partial content status code'); t.equal(eol.lf(body), 'e!!\n'); t.equal(parseInt(res.headers['content-length'], 10), body.length); if (eol.lf(body) != body) { // on Windows, depending on Git settings t.equal(res.headers['content-range'], 'bytes 3-11/12'); } else { t.equal(res.headers['content-range'], 'bytes 3-10/11'); } }); }); }); test('include last-modified, etag and cache-control headers', (t) => { t.plan(4); const server = http.createServer(ecstatic(`${__dirname}/public/subdir`)); t.on('end', () => { server.close(); }); server.listen(0, () => { const port = server.address().port; const opts = { uri: `http://localhost:${port}/e.html`, headers: { range: '3-5' }, }; request.get(opts, (err, res) => { t.error(err); t.ok(res.headers['cache-control']); t.ok(res.headers['last-modified']); t.ok(res.headers.etag); }); }); }); ================================================ FILE: test/showdir-href-encoding.test.js ================================================ 'use strict'; const test = require('tap').test; const ecstatic = require('../lib/core'); const http = require('http'); const request = require('request'); const path = require('path'); const root = `${__dirname}/public`; const baseDir = 'base'; test('url encoding in href', (t) => { const server = http.createServer( ecstatic({ root, baseDir, showDir: true, autoIndex: false, }) ); server.listen(0, () => { const port = server.address().port; const uri = `http://localhost:${port}${path.join('/', baseDir, 'show-dir%24%24href_encoding%24%24')}`; request.get({ uri, }, (err, res, body) => { t.match(body, /href="\.\/aname%2Baplus.txt"/, 'We found the right href'); server.close(); t.end(); }); }); }); ================================================ FILE: test/showdir-search-encoding.test.js ================================================ 'use strict'; const test = require('tap').test; const ecstatic = require('../lib/core'); const http = require('http'); const request = require('request'); const path = require('path'); const root = `${__dirname}/public`; const baseDir = 'base'; test('directory listing with query string specified', (t) => { require('portfinder').getPort((err, port) => { const uri = `http://localhost:${port}${path.join('/', baseDir, '?a=1&b=2')}`; const server = http.createServer( ecstatic({ root, baseDir, showDir: true, autoIndex: false, }) ); server.listen(port, () => { request.get({ uri, }, (err, res, body) => { t.match(body, /href="\.\/subdir\/\?a=1&b=2"/, 'We found the encoded href'); t.notMatch(body, /a=1&b=2/, 'We didn\'t find the unencoded query string value'); server.close(); t.end(); }); }); }); }); ================================================ FILE: test/showdir-with-spaces.test.js ================================================ 'use strict'; const test = require('tap').test; const ecstatic = require('../lib/core'); const http = require('http'); const request = require('request'); const path = require('path'); const root = `${__dirname}/public`; const baseDir = 'base'; test('directory listing when directory name contains spaces', (t) => { require('portfinder').getPort((err, port) => { const uri = `http://localhost:${port}${path.join('/', baseDir, 'subdir_with%20space')}`; const server = http.createServer( ecstatic({ root, baseDir, showDir: true, autoIndex: false, }) ); server.listen(port, () => { request.get({ uri, }, (err, res, body) => { t.ok(/href="\.\/index.html"/.test(body), 'We found the right href'); server.close(); t.end(); }); }); }); }); ================================================ FILE: test/timeout.test.js ================================================ 'use strict'; const test = require('tap').test; const http = require('http'); const httpServer = require('../lib/http-server'); const path = require('path'); const root = path.join(__dirname, 'fixtures', 'root'); test('timeout: default behavior (no timeout specified)', (t) => { t.plan(2); const server = httpServer.createServer({ root }); server.listen(0, () => { const port = server.address().port; // Verify server was created successfully t.ok(server, 'server created without timeout option'); // Check that server has a timeout property (Node.js default is usually 120000ms = 2 minutes) // But we're not setting it, so it should use Node.js default t.ok(server.server, 'server has underlying server instance'); server.close(); t.end(); }); }); test('timeout: custom timeout value in seconds', (t) => { t.plan(2); const timeoutSeconds = 60; const server = httpServer.createServer({ root, timeout: timeoutSeconds }); server.listen(0, () => { const port = server.address().port; t.ok(server, 'server created with custom timeout'); // Verify timeout was set on the underlying server // Note: Node.js setTimeout expects milliseconds, but we're passing seconds // This test verifies the current behavior (may need adjustment after PR merge) const underlyingServer = server.server; t.ok(underlyingServer, 'server has underlying server instance'); server.close(); t.end(); }); }); test('timeout: disabled timeout (0)', (t) => { t.plan(2); const server = httpServer.createServer({ root, timeout: 0 }); server.listen(0, () => { const port = server.address().port; t.ok(server, 'server created with timeout disabled'); const underlyingServer = server.server; t.ok(underlyingServer, 'server has underlying server instance'); server.close(); t.end(); }); }); test('timeout: connection actually times out after specified duration', (t) => { t.plan(2); // Use a short timeout for testing (1 second = 1000ms) // Note: This assumes timeout is in milliseconds. If PR converts to milliseconds, // we may need to adjust this test const timeoutMs = 1000; const server = httpServer.createServer({ root, timeout: timeoutMs }); let timeoutFired = false; server.server.on('timeout', (socket) => { if (!timeoutFired) { timeoutFired = true; t.pass('timeout event fired'); socket.destroy(); server.close(); t.pass('timeout handling works'); t.end(); } }); server.listen(0, () => { const port = server.address().port; // Create a connection but don't send any data const socket = require('net').createConnection(port, 'localhost', () => { // Don't send any data - keep connection idle to trigger timeout }); socket.on('error', () => { // Connection errors are expected when timeout fires }); // Safety timeout in case the server timeout doesn't work setTimeout(() => { if (!timeoutFired) { t.fail('server timeout did not fire within expected time'); socket.destroy(); server.close(); t.end(); } }, timeoutMs + 2000); }); }); test('timeout: server handles requests normally with timeout set', (t) => { t.plan(3); const server = httpServer.createServer({ root, timeout: 60 // 60 seconds }); server.listen(0, () => { const port = server.address().port; t.ok(server, 'server created with timeout option'); const req = http.get(`http://localhost:${port}/file`, (res) => { t.equal(res.statusCode, 200, 'request succeeds with timeout set'); let body = ''; res.on('data', (chunk) => { body += chunk; }); res.on('end', () => { t.ok(body.length > 0, 'response body received'); server.close(); t.end(); }); }); req.on('error', (err) => { t.fail(`request failed: ${err.message}`); server.close(); t.end(); }); }); }); test('timeout: multiple timeout values', (t) => { t.plan(4); const testCases = [ { timeout: 30, description: '30 seconds' }, { timeout: 120, description: '120 seconds (default)' }, { timeout: 300, description: '300 seconds' }, { timeout: 0, description: 'disabled (0)' } ]; let completed = 0; const total = testCases.length; testCases.forEach((testCase) => { const server = httpServer.createServer({ root, timeout: testCase.timeout }); server.listen(0, () => { const port = server.address().port; t.ok(server, `server created with timeout ${testCase.description}`); server.close(); completed++; if (completed === total) { t.end(); } }); }); }); ================================================ FILE: test/trailing-slash.test.js ================================================ 'use strict'; const test = require('tap').test; const ecstatic = require('../lib/core'); const http = require('http'); const request = require('request'); test('should not add trailing slash when showDir and autoIndex are off', (t) => { t.plan(3); const server = http.createServer( ecstatic({ root: `${__dirname}/public`, autoIndex: false, showDir: false, }) ); t.on('end', () => { server.close(); }); server.listen(0, () => { const port = server.address().port; request.get(`http://localhost:${port}/subdir`, (err, res) => { t.error(err); t.equal(res.statusCode, 404); t.equal(res.body, 'File not found. :('); }); }); }); ================================================ FILE: test/websocket-proxy.test.js ================================================ const test = require('tap').test const path = require('path') const http = require('http') const httpServer = require('../lib/http-server') const WebSocket = require('ws') // Prevent errors from being swallowed process.on('uncaughtException', console.error) test('websocket proxy functionality', (t) => { new Promise((resolve) => { // Create a target server that will handle websocket connections const targetServer = http.createServer() const targetWss = new WebSocket.Server({ server: targetServer }) targetWss.on('connection', (ws) => { ws.on('message', (message) => { // Echo the message back ws.send(`Echo: ${message}`) }) }) targetServer.listen(0, () => { const targetPort = targetServer.address().port const targetUrl = `http://localhost:${targetPort}` // Create http-server with websocket proxy enabled const proxyServer = httpServer.createServer({ proxy: targetUrl, websocket: true, root: path.join(__dirname, 'fixtures') }) proxyServer.listen(0, async () => { const proxyPort = proxyServer.server.address().port const proxyUrl = `http://localhost:${proxyPort}` try { // Test 1: Verify websocket proxy is enabled when both proxy and websocket options are set t.ok(proxyServer.server.listeners('upgrade').length > 0, 'upgrade event listener should be registered') // Test 2: Test websocket connection through proxy await new Promise((resolve, reject) => { const ws = new WebSocket(`ws://localhost:${proxyPort}`) ws.on('open', () => { t.pass('websocket connection should be established through proxy') // Send a test message ws.send('Hello WebSocket!') }) ws.on('message', (data) => { t.equal(data.toString(), 'Echo: Hello WebSocket!', 'should receive echoed message') ws.close() }) ws.on('close', () => { t.pass('websocket connection should close properly') resolve() }) ws.on('error', (err) => { t.fail(`websocket error: ${err.message}`) reject(err) }) // Set timeout to prevent hanging setTimeout(() => { ws.close() reject(new Error('WebSocket test timeout')) }, 5000) }) } catch (err) { t.fail(`websocket proxy test failed: ${err.message}`) } finally { proxyServer.close() targetServer.close() resolve() } }) }) }) .then(() => t.end()) .catch(err => { t.fail(err.toString()) t.end() }) }) test('websocket proxy without proxy configuration', (t) => { new Promise((resolve) => { // Create http-server with websocket enabled but no proxy const server = httpServer.createServer({ websocket: true, root: path.join(__dirname, 'fixtures') }) server.listen(0, () => { try { // Test: Verify no upgrade event listener is registered when proxy is not set t.equal(server.server.listeners('upgrade').length, 0, 'no upgrade event listener should be registered when proxy is not set') t.pass('websocket option should be ignored when proxy is not configured') } catch (err) { t.fail(`test failed: ${err.message}`) } finally { server.close() resolve() } }) }) .then(() => t.end()) .catch(err => { t.fail(err.toString()) t.end() }) }) test('ensure websocket proxy is not enabled when \'websocket\' is not set', (t) => { new Promise((resolve) => { // Create a target server that will handle websocket connections const targetServer = http.createServer() const targetWss = new WebSocket.Server({ server: targetServer }) targetWss.on('connection', (ws) => { ws.on('message', (message) => { // Echo the message back ws.send(`Echo: ${message}`) }) }) targetServer.listen(0, () => { const targetPort = targetServer.address().port const targetUrl = `http://localhost:${targetPort}` const proxyServer = httpServer.createServer({ proxy: targetUrl, root: path.join(__dirname, 'fixtures') }) try { t.equal(proxyServer.server.listeners('upgrade').length, 0, 'no upgrade event listener should be registered when websocket is not set') } catch (err) { t.fail(`test failed: ${err.message}`) } finally { proxyServer.close() targetServer.close() resolve() } }) }) .then(() => t.end()) .catch(err => { t.fail(err.toString()) t.end() }) }); test('websocket proxy error handling', (t) => { new Promise((resolve) => { // Create http-server with invalid proxy target const proxyServer = httpServer.createServer({ proxy: 'http://localhost:99999', // Invalid port websocket: true, root: path.join(__dirname, 'fixtures') }) proxyServer.listen(0, async () => { const proxyPort = proxyServer.server.address().port try { // Test: Verify websocket proxy handles connection errors gracefully t.ok(proxyServer.server.listeners('upgrade').length > 0, 'upgrade event listener should be registered even with invalid proxy') // Test websocket connection to invalid proxy target await new Promise((resolve, reject) => { const ws = new WebSocket(`ws://localhost:${proxyPort}`) ws.on('open', () => { t.fail('websocket should not connect to invalid proxy target') ws.close() resolve() }) ws.on('error', (err) => { t.pass('websocket should error when proxy target is invalid') resolve() // This is expected }) ws.on('close', () => { t.pass('websocket should close on error') resolve() }) setTimeout(() => { ws.close() resolve() // Timeout is acceptable for this test }, 2000) }) } catch (err) { t.fail(`websocket proxy error handling test failed: ${err.message}`) } finally { proxyServer.close() resolve() } }) }) .then(() => t.end()) .catch(err => { t.fail(err.toString()) t.end() }) })