├── tests ├── example │ ├── assets │ │ ├── file with space.html │ │ ├── exists.png │ │ ├── index.js │ │ └── index.css │ ├── nested │ │ ├── assets │ │ │ ├── index.js │ │ │ ├── exists.png │ │ │ └── index.css │ │ ├── component.js │ │ ├── index.html │ │ └── index.js │ ├── index.html │ └── index.js ├── new │ └── app.js └── index.js ├── .npmignore ├── .gitignore ├── utils ├── ca.conf ├── ssl.conf ├── openBrowser.js ├── common.js ├── directoryListing.js ├── openChrome.applescript └── mimeTypes.js ├── package.json ├── LICENSE ├── certify.sh ├── cli.js ├── servor.js └── README.md /tests/example/assets/file with space.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | 3 | *.key 4 | *.pem 5 | *.crt -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.key 2 | *.pem 3 | *.crt 4 | 5 | .DS_Store 6 | node_modules -------------------------------------------------------------------------------- /tests/example/assets/exists.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukejacksonn/servor/HEAD/tests/example/assets/exists.png -------------------------------------------------------------------------------- /tests/example/assets/index.js: -------------------------------------------------------------------------------- 1 | var $text = document.querySelector('h2'); 2 | $text.innerHTML = window.location; 3 | -------------------------------------------------------------------------------- /tests/example/nested/assets/index.js: -------------------------------------------------------------------------------- 1 | var $text = document.querySelector('h2'); 2 | $text.innerHTML = window.location 3 | -------------------------------------------------------------------------------- /tests/example/nested/assets/exists.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukejacksonn/servor/HEAD/tests/example/nested/assets/exists.png -------------------------------------------------------------------------------- /tests/example/nested/component.js: -------------------------------------------------------------------------------- 1 | import { react, html, css } from 'https://unpkg.com/rplus'; 2 | 3 | export default 'component'; 4 | -------------------------------------------------------------------------------- /utils/ca.conf: -------------------------------------------------------------------------------- 1 | [req] 2 | prompt = no 3 | distinguished_name = options 4 | [options] 5 | C = US 6 | ST = State 7 | L = Locality 8 | O = Company 9 | CN = servor -------------------------------------------------------------------------------- /utils/ssl.conf: -------------------------------------------------------------------------------- 1 | authorityKeyIdentifier=keyid,issuer 2 | basicConstraints=CA:FALSE 3 | keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment 4 | subjectAltName = @alt_names 5 | [alt_names] 6 | DNS.1 = localhost 7 | DNS.2 = 127.0.0.1 8 | DNS.3 = ::1 -------------------------------------------------------------------------------- /tests/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SERVOR_TEST_INDEX 6 | 7 | 8 | 9 | 10 | 11 |
12 | File that exists 13 |

servør

14 |

15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/example/nested/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SERVOR_TEST_NESTED_INDEX 6 | 7 | 8 | 9 | 10 | 11 | 12 | File that exists 13 |

servør

14 |

15 | 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "servor", 3 | "version": "4.0.2", 4 | "description": "A dependency free dev server for single page app development", 5 | "repository": "lukejacksonn/servor", 6 | "main": "./servor.js", 7 | "bin": { 8 | "servor": "./cli.js" 9 | }, 10 | "keywords": [ 11 | "server", 12 | "https", 13 | "livereload", 14 | "spa" 15 | ], 16 | "scripts": { 17 | "start": "sudo node cli tests/example --reload --browse --secure --static --module", 18 | "cleanup": "rm -f servor.key servor.crt", 19 | "test": "npm run cleanup && cd tests && node index.js" 20 | }, 21 | "author": "Luke Jackson ", 22 | "license": "MIT", 23 | "devDependencies": { 24 | "puppeteer": "^3.0.4" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /utils/openBrowser.js: -------------------------------------------------------------------------------- 1 | const childProcess = require('child_process'); 2 | 3 | module.exports = (url) => { 4 | let cmd; 5 | const args = []; 6 | 7 | if (process.platform === 'darwin') { 8 | try { 9 | childProcess.execSync( 10 | `osascript openChrome.applescript "${encodeURI(url)}"`, 11 | { 12 | cwd: __dirname, 13 | stdio: 'ignore', 14 | } 15 | ); 16 | return true; 17 | } catch (err) {} 18 | cmd = 'open'; 19 | } else if (process.platform === 'win32') { 20 | cmd = 'cmd.exe'; 21 | args.push('/c', 'start', '""', '/b'); 22 | url = url.replace(/&/g, '^&'); 23 | } else { 24 | cmd = 'xdg-open'; 25 | } 26 | 27 | args.push(url); 28 | childProcess.spawn(cmd, args); 29 | }; 30 | -------------------------------------------------------------------------------- /tests/new/app.js: -------------------------------------------------------------------------------- 1 | // Simple react app by @lukejacksonn 2 | // ---------------- 3 | 4 | import { react, html, css } from 'https://unpkg.com/rplus'; 5 | 6 | const random = () => `${Math.random() * 80}%`; 7 | const style = css` 8 | position: absolute; 9 | font-size: 2rem; 10 | width: 5rem; 11 | height: 5rem; 12 | border-radius: 50%; 13 | border: 5px solid black; 14 | font-weight: bold; 15 | `; 16 | 17 | const app = () => { 18 | const [score, setScore] = react.useState(0); 19 | return html` 20 | 27 | `; 28 | }; 29 | 30 | react.render(html`<${app} />`, document.body); 31 | -------------------------------------------------------------------------------- /tests/example/assets/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | display: block; 3 | flex: none; 4 | margin: 0; 5 | padding: 0; 6 | border: 0; 7 | color: #f2f2f2; 8 | box-sizing: border-box; 9 | -webkit-tap-highlight-color: transparent; 10 | } 11 | 12 | main { 13 | font-family: 'Roboto', sans-serif; 14 | font-size: 16px; 15 | background: #212121; 16 | height: 100vh; 17 | display: flex; 18 | align-items: center; 19 | justify-content: center; 20 | flex-direction: column; 21 | } 22 | 23 | head, 24 | script, 25 | style { 26 | display: none; 27 | } 28 | 29 | img { 30 | width: 30vmin; 31 | opacity: 0.38; 32 | } 33 | 34 | h1 { 35 | font-size: 20vmin; 36 | } 37 | 38 | h2 { 39 | color: #6f6f6f; 40 | font-size: 4vmin; 41 | font-weight: normal; 42 | } 43 | 44 | code { 45 | display: inline; 46 | } 47 | -------------------------------------------------------------------------------- /tests/example/nested/assets/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | display: block; 3 | flex: none; 4 | margin: 0; 5 | padding: 0; 6 | border: 0; 7 | color: #f2f2f2; 8 | box-sizing: border-box; 9 | -webkit-tap-highlight-color: transparent; 10 | } 11 | 12 | body { 13 | font-family: 'Roboto', sans-serif; 14 | font-size: 16px; 15 | background: #212121; 16 | height: 100vh; 17 | display: flex; 18 | align-items: center; 19 | justify-content: center; 20 | flex-direction: column; 21 | } 22 | 23 | head, 24 | script, 25 | style { 26 | display: none; 27 | } 28 | 29 | img { 30 | width: 30vmin; 31 | opacity: 0.38; 32 | } 33 | 34 | h1 { 35 | font-size: 20vmin; 36 | } 37 | 38 | h2 { 39 | color: #9f9f9f; 40 | font-size: 4vmin; 41 | font-weight: normal; 42 | } 43 | 44 | code { 45 | display: inline; 46 | } 47 | -------------------------------------------------------------------------------- /utils/common.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const os = require('os'); 3 | const net = require('net'); 4 | 5 | const fileWatch = 6 | process.platform !== 'linux' 7 | ? (x, cb) => fs.watch(x, { recursive: true }, cb) 8 | : (x, cb) => { 9 | if (fs.statSync(x).isDirectory()) { 10 | fs.watch(x, cb); 11 | fs.readdirSync(x).forEach((xx) => fileWatch(`${x}/${xx}`, cb)); 12 | } 13 | }; 14 | 15 | module.exports.fileWatch = fileWatch; 16 | 17 | const usePort = (port = 0) => 18 | new Promise((ok, x) => { 19 | const s = net.createServer(); 20 | s.on('error', x); 21 | s.listen(port, () => (a = s.address()) && s.close(() => ok(a.port))); 22 | }); 23 | 24 | module.exports.usePort = usePort; 25 | 26 | const networkIps = Object.values(os.networkInterfaces()) 27 | .reduce((every, i) => [...every, ...i], []) 28 | .filter((i) => i.family === 'IPv4' && i.internal === false) 29 | .map((i) => i.address); 30 | 31 | module.exports.networkIps = networkIps; 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Luke Jackson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/example/index.js: -------------------------------------------------------------------------------- 1 | /* SERVOR_TEST_MODULE_INDEX */ 2 | 3 | import { react, html, css } from 'https://unpkg.com/rplus'; 4 | 5 | document.title = 'SERVOR_TEST_MODULE_INDEX'; 6 | 7 | const style = css` 8 | font-family: 'Roboto', sans-serif; 9 | font-size: 16px; 10 | background: #212121; 11 | color: #f2f2f2; 12 | height: 100vh; 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | justify-content: center; 17 | 18 | :global(*) { 19 | display: block; 20 | flex: none; 21 | margin: 0; 22 | box-sizing: border-box; 23 | -webkit-tap-highlight-color: transparent; 24 | } 25 | 26 | :global(head), 27 | :global(script), 28 | :global(style) { 29 | display: none; 30 | } 31 | 32 | img { 33 | width: 30vmin; 34 | opacity: 0.38; 35 | } 36 | 37 | h1 { 38 | font-size: 20vmin; 39 | } 40 | 41 | h2 { 42 | color: #6f6f6f; 43 | font-size: 4vmin; 44 | font-weight: normal; 45 | } 46 | `; 47 | 48 | react.render( 49 | html` 50 |
51 | File that exists 52 |

servør

53 |

${location.href}

54 |
55 | `, 56 | document.body 57 | ); 58 | -------------------------------------------------------------------------------- /tests/example/nested/index.js: -------------------------------------------------------------------------------- 1 | /* SERVOR_TEST_NESTED_MODULE_INDEX */ 2 | 3 | import { react, html, css } from 'https://unpkg.com/rplus'; 4 | import component from './component.js'; 5 | 6 | document.title = 'SERVOR_TEST_NESTED_MODULE_INDEX'; 7 | 8 | console.log(component); 9 | 10 | const style = css` 11 | font-family: 'Roboto', sans-serif; 12 | font-size: 16px; 13 | background: #333; 14 | color: #f2f2f2; 15 | height: 100vh; 16 | display: flex; 17 | flex-direction: column; 18 | align-items: center; 19 | justify-content: center; 20 | 21 | :global(*) { 22 | display: block; 23 | flex: none; 24 | margin: 0; 25 | box-sizing: border-box; 26 | -webkit-tap-highlight-color: transparent; 27 | } 28 | 29 | :global(head), 30 | :global(script), 31 | :global(style) { 32 | display: none; 33 | } 34 | 35 | img { 36 | width: 30vmin; 37 | opacity: 0.38; 38 | } 39 | 40 | h1 { 41 | font-size: 20vmin; 42 | } 43 | 44 | h2 { 45 | color: #6f6f6f; 46 | font-size: 4vmin; 47 | font-weight: normal; 48 | } 49 | `; 50 | 51 | react.render( 52 | html` 53 |
54 | File that exists 55 |

servør

56 |

${location.href}

57 |
58 | `, 59 | document.body 60 | ); 61 | -------------------------------------------------------------------------------- /certify.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | name="servor" 4 | 5 | function command_exists () { 6 | type "$1" &> /dev/null ; 7 | } 8 | 9 | # Make sure openssl exists 10 | if ! command_exists openssl ; then 11 | echo "OpenSSL isn't installed. You need that to generate SSL certificates." 12 | exit 13 | fi 14 | 15 | ## Make sure the tmp/ directory exists 16 | if [ ! -d "tmp" ]; then 17 | mkdir tmp/ 18 | fi 19 | 20 | # Generate Certificate Authority 21 | openssl genrsa -out "tmp/${name}CA.key" 2048 &>/dev/null 22 | openssl req -x509 -config utils/ca.conf -new -nodes -key "tmp/${name}CA.key" -sha256 -days 1825 -out "${name}CA.pem" &>/dev/null 23 | 24 | # This is the part that demands root privileges 25 | if [ "$EUID" -eq 0 ] ; then 26 | if command_exists security ; then 27 | # Delete trusted certs by their common name via https://unix.stackexchange.com/a/227014 28 | security find-certificate -c "${name}" -a -Z | sudo awk '/SHA-1/{system("security delete-certificate -Z "$NF)}' 29 | # Trust the Root Certificate cert 30 | security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${name}CA.pem" 31 | fi 32 | fi 33 | 34 | # Generate CA-signed Certificate 35 | openssl genrsa -out "${name}.key" 2048 &>/dev/null 36 | openssl req -new -config utils/ca.conf -key "${name}.key" -out "tmp/${name}.csr" &>/dev/null 37 | 38 | # Generate SSL Certificate 39 | openssl x509 -req -in "tmp/${name}.csr" -CA "${name}CA.pem" -CAkey "tmp/${name}CA.key" -CAcreateserial -out "${name}.crt" -days 1825 -sha256 -extfile utils/ssl.conf &>/dev/null 40 | 41 | # Cleanup files 42 | rm servorCA.pem servorCA.srl 43 | rm -rf tmp 44 | -------------------------------------------------------------------------------- /utils/directoryListing.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | module.exports = (uri) => { 5 | const dir = (x) => fs.statSync(path.join(uri, x)).isDirectory(); 6 | const size = (x) => fs.statSync(path.join(uri, x)).size; 7 | 8 | const link = (x) => 9 | dir(x) 10 | ? ` 11 |
12 | 🗂 13 | ${x} 14 | ${size(x)}B 15 |
16 | ` 17 | : ` 18 |
19 | 📄 20 | ${x} 21 | ${size(x)}B 22 |
23 | `; 24 | 25 | return ` 26 | 27 | 28 | 63 | 64 | 65 |
66 | ${fs.readdirSync(uri).map(link).join('')} 67 |
68 | 69 | 70 | `; 71 | }; 72 | -------------------------------------------------------------------------------- /utils/openChrome.applescript: -------------------------------------------------------------------------------- 1 | (* 2 | Copyright (c) 2015-present, Facebook, Inc. 3 | This source code is licensed under the MIT license found in the 4 | LICENSE file in the root directory of this source tree. 5 | *) 6 | 7 | property targetTab: null 8 | property targetTabIndex: -1 9 | property targetWindow: null 10 | 11 | on run argv 12 | set theURL to item 1 of argv 13 | 14 | tell application "Chrome" 15 | 16 | if (count every window) = 0 then 17 | make new window 18 | end if 19 | 20 | -- 1: Looking for tab running debugger 21 | -- then, Reload debugging tab if found 22 | -- then return 23 | set found to my lookupTabWithUrl(theURL) 24 | if found then 25 | set targetWindow's active tab index to targetTabIndex 26 | tell targetTab to reload 27 | tell targetWindow to activate 28 | set index of targetWindow to 1 29 | return 30 | end if 31 | 32 | -- 2: Looking for Empty tab 33 | -- In case debugging tab was not found 34 | -- We try to find an empty tab instead 35 | set found to my lookupTabWithUrl("chrome://newtab/") 36 | if found then 37 | set targetWindow's active tab index to targetTabIndex 38 | set URL of targetTab to theURL 39 | tell targetWindow to activate 40 | return 41 | end if 42 | 43 | -- 3: Create new tab 44 | -- both debugging and empty tab were not found 45 | -- make a new tab with url 46 | tell window 1 47 | activate 48 | make new tab with properties {URL:theURL} 49 | end tell 50 | end tell 51 | end run 52 | 53 | -- Function: 54 | -- Lookup tab with given url 55 | -- if found, store tab, index, and window in properties 56 | -- (properties were declared on top of file) 57 | on lookupTabWithUrl(lookupUrl) 58 | tell application "Chrome" 59 | -- Find a tab with the given url 60 | set found to false 61 | set theTabIndex to -1 62 | repeat with theWindow in every window 63 | set theTabIndex to 0 64 | repeat with theTab in every tab of theWindow 65 | set theTabIndex to theTabIndex + 1 66 | if (theTab's URL as string) contains lookupUrl then 67 | -- assign tab, tab index, and window to properties 68 | set targetTab to theTab 69 | set targetTabIndex to theTabIndex 70 | set targetWindow to theWindow 71 | set found to true 72 | exit repeat 73 | end if 74 | end repeat 75 | 76 | if found then 77 | exit repeat 78 | end if 79 | end repeat 80 | end tell 81 | return found 82 | end lookupTabWithUrl -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require('fs'); 3 | const servor = require('./servor.js'); 4 | const openBrowser = require('./utils/openBrowser.js'); 5 | 6 | const readCredentials = () => ({ 7 | cert: fs.readFileSync(__dirname + '/servor.crt'), 8 | key: fs.readFileSync(__dirname + '/servor.key'), 9 | }); 10 | 11 | const certify = () => 12 | require('child_process').execSync(__dirname + '/certify.sh', { 13 | cwd: __dirname, 14 | }); 15 | 16 | const open = 17 | process.platform == 'darwin' 18 | ? 'open' 19 | : process.platform == 'win32' 20 | ? 'start' 21 | : 'xdg-open'; 22 | 23 | (async () => { 24 | const args = process.argv.slice(2).filter((x) => !~x.indexOf('--')); 25 | const admin = process.getuid && process.getuid() === 0; 26 | let credentials; 27 | 28 | if (args[0] && args[0].startsWith('gh:')) { 29 | const repo = args[0].replace('gh:', ''); 30 | const dest = repo.split('/')[1]; 31 | if (!fs.existsSync(dest)) { 32 | try { 33 | require('child_process').execSync( 34 | `git clone https://github.com/${repo}`, 35 | { stdio: 'ignore' } 36 | ); 37 | } catch (e) { 38 | console.log( 39 | `\n ⚠️ Could not clone from https://github.com/${repo}\n` 40 | ); 41 | process.exit(); 42 | } 43 | } 44 | args[0] = dest; 45 | } 46 | 47 | if (~process.argv.indexOf('--editor')) { 48 | try { 49 | require('child_process').execSync(`code ${args[0] || '.'}`); 50 | } catch (e) { 51 | console.log(`\n ⚠️ Could not open code editor for ${args[0] || '.'}`); 52 | } 53 | } 54 | 55 | // Generate ssl certificates 56 | 57 | if (~process.argv.indexOf('--secure')) { 58 | admin && certify(); 59 | admin && process.platform === 'darwin' && process.setuid(501); 60 | try { 61 | credentials = readCredentials(); 62 | } catch (e) { 63 | certify(); 64 | try { 65 | credentials = readCredentials(); 66 | } catch (e) { 67 | console.log( 68 | '\n ⚠️ There was a problem generating ssl credentials. Try removing `--secure`\n' 69 | ); 70 | process.exit(); 71 | } 72 | } 73 | } 74 | 75 | // Parse arguments from the command line 76 | 77 | const { root, protocol, port, ips, url } = await servor({ 78 | root: args[0], 79 | fallback: args[1], 80 | port: args[2], 81 | reload: !!~process.argv.indexOf('--reload'), 82 | module: !!~process.argv.indexOf('--module'), 83 | static: !!~process.argv.indexOf('--static'), 84 | credentials, 85 | }); 86 | 87 | // Output server details to the console 88 | 89 | !~process.argv.indexOf('--silent') && 90 | console.log(` 91 | 🗂 Serving:\t${root}\n 92 | 🏡 Local:\t${url} 93 | ${ips.map((ip) => `📡 Network:\t${protocol}://${ip}:${port}`).join('\n ')} 94 | `); 95 | 96 | // Browser the server index 97 | 98 | !!~process.argv.indexOf('--browse') && openBrowser(url); 99 | })(); 100 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const puppeteer = require('puppeteer'); 3 | const cp = require('child_process'); 4 | 5 | const matches = (obj, source) => 6 | Object.keys(source).every( 7 | (key) => 8 | obj.hasOwnProperty(key) && 9 | JSON.stringify(obj[key]) === JSON.stringify(source[key]) 10 | ); 11 | 12 | const modifyFile = (x) => 13 | fs.readFile(x, 'utf-8', (_, data) => { 14 | fs.writeFileSync(x, data, 'utf-8'); 15 | }); 16 | 17 | const test = (cmd) => (url) => async (expect) => { 18 | // Make sure nothing is running on port 8080 19 | cp.execSync( 20 | "lsof -n -i4TCP:8080 | grep LISTEN | awk '{ print $2 }' | xargs kill" 21 | ); 22 | 23 | // Run the command and wait for the server to start 24 | const [c, ...a] = ('node ../cli example ' + cmd).trim().split(' '); 25 | const servor = cp.spawn(c, a); 26 | const { origin } = await new Promise((resolve) => 27 | servor.stdout.once('data', (out) => { 28 | resolve(new URL(out.toString().match(/Local:\t(.*)\n/)[1])); 29 | }) 30 | ); 31 | 32 | const browser = await puppeteer.launch({ 33 | ignoreHTTPSErrors: true, 34 | headless: true, 35 | slowMo: 0, 36 | }); 37 | 38 | // Load new page and go to url 39 | const page = await browser.newPage(); 40 | await page.setCacheEnabled(false); 41 | 42 | const res = await page.goto(`${origin}${url}`); 43 | 44 | // Collect data from response and page 45 | const status = res.status(); 46 | const headers = res.headers(); 47 | const content = await page.content(); 48 | 49 | // Change a file to trigger reload 50 | let reload = false; 51 | if (cmd.includes('--reload')) { 52 | modifyFile('example/index.html'); 53 | reload = await page.waitForNavigation({ timeout: 1000 }).catch(() => false); 54 | modifyFile('example/assets/index.js'); 55 | reload = await page.waitForNavigation({ timeout: 1000 }).catch(() => false); 56 | } 57 | 58 | const result = { 59 | status, 60 | reload: !!reload, 61 | gzip: headers['content-encoding'] === 'gzip', 62 | cors: headers['access-control-allow-origin'] === '*', 63 | includes: [ 64 | 'SERVOR_TEST_INDEX', 65 | 'SERVOR_TEST_NESTED_INDEX', 66 | 'SERVOR_TEST_MODULE_INDEX', 67 | 'SERVOR_TEST_NESTED_MODULE_INDEX', 68 | ].filter((x) => content.includes(x)), 69 | }; 70 | 71 | const passed = matches(result, expect); 72 | console.log( 73 | passed 74 | ? { ['PASSED']: { cmd, url, out: JSON.stringify(result) } } 75 | : { ['FAILED']: { cmd, url, result, expect } } 76 | ); 77 | 78 | servor.kill(); 79 | await browser.close(); 80 | }; 81 | 82 | (async () => { 83 | const base = { status: 200, gzip: true, cors: true, reload: false }; 84 | 85 | await test('')('/')({ 86 | ...base, 87 | includes: ['SERVOR_TEST_INDEX'], 88 | }); 89 | 90 | await test('')('/index.html')({ 91 | ...base, 92 | includes: ['SERVOR_TEST_INDEX'], 93 | }); 94 | 95 | await test('')('/assets/file with space.html')({ 96 | ...base, 97 | }); 98 | 99 | await test('')('/assets/exists.png')({ 100 | ...base, 101 | gzip: false, 102 | }); 103 | 104 | await test('')('/assets/no-exists.png')({ 105 | ...base, 106 | status: 404, 107 | gzip: false, 108 | }); 109 | 110 | await test('')('/nested')({ 111 | ...base, 112 | status: 301, 113 | includes: ['SERVOR_TEST_INDEX'], 114 | }); 115 | 116 | await test('')('/nested/assets/exists.png')({ 117 | ...base, 118 | gzip: false, 119 | }); 120 | 121 | await test('')('/nested/assets/no-xists.png')({ 122 | ...base, 123 | status: 404, 124 | gzip: false, 125 | }); 126 | 127 | await test('--reload')('/')({ 128 | ...base, 129 | reload: true, 130 | includes: ['SERVOR_TEST_INDEX'], 131 | }); 132 | 133 | await test('--static')('/')({ 134 | ...base, 135 | includes: ['SERVOR_TEST_INDEX'], 136 | }); 137 | 138 | await test('--static')('/nested')({ 139 | ...base, 140 | includes: ['SERVOR_TEST_NESTED_INDEX'], 141 | }); 142 | 143 | await test('--static')('/broken-nested')({ 144 | ...base, 145 | status: 404, 146 | gzip: false, 147 | }); 148 | 149 | await test('--module')('/')({ 150 | ...base, 151 | includes: ['SERVOR_TEST_MODULE_INDEX'], 152 | }); 153 | 154 | await test('--secure')('/')({ 155 | ...base, 156 | includes: ['SERVOR_TEST_INDEX'], 157 | }); 158 | 159 | await test('--secure --reload --static --module')('/')({ 160 | ...base, 161 | reload: true, 162 | includes: ['SERVOR_TEST_MODULE_INDEX'], 163 | }); 164 | 165 | await test('--secure --reload --static --module')('/nested')({ 166 | ...base, 167 | reload: true, 168 | includes: ['SERVOR_TEST_NESTED_MODULE_INDEX'], 169 | }); 170 | })(); 171 | -------------------------------------------------------------------------------- /servor.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const url = require('url'); 3 | const path = require('path'); 4 | const http = require('http'); 5 | const http2 = require('http2'); 6 | const https = require('https'); 7 | const zlib = require('zlib'); 8 | 9 | const mimeTypes = require('./utils/mimeTypes.js'); 10 | const directoryListing = require('./utils/directoryListing.js'); 11 | 12 | const { fileWatch, usePort, networkIps } = require('./utils/common.js'); 13 | 14 | module.exports = async ({ 15 | root = '.', 16 | module = false, 17 | fallback = module ? 'index.js' : 'index.html', 18 | reload = true, 19 | static = false, 20 | inject = '', 21 | credentials, 22 | port, 23 | } = {}) => { 24 | // Try start on specified port then fail or find a free port 25 | 26 | try { 27 | port = await usePort(port || process.env.PORT || 8080); 28 | } catch (e) { 29 | if (port || process.env.PORT) { 30 | console.log('[ERR] The port you have specified is already in use!'); 31 | process.exit(); 32 | } 33 | port = await usePort(); 34 | } 35 | 36 | // Configure globals 37 | 38 | root = root.startsWith('/') ? root : path.join(process.cwd(), root); 39 | 40 | if (!fs.existsSync(root)) { 41 | console.log(`[ERR] Root directory ${root} does not exist!`); 42 | process.exit(); 43 | } 44 | 45 | if (!fs.statSync(root).isDirectory()) { 46 | console.log(`[ERR] Root directory "${root}" is not directory!`); 47 | process.exit(); 48 | } 49 | 50 | const reloadClients = []; 51 | const protocol = credentials ? 'https' : 'http'; 52 | const server = credentials 53 | ? reload 54 | ? (cb) => https.createServer(credentials, cb) 55 | : (cb) => http2.createSecureServer(credentials, cb) 56 | : (cb) => http.createServer(cb); 57 | 58 | const livereload = reload 59 | ? ` 60 | 67 | ` 68 | : ''; 69 | 70 | // Server utility functions 71 | 72 | const isRouteRequest = (pathname) => !~pathname.split('/').pop().indexOf('.'); 73 | const utf8 = (file) => Buffer.from(file, 'binary').toString('utf8'); 74 | 75 | const baseDoc = (pathname = '', base = path.join('/', pathname, '/')) => 76 | ``; 77 | 78 | const sendError = (res, status) => { 79 | res.writeHead(status); 80 | res.write(`${status}`); 81 | res.end(); 82 | }; 83 | 84 | const sendFile = (res, status, file, ext, encoding = 'binary') => { 85 | if (['js', 'css', 'html', 'json', 'xml', 'svg'].includes(ext)) { 86 | res.setHeader('content-encoding', 'gzip'); 87 | file = zlib.gzipSync(utf8(file)); 88 | encoding = 'utf8'; 89 | } 90 | res.writeHead(status, { 'content-type': mimeTypes(ext) }); 91 | res.write(file, encoding); 92 | res.end(); 93 | }; 94 | 95 | const sendMessage = (res, channel, data) => { 96 | res.write(`event: ${channel}\nid: 0\ndata: ${data}\n`); 97 | res.write('\n\n'); 98 | }; 99 | 100 | // Respond to reload requests with keep alive 101 | 102 | const serveReload = (res) => { 103 | res.writeHead(200, { 104 | connection: 'keep-alive', 105 | 'content-type': 'text/event-stream', 106 | 'cache-control': 'no-cache', 107 | }); 108 | sendMessage(res, 'connected', 'ready'); 109 | setInterval(sendMessage, 60000, res, 'ping', 'waiting'); 110 | reloadClients.push(res); 111 | }; 112 | 113 | // Respond to requests with a file extension 114 | 115 | const serveStaticFile = (res, pathname) => { 116 | const uri = path.join(root, pathname); 117 | let ext = uri.replace(/^.*[\.\/\\]/, '').toLowerCase(); 118 | if (!fs.existsSync(uri)) return sendError(res, 404); 119 | fs.readFile(uri, 'binary', (err, file) => 120 | err ? sendError(res, 500) : sendFile(res, 200, file, ext) 121 | ); 122 | }; 123 | 124 | // Respond to requests without a file extension 125 | 126 | const serveRoute = (res, pathname) => { 127 | const index = static 128 | ? path.join(root, pathname, fallback) 129 | : path.join(root, fallback); 130 | if (!fs.existsSync(index) || (pathname.endsWith('/') && pathname !== '/')) 131 | return serveDirectoryListing(res, pathname); 132 | fs.readFile(index, 'binary', (err, file) => { 133 | if (err) return sendError(res, 500); 134 | const status = pathname === '/' || static ? 200 : 301; 135 | if (module) file = ``; 136 | if (static) file = baseDoc(pathname) + file; 137 | file = file + inject + livereload; 138 | sendFile(res, status, file, 'html'); 139 | }); 140 | }; 141 | 142 | // Respond to requests with a trailing slash 143 | 144 | const serveDirectoryListing = (res, pathname) => { 145 | const uri = path.join(root, pathname); 146 | if (!fs.existsSync(uri)) return sendError(res, 404); 147 | res.writeHead(200, { 'Content-Type': 'text/html' }); 148 | res.write(baseDoc(pathname) + directoryListing(uri) + livereload); 149 | res.end(); 150 | }; 151 | 152 | // Start the server and route requests 153 | 154 | server((req, res) => { 155 | const decodePathname = decodeURI(url.parse(req.url).pathname); 156 | const pathname = path.normalize(decodePathname).replace(/^(\.\.(\/|\\|$))+/, ''); 157 | res.setHeader('access-control-allow-origin', '*'); 158 | if (reload && pathname === '/livereload') return serveReload(res); 159 | if (!isRouteRequest(pathname)) return serveStaticFile(res, pathname); 160 | return serveRoute(res, pathname); 161 | }).listen(parseInt(port, 10)); 162 | 163 | // Notify livereload reloadClients on file change 164 | 165 | reload && 166 | fileWatch(root, () => { 167 | while (reloadClients.length > 0) 168 | sendMessage(reloadClients.pop(), 'message', 'reload'); 169 | }); 170 | 171 | // Close socket connections on sigint 172 | 173 | process.on('SIGINT', () => { 174 | while (reloadClients.length > 0) reloadClients.pop().end(); 175 | process.exit(); 176 | }); 177 | 178 | const x = { url: `${protocol}://localhost:${port}` }; 179 | return { ...x, root, protocol, port, ips: networkIps }; 180 | }; 181 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Servør 2 | 3 | > A dependency free dev server for modern web application development 4 | 5 | A very compact but capable static file server with https, live reloading, gzip and other useful features to support modern web app development on localhost and over a local network. The motivation here was to write a package from the ground up with no dependencies; using only native, node and browser APIs to do some specific tasks with minimal code. 6 | 7 | Servør can be invoked via the command line or programmatically using the node API. 8 | 9 | **Quickstart Example** 10 | 11 | The following command instructs servør to; clone [perflink](https://github.com/lukejacksonn/perflink), start a server at the project root, open the url in a browser, open the source code in an editor and reload the browser when files change. 12 | 13 | ```s 14 | npx servor gh:lukejacksonn/perflink --browse --editor --reload 15 | ``` 16 | 17 | Most features are disabled by default but you can customize behaviour by passing positional arguments and flags to enable features. 18 | 19 |
20 | 21 | servor 22 | 23 | ## Features 24 | 25 | - 🗂 Serves static content like scripts, styles, images from a given directory 26 | - ♻️ Reloads the browser when project files get added, removed or modified 27 | - 🗜 Uses gzip on common filetypes like html, css, js and json 28 | - 🔐 Supports https and http2 with trusted self signed certificates 29 | - 🖥 Redirects all path requests to a single file for frontend routing 30 | - 📦 Accepts both HTML and JavaScript files as the root file for a directory 31 | - 🔎 Discovers freely available ports to start if the default is in use 32 | - 📄 Renders directory listing for urls ending with a trailing slash 33 | - 🗃 Opens browser tab and code editor to streamline quick start 34 | 35 | ## CLI Usage 36 | 37 | Run as a terminal command without adding it as a dependency using `npx`: 38 | 39 | ```s 40 | npx servor 41 | ``` 42 | 43 | > You can pass a GitHub repo as `` using the syntax `gh:/` 44 | 45 | - `` path to serve static files from (defaults to current directory `.`) 46 | - `` the file served for all non-file requests (defaults to `index.html`) 47 | - `` what port you want to serve the files from (defaults to `8080`) 48 | 49 | Optional flags passed as non-positional arguments: 50 | 51 | - `--browse` causes the browser to open when the server starts 52 | - `--reload` causes the browser to reload when files change 53 | - `--secure` starts the server with https using generated credentials 54 | - `--silent` prevents the server node process from logging to stdout 55 | - `--module` causes the server to wrap the root in script type module tags 56 | - `--static` causes the server to route nested index files if they exist 57 | - `--editor` opens a code editor (currently only vscode) at the project root 58 | 59 | Example usage with npm scripts in a `package.json` file after running `npm i servor -D`: 60 | 61 | ```json 62 | { 63 | "devDependencies": { 64 | "servor": "4.0.0" 65 | }, 66 | "scripts": { 67 | "start": "servor www index.html 8080 --reload --browse" 68 | } 69 | } 70 | ``` 71 | 72 | ### Generating Credentials 73 | 74 | > NOTE: This process depends on the `openssl` command existing (tested on macOS and linux only) 75 | 76 | The files `servor.crt` and `servor.key` need to exist for the server to start using https. If the files do not exist when the `--secure` flag is passed, then [`certify.sh`](/certify.sh) is invoked which: 77 | 78 | - Creates a local certificate authority used to generate self signed SSL certificates 79 | - Runs the appropriate `openssl` commands to produce: 80 | - a root certificate (pem) so the system will trust the self signed certificate 81 | - a public certificate (crt) that the server sends to clients 82 | - a private key for the certificate (key) to encrypt and decrypt traffic 83 | 84 | #### Adding credentials to the trusted store 85 | 86 | > NOTE: This process depends on the `sudo` and `security` commands existing (tested on macOS only) 87 | 88 | For the browser to trust self signed certificates the root certificate must be added to the system trusted store. This can be done automatically by running `sudo servor --secure` which: 89 | 90 | - Adds the root certificate to the system Keychain Access 91 | - Prevents the "⚠️ Your connection is not private" screen 92 | - Makes the 🔒 icon appear in the browsers address bar 93 | 94 | The approach was adopted from [@kingkool68/generate-ssl-certs-for-local-development](https://github.com/kingkool68/generate-ssl-certs-for-local-development) 95 | 96 | ## API Usage 97 | 98 | Use servor programmatically with node by requiring it as a module in your script: 99 | 100 | ```js 101 | const servor = require('servor'); 102 | const instance = await servor({ 103 | root: '.', 104 | fallback: 'index.html', 105 | module: false, 106 | static: false, 107 | reload: false, 108 | inject: '' 109 | credentials: null, 110 | port: 8080, 111 | }); 112 | ``` 113 | 114 | The `servor` function accepts a config object with optional props assigned the above default values if none are provided. Calling the `servor` function starts up a new server and returns an object describing its configuration. 115 | 116 | ```js 117 | const { url, root, protocol, port, ips } = await servor(config); 118 | ``` 119 | 120 | ### Inject 121 | 122 | The `inject` property accepts a string that gets appended to the servers root document (which is `index.html` by default). This could be used to inject config or extend the development servers behavior and capabilities to suit specific environments. 123 | 124 | ```js 125 | const config = require('package.json'); 126 | servor({ inject: `` }); 127 | ``` 128 | 129 | ### Credentials 130 | 131 | The `credentials` property accepts an object containing the entries `cert` and `key` which must both be valid for the server to start successfully. If valid credentials are provided then the server will start serving over https. 132 | 133 | It is possible to generate the appropriate credentials using the `--secure` CLI flag. 134 | 135 | ## Notes 136 | 137 | Thanks to all the contributors to this projects so far. If you find a bug please create an issue or if you have an idea for a new feature then fork the project and create a pull request. Let me know how you are using servør [on twitter](https://twitter.com/lukejacksonn). 138 | -------------------------------------------------------------------------------- /utils/mimeTypes.js: -------------------------------------------------------------------------------- 1 | const types = { 2 | 'application/andrew-inset': ['ez'], 3 | 'application/applixware': ['aw'], 4 | 'application/atom+xml': ['atom'], 5 | 'application/atomcat+xml': ['atomcat'], 6 | 'application/atomsvc+xml': ['atomsvc'], 7 | 'application/bdoc': ['bdoc'], 8 | 'application/ccxml+xml': ['ccxml'], 9 | 'application/cdmi-capability': ['cdmia'], 10 | 'application/cdmi-container': ['cdmic'], 11 | 'application/cdmi-domain': ['cdmid'], 12 | 'application/cdmi-object': ['cdmio'], 13 | 'application/cdmi-queue': ['cdmiq'], 14 | 'application/cu-seeme': ['cu'], 15 | 'application/dash+xml': ['mpd'], 16 | 'application/davmount+xml': ['davmount'], 17 | 'application/docbook+xml': ['dbk'], 18 | 'application/dssc+der': ['dssc'], 19 | 'application/dssc+xml': ['xdssc'], 20 | 'application/ecmascript': ['ecma'], 21 | 'application/emma+xml': ['emma'], 22 | 'application/epub+zip': ['epub'], 23 | 'application/exi': ['exi'], 24 | 'application/font-tdpfr': ['pfr'], 25 | 'application/font-woff': [], 26 | 'application/font-woff2': [], 27 | 'application/geo+json': ['geojson'], 28 | 'application/gml+xml': ['gml'], 29 | 'application/gpx+xml': ['gpx'], 30 | 'application/gxf': ['gxf'], 31 | 'application/gzip': ['gz'], 32 | 'application/hyperstudio': ['stk'], 33 | 'application/inkml+xml': ['ink', 'inkml'], 34 | 'application/ipfix': ['ipfix'], 35 | 'application/java-archive': ['jar', 'war', 'ear'], 36 | 'application/java-serialized-object': ['ser'], 37 | 'application/java-vm': ['class'], 38 | 'application/javascript': ['js', 'mjs'], 39 | 'application/json': ['json', 'map'], 40 | 'application/json5': ['json5'], 41 | 'application/jsonml+json': ['jsonml'], 42 | 'application/ld+json': ['jsonld'], 43 | 'application/lost+xml': ['lostxml'], 44 | 'application/mac-binhex40': ['hqx'], 45 | 'application/mac-compactpro': ['cpt'], 46 | 'application/mads+xml': ['mads'], 47 | 'application/manifest+json': ['webmanifest'], 48 | 'application/marc': ['mrc'], 49 | 'application/marcxml+xml': ['mrcx'], 50 | 'application/mathematica': ['ma', 'nb', 'mb'], 51 | 'application/mathml+xml': ['mathml'], 52 | 'application/mbox': ['mbox'], 53 | 'application/mediaservercontrol+xml': ['mscml'], 54 | 'application/metalink+xml': ['metalink'], 55 | 'application/metalink4+xml': ['meta4'], 56 | 'application/mets+xml': ['mets'], 57 | 'application/mods+xml': ['mods'], 58 | 'application/mp21': ['m21', 'mp21'], 59 | 'application/mp4': ['mp4s', 'm4p'], 60 | 'application/msword': ['doc', 'dot'], 61 | 'application/mxf': ['mxf'], 62 | 'application/octet-stream': [ 63 | 'bin', 64 | 'dms', 65 | 'lrf', 66 | 'mar', 67 | 'so', 68 | 'dist', 69 | 'distz', 70 | 'pkg', 71 | 'bpk', 72 | 'dump', 73 | 'elc', 74 | 'deploy', 75 | 'exe', 76 | 'dll', 77 | 'deb', 78 | 'dmg', 79 | 'iso', 80 | 'img', 81 | 'msi', 82 | 'msp', 83 | 'msm', 84 | 'buffer', 85 | ], 86 | 'application/oda': ['oda'], 87 | 'application/oebps-package+xml': ['opf'], 88 | 'application/ogg': ['ogx'], 89 | 'application/omdoc+xml': ['omdoc'], 90 | 'application/onenote': ['onetoc', 'onetoc2', 'onetmp', 'onepkg'], 91 | 'application/oxps': ['oxps'], 92 | 'application/patch-ops-error+xml': ['xer'], 93 | 'application/pdf': ['pdf'], 94 | 'application/pgp-encrypted': ['pgp'], 95 | 'application/pgp-signature': ['asc', 'sig'], 96 | 'application/pics-rules': ['prf'], 97 | 'application/pkcs10': ['p10'], 98 | 'application/pkcs7-mime': ['p7m', 'p7c'], 99 | 'application/pkcs7-signature': ['p7s'], 100 | 'application/pkcs8': ['p8'], 101 | 'application/pkix-attr-cert': ['ac'], 102 | 'application/pkix-cert': ['cer'], 103 | 'application/pkix-crl': ['crl'], 104 | 'application/pkix-pkipath': ['pkipath'], 105 | 'application/pkixcmp': ['pki'], 106 | 'application/pls+xml': ['pls'], 107 | 'application/postscript': ['ai', 'eps', 'ps'], 108 | 'application/prs.cww': ['cww'], 109 | 'application/pskc+xml': ['pskcxml'], 110 | 'application/raml+yaml': ['raml'], 111 | 'application/rdf+xml': ['rdf'], 112 | 'application/reginfo+xml': ['rif'], 113 | 'application/relax-ng-compact-syntax': ['rnc'], 114 | 'application/resource-lists+xml': ['rl'], 115 | 'application/resource-lists-diff+xml': ['rld'], 116 | 'application/rls-services+xml': ['rs'], 117 | 'application/rpki-ghostbusters': ['gbr'], 118 | 'application/rpki-manifest': ['mft'], 119 | 'application/rpki-roa': ['roa'], 120 | 'application/rsd+xml': ['rsd'], 121 | 'application/rss+xml': ['rss'], 122 | 'application/rtf': ['rtf'], 123 | 'application/sbml+xml': ['sbml'], 124 | 'application/scvp-cv-request': ['scq'], 125 | 'application/scvp-cv-response': ['scs'], 126 | 'application/scvp-vp-request': ['spq'], 127 | 'application/scvp-vp-response': ['spp'], 128 | 'application/sdp': ['sdp'], 129 | 'application/set-payment-initiation': ['setpay'], 130 | 'application/set-registration-initiation': ['setreg'], 131 | 'application/shf+xml': ['shf'], 132 | 'application/smil+xml': ['smi', 'smil'], 133 | 'application/sparql-query': ['rq'], 134 | 'application/sparql-results+xml': ['srx'], 135 | 'application/srgs': ['gram'], 136 | 'application/srgs+xml': ['grxml'], 137 | 'application/sru+xml': ['sru'], 138 | 'application/ssdl+xml': ['ssdl'], 139 | 'application/ssml+xml': ['ssml'], 140 | 'application/tei+xml': ['tei', 'teicorpus'], 141 | 'application/thraud+xml': ['tfi'], 142 | 'application/timestamped-data': ['tsd'], 143 | 'application/vnd.3gpp.pic-bw-large': ['plb'], 144 | 'application/vnd.3gpp.pic-bw-small': ['psb'], 145 | 'application/vnd.3gpp.pic-bw-var': ['pvb'], 146 | 'application/vnd.3gpp2.tcap': ['tcap'], 147 | 'application/vnd.3m.post-it-notes': ['pwn'], 148 | 'application/vnd.accpac.simply.aso': ['aso'], 149 | 'application/vnd.accpac.simply.imp': ['imp'], 150 | 'application/vnd.acucobol': ['acu'], 151 | 'application/vnd.acucorp': ['atc', 'acutc'], 152 | 'application/vnd.adobe.air-application-installer-package+zip': ['air'], 153 | 'application/vnd.adobe.formscentral.fcdt': ['fcdt'], 154 | 'application/vnd.adobe.fxp': ['fxp', 'fxpl'], 155 | 'application/vnd.adobe.xdp+xml': ['xdp'], 156 | 'application/vnd.adobe.xfdf': ['xfdf'], 157 | 'application/vnd.ahead.space': ['ahead'], 158 | 'application/vnd.airzip.filesecure.azf': ['azf'], 159 | 'application/vnd.airzip.filesecure.azs': ['azs'], 160 | 'application/vnd.amazon.ebook': ['azw'], 161 | 'application/vnd.americandynamics.acc': ['acc'], 162 | 'application/vnd.amiga.ami': ['ami'], 163 | 'application/vnd.android.package-archive': ['apk'], 164 | 'application/vnd.anser-web-certificate-issue-initiation': ['cii'], 165 | 'application/vnd.anser-web-funds-transfer-initiation': ['fti'], 166 | 'application/vnd.antix.game-component': ['atx'], 167 | 'application/vnd.apple.installer+xml': ['mpkg'], 168 | 'application/vnd.apple.mpegurl': ['m3u8'], 169 | 'application/vnd.apple.pkpass': ['pkpass'], 170 | 'application/vnd.aristanetworks.swi': ['swi'], 171 | 'application/vnd.astraea-software.iota': ['iota'], 172 | 'application/vnd.audiograph': ['aep'], 173 | 'application/vnd.blueice.multipass': ['mpm'], 174 | 'application/vnd.bmi': ['bmi'], 175 | 'application/vnd.businessobjects': ['rep'], 176 | 'application/vnd.chemdraw+xml': ['cdxml'], 177 | 'application/vnd.chipnuts.karaoke-mmd': ['mmd'], 178 | 'application/vnd.cinderella': ['cdy'], 179 | 'application/vnd.claymore': ['cla'], 180 | 'application/vnd.cloanto.rp9': ['rp9'], 181 | 'application/vnd.clonk.c4group': ['c4g', 'c4d', 'c4f', 'c4p', 'c4u'], 182 | 'application/vnd.cluetrust.cartomobile-config': ['c11amc'], 183 | 'application/vnd.cluetrust.cartomobile-config-pkg': ['c11amz'], 184 | 'application/vnd.commonspace': ['csp'], 185 | 'application/vnd.contact.cmsg': ['cdbcmsg'], 186 | 'application/vnd.cosmocaller': ['cmc'], 187 | 'application/vnd.crick.clicker': ['clkx'], 188 | 'application/vnd.crick.clicker.keyboard': ['clkk'], 189 | 'application/vnd.crick.clicker.palette': ['clkp'], 190 | 'application/vnd.crick.clicker.template': ['clkt'], 191 | 'application/vnd.crick.clicker.wordbank': ['clkw'], 192 | 'application/vnd.criticaltools.wbs+xml': ['wbs'], 193 | 'application/vnd.ctc-posml': ['pml'], 194 | 'application/vnd.cups-ppd': ['ppd'], 195 | 'application/vnd.curl.car': ['car'], 196 | 'application/vnd.curl.pcurl': ['pcurl'], 197 | 'application/vnd.dart': ['dart'], 198 | 'application/vnd.data-vision.rdz': ['rdz'], 199 | 'application/vnd.dece.data': ['uvf', 'uvvf', 'uvd', 'uvvd'], 200 | 'application/vnd.dece.ttml+xml': ['uvt', 'uvvt'], 201 | 'application/vnd.dece.unspecified': ['uvx', 'uvvx'], 202 | 'application/vnd.dece.zip': ['uvz', 'uvvz'], 203 | 'application/vnd.denovo.fcselayout-link': ['fe_launch'], 204 | 'application/vnd.dna': ['dna'], 205 | 'application/vnd.dolby.mlp': ['mlp'], 206 | 'application/vnd.dpgraph': ['dpg'], 207 | 'application/vnd.dreamfactory': ['dfac'], 208 | 'application/vnd.ds-keypoint': ['kpxx'], 209 | 'application/vnd.dvb.ait': ['ait'], 210 | 'application/vnd.dvb.service': ['svc'], 211 | 'application/vnd.dynageo': ['geo'], 212 | 'application/vnd.ecowin.chart': ['mag'], 213 | 'application/vnd.enliven': ['nml'], 214 | 'application/vnd.epson.esf': ['esf'], 215 | 'application/vnd.epson.msf': ['msf'], 216 | 'application/vnd.epson.quickanime': ['qam'], 217 | 'application/vnd.epson.salt': ['slt'], 218 | 'application/vnd.epson.ssf': ['ssf'], 219 | 'application/vnd.eszigno3+xml': ['es3', 'et3'], 220 | 'application/vnd.ezpix-album': ['ez2'], 221 | 'application/vnd.ezpix-package': ['ez3'], 222 | 'application/vnd.fdf': ['fdf'], 223 | 'application/vnd.fdsn.mseed': ['mseed'], 224 | 'application/vnd.fdsn.seed': ['seed', 'dataless'], 225 | 'application/vnd.flographit': ['gph'], 226 | 'application/vnd.fluxtime.clip': ['ftc'], 227 | 'application/vnd.framemaker': ['fm', 'frame', 'maker', 'book'], 228 | 'application/vnd.frogans.fnc': ['fnc'], 229 | 'application/vnd.frogans.ltf': ['ltf'], 230 | 'application/vnd.fsc.weblaunch': ['fsc'], 231 | 'application/vnd.fujitsu.oasys': ['oas'], 232 | 'application/vnd.fujitsu.oasys2': ['oa2'], 233 | 'application/vnd.fujitsu.oasys3': ['oa3'], 234 | 'application/vnd.fujitsu.oasysgp': ['fg5'], 235 | 'application/vnd.fujitsu.oasysprs': ['bh2'], 236 | 'application/vnd.fujixerox.ddd': ['ddd'], 237 | 'application/vnd.fujixerox.docuworks': ['xdw'], 238 | 'application/vnd.fujixerox.docuworks.binder': ['xbd'], 239 | 'application/vnd.fuzzysheet': ['fzs'], 240 | 'application/vnd.genomatix.tuxedo': ['txd'], 241 | 'application/vnd.geogebra.file': ['ggb'], 242 | 'application/vnd.geogebra.tool': ['ggt'], 243 | 'application/vnd.geometry-explorer': ['gex', 'gre'], 244 | 'application/vnd.geonext': ['gxt'], 245 | 'application/vnd.geoplan': ['g2w'], 246 | 'application/vnd.geospace': ['g3w'], 247 | 'application/vnd.gmx': ['gmx'], 248 | 'application/vnd.google-apps.document': ['gdoc'], 249 | 'application/vnd.google-apps.presentation': ['gslides'], 250 | 'application/vnd.google-apps.spreadsheet': ['gsheet'], 251 | 'application/vnd.google-earth.kml+xml': ['kml'], 252 | 'application/vnd.google-earth.kmz': ['kmz'], 253 | 'application/vnd.grafeq': ['gqf', 'gqs'], 254 | 'application/vnd.groove-account': ['gac'], 255 | 'application/vnd.groove-help': ['ghf'], 256 | 'application/vnd.groove-identity-message': ['gim'], 257 | 'application/vnd.groove-injector': ['grv'], 258 | 'application/vnd.groove-tool-message': ['gtm'], 259 | 'application/vnd.groove-tool-template': ['tpl'], 260 | 'application/vnd.groove-vcard': ['vcg'], 261 | 'application/vnd.hal+xml': ['hal'], 262 | 'application/vnd.handheld-entertainment+xml': ['zmm'], 263 | 'application/vnd.hbci': ['hbci'], 264 | 'application/vnd.hhe.lesson-player': ['les'], 265 | 'application/vnd.hp-hpgl': ['hpgl'], 266 | 'application/vnd.hp-hpid': ['hpid'], 267 | 'application/vnd.hp-hps': ['hps'], 268 | 'application/vnd.hp-jlyt': ['jlt'], 269 | 'application/vnd.hp-pcl': ['pcl'], 270 | 'application/vnd.hp-pclxl': ['pclxl'], 271 | 'application/vnd.hydrostatix.sof-data': ['sfd-hdstx'], 272 | 'application/vnd.ibm.minipay': ['mpy'], 273 | 'application/vnd.ibm.modcap': ['afp', 'listafp', 'list3820'], 274 | 'application/vnd.ibm.rights-management': ['irm'], 275 | 'application/vnd.ibm.secure-container': ['sc'], 276 | 'application/vnd.iccprofile': ['icc', 'icm'], 277 | 'application/vnd.igloader': ['igl'], 278 | 'application/vnd.immervision-ivp': ['ivp'], 279 | 'application/vnd.immervision-ivu': ['ivu'], 280 | 'application/vnd.insors.igm': ['igm'], 281 | 'application/vnd.intercon.formnet': ['xpw', 'xpx'], 282 | 'application/vnd.intergeo': ['i2g'], 283 | 'application/vnd.intu.qbo': ['qbo'], 284 | 'application/vnd.intu.qfx': ['qfx'], 285 | 'application/vnd.ipunplugged.rcprofile': ['rcprofile'], 286 | 'application/vnd.irepository.package+xml': ['irp'], 287 | 'application/vnd.is-xpr': ['xpr'], 288 | 'application/vnd.isac.fcs': ['fcs'], 289 | 'application/vnd.jam': ['jam'], 290 | 'application/vnd.jcp.javame.midlet-rms': ['rms'], 291 | 'application/vnd.jisp': ['jisp'], 292 | 'application/vnd.joost.joda-archive': ['joda'], 293 | 'application/vnd.kahootz': ['ktz', 'ktr'], 294 | 'application/vnd.kde.karbon': ['karbon'], 295 | 'application/vnd.kde.kchart': ['chrt'], 296 | 'application/vnd.kde.kformula': ['kfo'], 297 | 'application/vnd.kde.kivio': ['flw'], 298 | 'application/vnd.kde.kontour': ['kon'], 299 | 'application/vnd.kde.kpresenter': ['kpr', 'kpt'], 300 | 'application/vnd.kde.kspread': ['ksp'], 301 | 'application/vnd.kde.kword': ['kwd', 'kwt'], 302 | 'application/vnd.kenameaapp': ['htke'], 303 | 'application/vnd.kidspiration': ['kia'], 304 | 'application/vnd.kinar': ['kne', 'knp'], 305 | 'application/vnd.koan': ['skp', 'skd', 'skt', 'skm'], 306 | 'application/vnd.kodak-descriptor': ['sse'], 307 | 'application/vnd.las.las+xml': ['lasxml'], 308 | 'application/vnd.llamagraphics.life-balance.desktop': ['lbd'], 309 | 'application/vnd.llamagraphics.life-balance.exchange+xml': ['lbe'], 310 | 'application/vnd.lotus-1-2-3': ['123'], 311 | 'application/vnd.lotus-approach': ['apr'], 312 | 'application/vnd.lotus-freelance': ['pre'], 313 | 'application/vnd.lotus-notes': ['nsf'], 314 | 'application/vnd.lotus-organizer': ['org'], 315 | 'application/vnd.lotus-screencam': ['scm'], 316 | 'application/vnd.lotus-wordpro': ['lwp'], 317 | 'application/vnd.macports.portpkg': ['portpkg'], 318 | 'application/vnd.mcd': ['mcd'], 319 | 'application/vnd.medcalcdata': ['mc1'], 320 | 'application/vnd.mediastation.cdkey': ['cdkey'], 321 | 'application/vnd.mfer': ['mwf'], 322 | 'application/vnd.mfmp': ['mfm'], 323 | 'application/vnd.micrografx.flo': ['flo'], 324 | 'application/vnd.micrografx.igx': ['igx'], 325 | 'application/vnd.mif': ['mif'], 326 | 'application/vnd.mobius.daf': ['daf'], 327 | 'application/vnd.mobius.dis': ['dis'], 328 | 'application/vnd.mobius.mbk': ['mbk'], 329 | 'application/vnd.mobius.mqy': ['mqy'], 330 | 'application/vnd.mobius.msl': ['msl'], 331 | 'application/vnd.mobius.plc': ['plc'], 332 | 'application/vnd.mobius.txf': ['txf'], 333 | 'application/vnd.mophun.application': ['mpn'], 334 | 'application/vnd.mophun.certificate': ['mpc'], 335 | 'application/vnd.mozilla.xul+xml': ['xul'], 336 | 'application/vnd.ms-artgalry': ['cil'], 337 | 'application/vnd.ms-cab-compressed': ['cab'], 338 | 'application/vnd.ms-excel': ['xls', 'xlm', 'xla', 'xlc', 'xlt', 'xlw'], 339 | 'application/vnd.ms-excel.addin.macroenabled.12': ['xlam'], 340 | 'application/vnd.ms-excel.sheet.binary.macroenabled.12': ['xlsb'], 341 | 'application/vnd.ms-excel.sheet.macroenabled.12': ['xlsm'], 342 | 'application/vnd.ms-excel.template.macroenabled.12': ['xltm'], 343 | 'application/vnd.ms-fontobject': ['eot'], 344 | 'application/vnd.ms-htmlhelp': ['chm'], 345 | 'application/vnd.ms-ims': ['ims'], 346 | 'application/vnd.ms-lrm': ['lrm'], 347 | 'application/vnd.ms-officetheme': ['thmx'], 348 | 'application/vnd.ms-outlook': ['msg'], 349 | 'application/vnd.ms-pki.seccat': ['cat'], 350 | 'application/vnd.ms-pki.stl': ['stl'], 351 | 'application/vnd.ms-powerpoint': ['ppt', 'pps', 'pot'], 352 | 'application/vnd.ms-powerpoint.addin.macroenabled.12': ['ppam'], 353 | 'application/vnd.ms-powerpoint.presentation.macroenabled.12': ['pptm'], 354 | 'application/vnd.ms-powerpoint.slide.macroenabled.12': ['sldm'], 355 | 'application/vnd.ms-powerpoint.slideshow.macroenabled.12': ['ppsm'], 356 | 'application/vnd.ms-powerpoint.template.macroenabled.12': ['potm'], 357 | 'application/vnd.ms-project': ['mpp', 'mpt'], 358 | 'application/vnd.ms-word.document.macroenabled.12': ['docm'], 359 | 'application/vnd.ms-word.template.macroenabled.12': ['dotm'], 360 | 'application/vnd.ms-works': ['wps', 'wks', 'wcm', 'wdb'], 361 | 'application/vnd.ms-wpl': ['wpl'], 362 | 'application/vnd.ms-xpsdocument': ['xps'], 363 | 'application/vnd.mseq': ['mseq'], 364 | 'application/vnd.musician': ['mus'], 365 | 'application/vnd.muvee.style': ['msty'], 366 | 'application/vnd.mynfc': ['taglet'], 367 | 'application/vnd.neurolanguage.nlu': ['nlu'], 368 | 'application/vnd.nitf': ['ntf', 'nitf'], 369 | 'application/vnd.noblenet-directory': ['nnd'], 370 | 'application/vnd.noblenet-sealer': ['nns'], 371 | 'application/vnd.noblenet-web': ['nnw'], 372 | 'application/vnd.nokia.n-gage.data': ['ngdat'], 373 | 'application/vnd.nokia.n-gage.symbian.install': ['n-gage'], 374 | 'application/vnd.nokia.radio-preset': ['rpst'], 375 | 'application/vnd.nokia.radio-presets': ['rpss'], 376 | 'application/vnd.novadigm.edm': ['edm'], 377 | 'application/vnd.novadigm.edx': ['edx'], 378 | 'application/vnd.novadigm.ext': ['ext'], 379 | 'application/vnd.oasis.opendocument.chart': ['odc'], 380 | 'application/vnd.oasis.opendocument.chart-template': ['otc'], 381 | 'application/vnd.oasis.opendocument.database': ['odb'], 382 | 'application/vnd.oasis.opendocument.formula': ['odf'], 383 | 'application/vnd.oasis.opendocument.formula-template': ['odft'], 384 | 'application/vnd.oasis.opendocument.graphics': ['odg'], 385 | 'application/vnd.oasis.opendocument.graphics-template': ['otg'], 386 | 'application/vnd.oasis.opendocument.image': ['odi'], 387 | 'application/vnd.oasis.opendocument.image-template': ['oti'], 388 | 'application/vnd.oasis.opendocument.presentation': ['odp'], 389 | 'application/vnd.oasis.opendocument.presentation-template': ['otp'], 390 | 'application/vnd.oasis.opendocument.spreadsheet': ['ods'], 391 | 'application/vnd.oasis.opendocument.spreadsheet-template': ['ots'], 392 | 'application/vnd.oasis.opendocument.text': ['odt'], 393 | 'application/vnd.oasis.opendocument.text-master': ['odm'], 394 | 'application/vnd.oasis.opendocument.text-template': ['ott'], 395 | 'application/vnd.oasis.opendocument.text-web': ['oth'], 396 | 'application/vnd.olpc-sugar': ['xo'], 397 | 'application/vnd.oma.dd2+xml': ['dd2'], 398 | 'application/vnd.openofficeorg.extension': ['oxt'], 399 | 'application/vnd.openxmlformats-officedocument.presentationml.presentation': [ 400 | 'pptx', 401 | ], 402 | 'application/vnd.openxmlformats-officedocument.presentationml.slide': [ 403 | 'sldx', 404 | ], 405 | 'application/vnd.openxmlformats-officedocument.presentationml.slideshow': [ 406 | 'ppsx', 407 | ], 408 | 'application/vnd.openxmlformats-officedocument.presentationml.template': [ 409 | 'potx', 410 | ], 411 | 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['xlsx'], 412 | 'application/vnd.openxmlformats-officedocument.spreadsheetml.template': [ 413 | 'xltx', 414 | ], 415 | 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': [ 416 | 'docx', 417 | ], 418 | 'application/vnd.openxmlformats-officedocument.wordprocessingml.template': [ 419 | 'dotx', 420 | ], 421 | 'application/vnd.osgeo.mapguide.package': ['mgp'], 422 | 'application/vnd.osgi.dp': ['dp'], 423 | 'application/vnd.osgi.subsystem': ['esa'], 424 | 'application/vnd.palm': ['pdb', 'pqa', 'oprc'], 425 | 'application/vnd.pawaafile': ['paw'], 426 | 'application/vnd.pg.format': ['str'], 427 | 'application/vnd.pg.osasli': ['ei6'], 428 | 'application/vnd.picsel': ['efif'], 429 | 'application/vnd.pmi.widget': ['wg'], 430 | 'application/vnd.pocketlearn': ['plf'], 431 | 'application/vnd.powerbuilder6': ['pbd'], 432 | 'application/vnd.previewsystems.box': ['box'], 433 | 'application/vnd.proteus.magazine': ['mgz'], 434 | 'application/vnd.publishare-delta-tree': ['qps'], 435 | 'application/vnd.pvi.ptid1': ['ptid'], 436 | 'application/vnd.quark.quarkxpress': [ 437 | 'qxd', 438 | 'qxt', 439 | 'qwd', 440 | 'qwt', 441 | 'qxl', 442 | 'qxb', 443 | ], 444 | 'application/vnd.realvnc.bed': ['bed'], 445 | 'application/vnd.recordare.musicxml': ['mxl'], 446 | 'application/vnd.recordare.musicxml+xml': ['musicxml'], 447 | 'application/vnd.rig.cryptonote': ['cryptonote'], 448 | 'application/vnd.rim.cod': ['cod'], 449 | 'application/vnd.rn-realmedia': ['rm'], 450 | 'application/vnd.rn-realmedia-vbr': ['rmvb'], 451 | 'application/vnd.route66.link66+xml': ['link66'], 452 | 'application/vnd.sailingtracker.track': ['st'], 453 | 'application/vnd.seemail': ['see'], 454 | 'application/vnd.sema': ['sema'], 455 | 'application/vnd.semd': ['semd'], 456 | 'application/vnd.semf': ['semf'], 457 | 'application/vnd.shana.informed.formdata': ['ifm'], 458 | 'application/vnd.shana.informed.formtemplate': ['itp'], 459 | 'application/vnd.shana.informed.interchange': ['iif'], 460 | 'application/vnd.shana.informed.package': ['ipk'], 461 | 'application/vnd.simtech-mindmapper': ['twd', 'twds'], 462 | 'application/vnd.smaf': ['mmf'], 463 | 'application/vnd.smart.teacher': ['teacher'], 464 | 'application/vnd.solent.sdkm+xml': ['sdkm', 'sdkd'], 465 | 'application/vnd.spotfire.dxp': ['dxp'], 466 | 'application/vnd.spotfire.sfs': ['sfs'], 467 | 'application/vnd.stardivision.calc': ['sdc'], 468 | 'application/vnd.stardivision.draw': ['sda'], 469 | 'application/vnd.stardivision.impress': ['sdd'], 470 | 'application/vnd.stardivision.math': ['smf'], 471 | 'application/vnd.stardivision.writer': ['sdw', 'vor'], 472 | 'application/vnd.stardivision.writer-global': ['sgl'], 473 | 'application/vnd.stepmania.package': ['smzip'], 474 | 'application/vnd.stepmania.stepchart': ['sm'], 475 | 'application/vnd.sun.wadl+xml': ['wadl'], 476 | 'application/vnd.sun.xml.calc': ['sxc'], 477 | 'application/vnd.sun.xml.calc.template': ['stc'], 478 | 'application/vnd.sun.xml.draw': ['sxd'], 479 | 'application/vnd.sun.xml.draw.template': ['std'], 480 | 'application/vnd.sun.xml.impress': ['sxi'], 481 | 'application/vnd.sun.xml.impress.template': ['sti'], 482 | 'application/vnd.sun.xml.math': ['sxm'], 483 | 'application/vnd.sun.xml.writer': ['sxw'], 484 | 'application/vnd.sun.xml.writer.global': ['sxg'], 485 | 'application/vnd.sun.xml.writer.template': ['stw'], 486 | 'application/vnd.sus-calendar': ['sus', 'susp'], 487 | 'application/vnd.svd': ['svd'], 488 | 'application/vnd.symbian.install': ['sis', 'sisx'], 489 | 'application/vnd.syncml+xml': ['xsm'], 490 | 'application/vnd.syncml.dm+wbxml': ['bdm'], 491 | 'application/vnd.syncml.dm+xml': ['xdm'], 492 | 'application/vnd.tao.intent-module-archive': ['tao'], 493 | 'application/vnd.tcpdump.pcap': ['pcap', 'cap', 'dmp'], 494 | 'application/vnd.tmobile-livetv': ['tmo'], 495 | 'application/vnd.trid.tpt': ['tpt'], 496 | 'application/vnd.triscape.mxs': ['mxs'], 497 | 'application/vnd.trueapp': ['tra'], 498 | 'application/vnd.ufdl': ['ufd', 'ufdl'], 499 | 'application/vnd.uiq.theme': ['utz'], 500 | 'application/vnd.umajin': ['umj'], 501 | 'application/vnd.unity': ['unityweb'], 502 | 'application/vnd.uoml+xml': ['uoml'], 503 | 'application/vnd.vcx': ['vcx'], 504 | 'application/vnd.visio': ['vsd', 'vst', 'vss', 'vsw'], 505 | 'application/vnd.visionary': ['vis'], 506 | 'application/vnd.vsf': ['vsf'], 507 | 'application/vnd.wap.wbxml': ['wbxml'], 508 | 'application/vnd.wap.wmlc': ['wmlc'], 509 | 'application/vnd.wap.wmlscriptc': ['wmlsc'], 510 | 'application/vnd.webturbo': ['wtb'], 511 | 'application/vnd.wolfram.player': ['nbp'], 512 | 'application/vnd.wordperfect': ['wpd'], 513 | 'application/vnd.wqd': ['wqd'], 514 | 'application/vnd.wt.stf': ['stf'], 515 | 'application/vnd.xara': ['xar'], 516 | 'application/vnd.xfdl': ['xfdl'], 517 | 'application/vnd.yamaha.hv-dic': ['hvd'], 518 | 'application/vnd.yamaha.hv-script': ['hvs'], 519 | 'application/vnd.yamaha.hv-voice': ['hvp'], 520 | 'application/vnd.yamaha.openscoreformat': ['osf'], 521 | 'application/vnd.yamaha.openscoreformat.osfpvg+xml': ['osfpvg'], 522 | 'application/vnd.yamaha.smaf-audio': ['saf'], 523 | 'application/vnd.yamaha.smaf-phrase': ['spf'], 524 | 'application/vnd.yellowriver-custom-menu': ['cmp'], 525 | 'application/vnd.zul': ['zir', 'zirz'], 526 | 'application/vnd.zzazz.deck+xml': ['zaz'], 527 | 'application/voicexml+xml': ['vxml'], 528 | 'application/wasm': ['wasm'], 529 | 'application/widget': ['wgt'], 530 | 'application/winhlp': ['hlp'], 531 | 'application/wsdl+xml': ['wsdl'], 532 | 'application/wspolicy+xml': ['wspolicy'], 533 | 'application/x-7z-compressed': ['7z'], 534 | 'application/x-abiword': ['abw'], 535 | 'application/x-ace-compressed': ['ace'], 536 | 'application/x-apple-diskimage': [], 537 | 'application/x-arj': ['arj'], 538 | 'application/x-authorware-bin': ['aab', 'x32', 'u32', 'vox'], 539 | 'application/x-authorware-map': ['aam'], 540 | 'application/x-authorware-seg': ['aas'], 541 | 'application/x-bcpio': ['bcpio'], 542 | 'application/x-bdoc': [], 543 | 'application/x-bittorrent': ['torrent'], 544 | 'application/x-blorb': ['blb', 'blorb'], 545 | 'application/x-bzip': ['bz'], 546 | 'application/x-bzip2': ['bz2', 'boz'], 547 | 'application/x-cbr': ['cbr', 'cba', 'cbt', 'cbz', 'cb7'], 548 | 'application/x-cdlink': ['vcd'], 549 | 'application/x-cfs-compressed': ['cfs'], 550 | 'application/x-chat': ['chat'], 551 | 'application/x-chess-pgn': ['pgn'], 552 | 'application/x-chrome-extension': ['crx'], 553 | 'application/x-cocoa': ['cco'], 554 | 'application/x-conference': ['nsc'], 555 | 'application/x-cpio': ['cpio'], 556 | 'application/x-csh': ['csh'], 557 | 'application/x-debian-package': ['udeb'], 558 | 'application/x-dgc-compressed': ['dgc'], 559 | 'application/x-director': [ 560 | 'dir', 561 | 'dcr', 562 | 'dxr', 563 | 'cst', 564 | 'cct', 565 | 'cxt', 566 | 'w3d', 567 | 'fgd', 568 | 'swa', 569 | ], 570 | 'application/x-doom': ['wad'], 571 | 'application/x-dtbncx+xml': ['ncx'], 572 | 'application/x-dtbook+xml': ['dtb'], 573 | 'application/x-dtbresource+xml': ['res'], 574 | 'application/x-dvi': ['dvi'], 575 | 'application/x-envoy': ['evy'], 576 | 'application/x-eva': ['eva'], 577 | 'application/x-font-bdf': ['bdf'], 578 | 'application/x-font-ghostscript': ['gsf'], 579 | 'application/x-font-linux-psf': ['psf'], 580 | 'application/x-font-pcf': ['pcf'], 581 | 'application/x-font-snf': ['snf'], 582 | 'application/x-font-type1': ['pfa', 'pfb', 'pfm', 'afm'], 583 | 'application/x-freearc': ['arc'], 584 | 'application/x-futuresplash': ['spl'], 585 | 'application/x-gca-compressed': ['gca'], 586 | 'application/x-glulx': ['ulx'], 587 | 'application/x-gnumeric': ['gnumeric'], 588 | 'application/x-gramps-xml': ['gramps'], 589 | 'application/x-gtar': ['gtar'], 590 | 'application/x-hdf': ['hdf'], 591 | 'application/x-httpd-php': ['php'], 592 | 'application/x-install-instructions': ['install'], 593 | 'application/x-iso9660-image': [], 594 | 'application/x-java-archive-diff': ['jardiff'], 595 | 'application/x-java-jnlp-file': ['jnlp'], 596 | 'application/x-latex': ['latex'], 597 | 'application/x-lua-bytecode': ['luac'], 598 | 'application/x-lzh-compressed': ['lzh', 'lha'], 599 | 'application/x-makeself': ['run'], 600 | 'application/x-mie': ['mie'], 601 | 'application/x-mobipocket-ebook': ['prc', 'mobi'], 602 | 'application/x-ms-application': ['application'], 603 | 'application/x-ms-shortcut': ['lnk'], 604 | 'application/x-ms-wmd': ['wmd'], 605 | 'application/x-ms-wmz': ['wmz'], 606 | 'application/x-ms-xbap': ['xbap'], 607 | 'application/x-msaccess': ['mdb'], 608 | 'application/x-msbinder': ['obd'], 609 | 'application/x-mscardfile': ['crd'], 610 | 'application/x-msclip': ['clp'], 611 | 'application/x-msdos-program': [], 612 | 'application/x-msdownload': ['com', 'bat'], 613 | 'application/x-msmediaview': ['mvb', 'm13', 'm14'], 614 | 'application/x-msmetafile': ['wmf', 'emf', 'emz'], 615 | 'application/x-msmoney': ['mny'], 616 | 'application/x-mspublisher': ['pub'], 617 | 'application/x-msschedule': ['scd'], 618 | 'application/x-msterminal': ['trm'], 619 | 'application/x-mswrite': ['wri'], 620 | 'application/x-netcdf': ['nc', 'cdf'], 621 | 'application/x-ns-proxy-autoconfig': ['pac'], 622 | 'application/x-nzb': ['nzb'], 623 | 'application/x-perl': ['pl', 'pm'], 624 | 'application/x-pilot': [], 625 | 'application/x-pkcs12': ['p12', 'pfx'], 626 | 'application/x-pkcs7-certificates': ['p7b', 'spc'], 627 | 'application/x-pkcs7-certreqresp': ['p7r'], 628 | 'application/x-rar-compressed': ['rar'], 629 | 'application/x-redhat-package-manager': ['rpm'], 630 | 'application/x-research-info-systems': ['ris'], 631 | 'application/x-sea': ['sea'], 632 | 'application/x-sh': ['sh'], 633 | 'application/x-shar': ['shar'], 634 | 'application/x-shockwave-flash': ['swf'], 635 | 'application/x-silverlight-app': ['xap'], 636 | 'application/x-sql': ['sql'], 637 | 'application/x-stuffit': ['sit'], 638 | 'application/x-stuffitx': ['sitx'], 639 | 'application/x-subrip': ['srt'], 640 | 'application/x-sv4cpio': ['sv4cpio'], 641 | 'application/x-sv4crc': ['sv4crc'], 642 | 'application/x-t3vm-image': ['t3'], 643 | 'application/x-tads': ['gam'], 644 | 'application/x-tar': ['tar'], 645 | 'application/x-tcl': ['tcl', 'tk'], 646 | 'application/x-tex': ['tex'], 647 | 'application/x-tex-tfm': ['tfm'], 648 | 'application/x-texinfo': ['texinfo', 'texi'], 649 | 'application/x-tgif': ['obj'], 650 | 'application/x-ustar': ['ustar'], 651 | 'application/x-virtualbox-hdd': ['hdd'], 652 | 'application/x-virtualbox-ova': ['ova'], 653 | 'application/x-virtualbox-ovf': ['ovf'], 654 | 'application/x-virtualbox-vbox': ['vbox'], 655 | 'application/x-virtualbox-vbox-extpack': ['vbox-extpack'], 656 | 'application/x-virtualbox-vdi': ['vdi'], 657 | 'application/x-virtualbox-vhd': ['vhd'], 658 | 'application/x-virtualbox-vmdk': ['vmdk'], 659 | 'application/x-wais-source': ['src'], 660 | 'application/x-web-app-manifest+json': ['webapp'], 661 | 'application/x-x509-ca-cert': ['der', 'crt', 'pem'], 662 | 'application/x-xfig': ['fig'], 663 | 'application/x-xliff+xml': ['xlf'], 664 | 'application/x-xpinstall': ['xpi'], 665 | 'application/x-xz': ['xz'], 666 | 'application/x-zmachine': ['z1', 'z2', 'z3', 'z4', 'z5', 'z6', 'z7', 'z8'], 667 | 'application/xaml+xml': ['xaml'], 668 | 'application/xcap-diff+xml': ['xdf'], 669 | 'application/xenc+xml': ['xenc'], 670 | 'application/xhtml+xml': ['xhtml', 'xht'], 671 | 'application/xml': ['xml', 'xsl', 'xsd', 'rng'], 672 | 'application/xml-dtd': ['dtd'], 673 | 'application/xop+xml': ['xop'], 674 | 'application/xproc+xml': ['xpl'], 675 | 'application/xslt+xml': ['xslt'], 676 | 'application/xspf+xml': ['xspf'], 677 | 'application/xv+xml': ['mxml', 'xhvml', 'xvml', 'xvm'], 678 | 'application/yang': ['yang'], 679 | 'application/yin+xml': ['yin'], 680 | 'application/zip': ['zip'], 681 | 'audio/3gpp': [], 682 | 'audio/adpcm': ['adp'], 683 | 'audio/basic': ['au', 'snd'], 684 | 'audio/midi': ['mid', 'midi', 'kar', 'rmi'], 685 | 'audio/mp3': [], 686 | 'audio/mp4': ['m4a', 'mp4a'], 687 | 'audio/mpeg': ['mpga', 'mp2', 'mp2a', 'mp3', 'm2a', 'm3a'], 688 | 'audio/ogg': ['oga', 'ogg', 'spx'], 689 | 'audio/s3m': ['s3m'], 690 | 'audio/silk': ['sil'], 691 | 'audio/vnd.dece.audio': ['uva', 'uvva'], 692 | 'audio/vnd.digital-winds': ['eol'], 693 | 'audio/vnd.dra': ['dra'], 694 | 'audio/vnd.dts': ['dts'], 695 | 'audio/vnd.dts.hd': ['dtshd'], 696 | 'audio/vnd.lucent.voice': ['lvp'], 697 | 'audio/vnd.ms-playready.media.pya': ['pya'], 698 | 'audio/vnd.nuera.ecelp4800': ['ecelp4800'], 699 | 'audio/vnd.nuera.ecelp7470': ['ecelp7470'], 700 | 'audio/vnd.nuera.ecelp9600': ['ecelp9600'], 701 | 'audio/vnd.rip': ['rip'], 702 | 'audio/wav': ['wav'], 703 | 'audio/wave': [], 704 | 'audio/webm': ['weba'], 705 | 'audio/x-aac': ['aac'], 706 | 'audio/x-aiff': ['aif', 'aiff', 'aifc'], 707 | 'audio/x-caf': ['caf'], 708 | 'audio/x-flac': ['flac'], 709 | 'audio/x-m4a': [], 710 | 'audio/x-matroska': ['mka'], 711 | 'audio/x-mpegurl': ['m3u'], 712 | 'audio/x-ms-wax': ['wax'], 713 | 'audio/x-ms-wma': ['wma'], 714 | 'audio/x-pn-realaudio': ['ram', 'ra'], 715 | 'audio/x-pn-realaudio-plugin': ['rmp'], 716 | 'audio/x-realaudio': [], 717 | 'audio/x-wav': [], 718 | 'audio/xm': ['xm'], 719 | 'chemical/x-cdx': ['cdx'], 720 | 'chemical/x-cif': ['cif'], 721 | 'chemical/x-cmdf': ['cmdf'], 722 | 'chemical/x-cml': ['cml'], 723 | 'chemical/x-csml': ['csml'], 724 | 'chemical/x-xyz': ['xyz'], 725 | 'font/collection': ['ttc'], 726 | 'font/otf': ['otf'], 727 | 'font/ttf': ['ttf'], 728 | 'font/woff': ['woff'], 729 | 'font/woff2': ['woff2'], 730 | 'image/apng': ['apng'], 731 | 'image/bmp': ['bmp'], 732 | 'image/cgm': ['cgm'], 733 | 'image/g3fax': ['g3'], 734 | 'image/gif': ['gif'], 735 | 'image/ief': ['ief'], 736 | 'image/jp2': ['jp2', 'jpg2'], 737 | 'image/jpeg': ['jpeg', 'jpg', 'jpe'], 738 | 'image/jpm': ['jpm'], 739 | 'image/jpx': ['jpx', 'jpf'], 740 | 'image/ktx': ['ktx'], 741 | 'image/png': ['png'], 742 | 'image/prs.btif': ['btif'], 743 | 'image/sgi': ['sgi'], 744 | 'image/svg+xml': ['svg', 'svgz'], 745 | 'image/tiff': ['tiff', 'tif'], 746 | 'image/vnd.adobe.photoshop': ['psd'], 747 | 'image/vnd.dece.graphic': ['uvi', 'uvvi', 'uvg', 'uvvg'], 748 | 'image/vnd.djvu': ['djvu', 'djv'], 749 | 'image/vnd.dvb.subtitle': [], 750 | 'image/vnd.dwg': ['dwg'], 751 | 'image/vnd.dxf': ['dxf'], 752 | 'image/vnd.fastbidsheet': ['fbs'], 753 | 'image/vnd.fpx': ['fpx'], 754 | 'image/vnd.fst': ['fst'], 755 | 'image/vnd.fujixerox.edmics-mmr': ['mmr'], 756 | 'image/vnd.fujixerox.edmics-rlc': ['rlc'], 757 | 'image/vnd.ms-modi': ['mdi'], 758 | 'image/vnd.ms-photo': ['wdp'], 759 | 'image/vnd.net-fpx': ['npx'], 760 | 'image/vnd.wap.wbmp': ['wbmp'], 761 | 'image/vnd.xiff': ['xif'], 762 | 'image/webp': ['webp'], 763 | 'image/x-3ds': ['3ds'], 764 | 'image/x-cmu-raster': ['ras'], 765 | 'image/x-cmx': ['cmx'], 766 | 'image/x-freehand': ['fh', 'fhc', 'fh4', 'fh5', 'fh7'], 767 | 'image/x-icon': ['ico'], 768 | 'image/x-jng': ['jng'], 769 | 'image/x-mrsid-image': ['sid'], 770 | 'image/x-ms-bmp': [], 771 | 'image/x-pcx': ['pcx'], 772 | 'image/x-pict': ['pic', 'pct'], 773 | 'image/x-portable-anymap': ['pnm'], 774 | 'image/x-portable-bitmap': ['pbm'], 775 | 'image/x-portable-graymap': ['pgm'], 776 | 'image/x-portable-pixmap': ['ppm'], 777 | 'image/x-rgb': ['rgb'], 778 | 'image/x-tga': ['tga'], 779 | 'image/x-xbitmap': ['xbm'], 780 | 'image/x-xpixmap': ['xpm'], 781 | 'image/x-xwindowdump': ['xwd'], 782 | 'message/rfc822': ['eml', 'mime'], 783 | 'model/gltf+json': ['gltf'], 784 | 'model/gltf-binary': ['glb'], 785 | 'model/iges': ['igs', 'iges'], 786 | 'model/mesh': ['msh', 'mesh', 'silo'], 787 | 'model/vnd.collada+xml': ['dae'], 788 | 'model/vnd.dwf': ['dwf'], 789 | 'model/vnd.gdl': ['gdl'], 790 | 'model/vnd.gtw': ['gtw'], 791 | 'model/vnd.mts': ['mts'], 792 | 'model/vnd.vtu': ['vtu'], 793 | 'model/vrml': ['wrl', 'vrml'], 794 | 'model/x3d+binary': ['x3db', 'x3dbz'], 795 | 'model/x3d+vrml': ['x3dv', 'x3dvz'], 796 | 'model/x3d+xml': ['x3d', 'x3dz'], 797 | 'text/cache-manifest': ['appcache', 'manifest'], 798 | 'text/calendar': ['ics', 'ifb'], 799 | 'text/coffeescript': ['coffee', 'litcoffee'], 800 | 'text/css': ['css'], 801 | 'text/csv': ['csv'], 802 | 'text/hjson': ['hjson'], 803 | 'text/html': ['html', 'htm', 'shtml'], 804 | 'text/jade': ['jade'], 805 | 'text/jsx': ['jsx'], 806 | 'text/less': ['less'], 807 | 'text/markdown': ['markdown', 'md'], 808 | 'text/mathml': ['mml'], 809 | 'text/n3': ['n3'], 810 | 'text/plain': ['txt', 'text', 'conf', 'def', 'list', 'log', 'in', 'ini'], 811 | 'text/prs.lines.tag': ['dsc'], 812 | 'text/richtext': ['rtx'], 813 | 'text/rtf': [], 814 | 'text/sgml': ['sgml', 'sgm'], 815 | 'text/slim': ['slim', 'slm'], 816 | 'text/stylus': ['stylus', 'styl'], 817 | 'text/tab-separated-values': ['tsv'], 818 | 'text/troff': ['t', 'tr', 'roff', 'man', 'me', 'ms'], 819 | 'text/turtle': ['ttl'], 820 | 'text/uri-list': ['uri', 'uris', 'urls'], 821 | 'text/vcard': ['vcard'], 822 | 'text/vnd.curl': ['curl'], 823 | 'text/vnd.curl.dcurl': ['dcurl'], 824 | 'text/vnd.curl.mcurl': ['mcurl'], 825 | 'text/vnd.curl.scurl': ['scurl'], 826 | 'text/vnd.dvb.subtitle': ['sub'], 827 | 'text/vnd.fly': ['fly'], 828 | 'text/vnd.fmi.flexstor': ['flx'], 829 | 'text/vnd.graphviz': ['gv'], 830 | 'text/vnd.in3d.3dml': ['3dml'], 831 | 'text/vnd.in3d.spot': ['spot'], 832 | 'text/vnd.sun.j2me.app-descriptor': ['jad'], 833 | 'text/vnd.wap.wml': ['wml'], 834 | 'text/vnd.wap.wmlscript': ['wmls'], 835 | 'text/vtt': ['vtt'], 836 | 'text/x-asm': ['s', 'asm'], 837 | 'text/x-c': ['c', 'cc', 'cxx', 'cpp', 'h', 'hh', 'dic'], 838 | 'text/x-component': ['htc'], 839 | 'text/x-fortran': ['f', 'for', 'f77', 'f90'], 840 | 'text/x-handlebars-template': ['hbs'], 841 | 'text/x-java-source': ['java'], 842 | 'text/x-lua': ['lua'], 843 | 'text/x-markdown': ['mkd'], 844 | 'text/x-nfo': ['nfo'], 845 | 'text/x-opml': ['opml'], 846 | 'text/x-org': [], 847 | 'text/x-pascal': ['p', 'pas'], 848 | 'text/x-processing': ['pde'], 849 | 'text/x-sass': ['sass'], 850 | 'text/x-scss': ['scss'], 851 | 'text/x-setext': ['etx'], 852 | 'text/x-sfv': ['sfv'], 853 | 'text/x-suse-ymp': ['ymp'], 854 | 'text/x-uuencode': ['uu'], 855 | 'text/x-vcalendar': ['vcs'], 856 | 'text/x-vcard': ['vcf'], 857 | 'text/xml': [], 858 | 'text/yaml': ['yaml', 'yml'], 859 | 'video/3gpp': ['3gp', '3gpp'], 860 | 'video/3gpp2': ['3g2'], 861 | 'video/h261': ['h261'], 862 | 'video/h263': ['h263'], 863 | 'video/h264': ['h264'], 864 | 'video/jpeg': ['jpgv'], 865 | 'video/jpm': ['jpgm'], 866 | 'video/mj2': ['mj2', 'mjp2'], 867 | 'video/mp2t': ['ts'], 868 | 'video/mp4': ['mp4', 'mp4v', 'mpg4'], 869 | 'video/mpeg': ['mpeg', 'mpg', 'mpe', 'm1v', 'm2v'], 870 | 'video/ogg': ['ogv'], 871 | 'video/quicktime': ['qt', 'mov'], 872 | 'video/vnd.dece.hd': ['uvh', 'uvvh'], 873 | 'video/vnd.dece.mobile': ['uvm', 'uvvm'], 874 | 'video/vnd.dece.pd': ['uvp', 'uvvp'], 875 | 'video/vnd.dece.sd': ['uvs', 'uvvs'], 876 | 'video/vnd.dece.video': ['uvv', 'uvvv'], 877 | 'video/vnd.dvb.file': ['dvb'], 878 | 'video/vnd.fvt': ['fvt'], 879 | 'video/vnd.mpegurl': ['mxu', 'm4u'], 880 | 'video/vnd.ms-playready.media.pyv': ['pyv'], 881 | 'video/vnd.uvvu.mp4': ['uvu', 'uvvu'], 882 | 'video/vnd.vivo': ['viv'], 883 | 'video/webm': ['webm'], 884 | 'video/x-f4v': ['f4v'], 885 | 'video/x-fli': ['fli'], 886 | 'video/x-flv': ['flv'], 887 | 'video/x-m4v': ['m4v'], 888 | 'video/x-matroska': ['mkv', 'mk3d', 'mks'], 889 | 'video/x-mng': ['mng'], 890 | 'video/x-ms-asf': ['asf', 'asx'], 891 | 'video/x-ms-vob': ['vob'], 892 | 'video/x-ms-wm': ['wm'], 893 | 'video/x-ms-wmv': ['wmv'], 894 | 'video/x-ms-wmx': ['wmx'], 895 | 'video/x-ms-wvx': ['wvx'], 896 | 'video/x-msvideo': ['avi'], 897 | 'video/x-sgi-movie': ['movie'], 898 | 'video/x-smv': ['smv'], 899 | 'x-conference/x-cooltalk': ['ice'], 900 | }; 901 | 902 | const mimes = Object.entries(types).reduce( 903 | (all, [type, exts]) => 904 | Object.assign(all, ...exts.map((ext) => ({ [ext]: type }))), 905 | {} 906 | ); 907 | 908 | module.exports = (ext) => mimes[ext] || 'application/octet-stream'; 909 | --------------------------------------------------------------------------------