├── .eslintignore ├── .eslintrc.yml ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── HISTORY.md ├── LICENSE ├── README.md ├── index.js ├── package.json └── test ├── .eslintrc.yml └── test.js /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | root: true 2 | extends: 3 | - standard 4 | - plugin:markdown/recommended 5 | plugins: 6 | - markdown 7 | overrides: 8 | - files: '**/*.md' 9 | processor: 'markdown/markdown' 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | - pull_request 5 | - push 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-20.04 10 | strategy: 11 | matrix: 12 | name: 13 | - Node.js 0.8 14 | - Node.js 0.10 15 | - Node.js 0.12 16 | - io.js 1.x 17 | - io.js 2.x 18 | - io.js 3.x 19 | - Node.js 4.x 20 | - Node.js 5.x 21 | - Node.js 6.x 22 | - Node.js 7.x 23 | - Node.js 8.x 24 | - Node.js 9.x 25 | - Node.js 10.x 26 | - Node.js 11.x 27 | - Node.js 12.x 28 | - Node.js 13.x 29 | - Node.js 14.x 30 | - Node.js 15.x 31 | - Node.js 16.x 32 | - Node.js 17.x 33 | - Node.js 18.x 34 | 35 | include: 36 | - name: Node.js 0.8 37 | node-version: "0.8" 38 | npm-i: mocha@2.5.3 supertest@1.1.0 39 | npm-rm: nyc 40 | 41 | - name: Node.js 0.10 42 | node-version: "0.10" 43 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 44 | 45 | - name: Node.js 0.12 46 | node-version: "0.12" 47 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 48 | 49 | - name: io.js 1.x 50 | node-version: "1.8" 51 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 52 | 53 | - name: io.js 2.x 54 | node-version: "2.5" 55 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 56 | 57 | - name: io.js 3.x 58 | node-version: "3.3" 59 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 60 | 61 | - name: Node.js 4.x 62 | node-version: "4.9" 63 | npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 64 | 65 | - name: Node.js 5.x 66 | node-version: "5.12" 67 | npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 68 | 69 | - name: Node.js 6.x 70 | node-version: "6.16" 71 | npm-i: mocha@6.2.3 nyc@14.1.1 supertest@6.1.6 72 | 73 | - name: Node.js 7.x 74 | node-version: "7.10" 75 | npm-i: mocha@6.2.3 nyc@14.1.1 supertest@6.1.6 76 | 77 | - name: Node.js 8.x 78 | node-version: "8.16" 79 | npm-i: mocha@7.2.0 80 | 81 | - name: Node.js 9.x 82 | node-version: "9.11" 83 | npm-i: mocha@7.2.0 84 | 85 | - name: Node.js 10.x 86 | node-version: "10.24" 87 | npm-i: mocha@8.4.0 88 | 89 | - name: Node.js 11.x 90 | node-version: "11.15" 91 | npm-i: mocha@8.4.0 92 | 93 | - name: Node.js 12.x 94 | node-version: "12.22" 95 | 96 | - name: Node.js 13.x 97 | node-version: "13.14" 98 | 99 | - name: Node.js 13.x 100 | node-version: "14.21" 101 | 102 | - name: Node.js 15.x 103 | node-version: "15.14" 104 | 105 | - name: Node.js 16.x 106 | node-version: "16.19" 107 | 108 | - name: Node.js 17.x 109 | node-version: "17.9" 110 | 111 | - name: Node.js 18.x 112 | node-version: "18.13" 113 | 114 | steps: 115 | - uses: actions/checkout@v3 116 | 117 | - name: Install Node.js ${{ matrix.node-version }} 118 | shell: bash -eo pipefail -l {0} 119 | run: | 120 | nvm install --default ${{ matrix.node-version }} 121 | if [[ "${{ matrix.node-version }}" == 0.* && "$(cut -d. -f2 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then 122 | nvm install --alias=npm 0.10 123 | nvm use ${{ matrix.node-version }} 124 | sed -i '1s;^.*$;'"$(printf '#!%q' "$(nvm which npm)")"';' "$(readlink -f "$(which npm)")" 125 | npm config set strict-ssl false 126 | fi 127 | dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" 128 | 129 | - name: Configure npm 130 | run: npm config set shrinkwrap false 131 | 132 | - name: Remove npm module(s) ${{ matrix.npm-rm }} 133 | run: npm rm --silent --save-dev ${{ matrix.npm-rm }} 134 | if: matrix.npm-rm != '' 135 | 136 | - name: Install npm module(s) ${{ matrix.npm-i }} 137 | run: npm install --save-dev ${{ matrix.npm-i }} 138 | if: matrix.npm-i != '' 139 | 140 | - name: Setup Node.js version-specific dependencies 141 | shell: bash 142 | run: | 143 | # eslint for linting 144 | # - remove on Node.js < 12 145 | if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 12 ]]; then 146 | node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \ 147 | grep -E '^eslint(-|$)' | \ 148 | sort -r | \ 149 | xargs -n1 npm rm --silent --save-dev 150 | fi 151 | 152 | - name: Install Node.js dependencies 153 | run: npm install 154 | 155 | - name: List environment 156 | id: list_env 157 | shell: bash 158 | run: | 159 | echo "node@$(node -v)" 160 | echo "npm@$(npm -v)" 161 | npm -s ls ||: 162 | (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print $2 "=" $3 }' >> "$GITHUB_OUTPUT" 163 | 164 | - name: Run tests 165 | shell: bash 166 | run: | 167 | if npm -ps ls nyc | grep -q nyc; then 168 | npm run test-ci 169 | cp coverage/lcov.info "coverage/${{ matrix.name }}.lcov" 170 | else 171 | npm test 172 | fi 173 | 174 | - name: Lint code 175 | if: steps.list_env.outputs.eslint != '' 176 | run: npm run lint 177 | 178 | - name: Collect code coverage 179 | if: steps.list_env.outputs.nyc != '' 180 | run: | 181 | if [[ -d ./coverage ]]; then 182 | mv ./coverage "./${{ matrix.name }}" 183 | mkdir ./coverage 184 | mv "./${{ matrix.name }}" "./coverage/${{ matrix.name }}" 185 | fi 186 | 187 | - name: Upload code coverage 188 | uses: actions/upload-artifact@v3 189 | if: steps.list_env.outputs.nyc != '' 190 | with: 191 | name: coverage 192 | path: ./coverage 193 | retention-days: 1 194 | 195 | coverage: 196 | needs: test 197 | runs-on: ubuntu-latest 198 | steps: 199 | - uses: actions/checkout@v3 200 | 201 | - name: Install lcov 202 | shell: bash 203 | run: sudo apt-get -y install lcov 204 | 205 | - name: Collect coverage reports 206 | uses: actions/download-artifact@v3 207 | with: 208 | name: coverage 209 | path: ./coverage 210 | 211 | - name: Merge coverage reports 212 | shell: bash 213 | run: find ./coverage -name lcov.info -exec printf '-a %q\n' {} \; | xargs lcov -o ./coverage/lcov.info 214 | 215 | - name: Upload coverage report 216 | uses: coverallsapp/github-action@master 217 | with: 218 | github-token: ${{ secrets.GITHUB_TOKEN }} 219 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | coverage/ 3 | node_modules/ 4 | npm-debug.log 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | 3.0.2 / 2015-10-12 2 | ================== 3 | 4 | * perf: hoist regular expressions 5 | * perf: use single regular expression for anchor checking 6 | 7 | 3.0.1 / 2015-07-19 8 | ================== 9 | 10 | * perf: enable strict mode 11 | 12 | 3.0.0 / 2014-08-29 13 | ================== 14 | 15 | * Remove support for sub-http servers; use the `handle` function 16 | 17 | 2.0.0 / 2014-06-08 18 | ================== 19 | 20 | * Accept `RegExp` object for `hostname` 21 | * Provide `req.vhost` object 22 | * Remove old invocation of `server.onvhost` 23 | * String `hostname` with `*` behaves more like SSL certificates 24 | - Matches 1 or more characters instead of zero 25 | - No longer matches "." characters 26 | * Support IPv6 literal in `Host` header 27 | 28 | 1.0.0 / 2014-03-05 29 | ================== 30 | 31 | * Genesis from `connect` 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2014 Jonathan Ong 4 | Copyright (c) 2014-2015 Douglas Christopher Wilson 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | 'Software'), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vhost 2 | 3 | [![NPM Version][npm-image]][npm-url] 4 | [![NPM Downloads][downloads-image]][downloads-url] 5 | [![Build Status][github-actions-ci-image]][github-actions-ci-url] 6 | [![Test Coverage][coveralls-image]][coveralls-url] 7 | 8 | ## Install 9 | 10 | ```sh 11 | $ npm install vhost 12 | ``` 13 | 14 | ## API 15 | 16 | ```js 17 | var vhost = require('vhost') 18 | ``` 19 | 20 | ### vhost(hostname, handle) 21 | 22 | Create a new middleware function to hand off request to `handle` when the incoming 23 | host for the request matches `hostname`. The function is called as 24 | `handle(req, res, next)`, like a standard middleware. 25 | 26 | `hostname` can be a string or a RegExp object. When `hostname` is a string it can 27 | contain `*` to match 1 or more characters in that section of the hostname. When 28 | `hostname` is a RegExp, it will be forced to case-insensitive (since hostnames are) 29 | and will be forced to match based on the start and end of the hostname. 30 | 31 | When host is matched and the request is sent down to a vhost handler, the `req.vhost` 32 | property will be populated with an object. This object will have numeric properties 33 | corresponding to each wildcard (or capture group if RegExp object provided) and the 34 | `hostname` that was matched. 35 | 36 | ```js 37 | var connect = require('connect') 38 | var vhost = require('vhost') 39 | var app = connect() 40 | 41 | app.use(vhost('*.*.example.com', function handle (req, res, next) { 42 | // for match of "foo.bar.example.com:8080" against "*.*.example.com": 43 | console.dir(req.vhost.host) // => 'foo.bar.example.com:8080' 44 | console.dir(req.vhost.hostname) // => 'foo.bar.example.com' 45 | console.dir(req.vhost.length) // => 2 46 | console.dir(req.vhost[0]) // => 'foo' 47 | console.dir(req.vhost[1]) // => 'bar' 48 | })) 49 | ``` 50 | 51 | ## Examples 52 | 53 | ### using with connect for static serving 54 | 55 | ```js 56 | var connect = require('connect') 57 | var serveStatic = require('serve-static') 58 | var vhost = require('vhost') 59 | 60 | var mailapp = connect() 61 | 62 | // add middlewares to mailapp for mail.example.com 63 | 64 | // create app to serve static files on subdomain 65 | var staticapp = connect() 66 | staticapp.use(serveStatic('public')) 67 | 68 | // create main app 69 | var app = connect() 70 | 71 | // add vhost routing to main app for mail 72 | app.use(vhost('mail.example.com', mailapp)) 73 | 74 | // route static assets for "assets-*" subdomain to get 75 | // around max host connections limit on browsers 76 | app.use(vhost('assets-*.example.com', staticapp)) 77 | 78 | // add middlewares and main usage to app 79 | 80 | app.listen(3000) 81 | ``` 82 | 83 | ### using with connect for user subdomains 84 | 85 | ```js 86 | var connect = require('connect') 87 | var serveStatic = require('serve-static') 88 | var vhost = require('vhost') 89 | 90 | var mainapp = connect() 91 | 92 | // add middlewares to mainapp for the main web site 93 | 94 | // create app that will server user content from public/{username}/ 95 | var userapp = connect() 96 | 97 | userapp.use(function (req, res, next) { 98 | var username = req.vhost[0] // username is the "*" 99 | 100 | // pretend request was for /{username}/* for file serving 101 | req.originalUrl = req.url 102 | req.url = '/' + username + req.url 103 | 104 | next() 105 | }) 106 | userapp.use(serveStatic('public')) 107 | 108 | // create main app 109 | var app = connect() 110 | 111 | // add vhost routing for main app 112 | app.use(vhost('userpages.local', mainapp)) 113 | app.use(vhost('www.userpages.local', mainapp)) 114 | 115 | // listen on all subdomains for user pages 116 | app.use(vhost('*.userpages.local', userapp)) 117 | 118 | app.listen(3000) 119 | ``` 120 | 121 | ### using with any generic request handler 122 | 123 | ```js 124 | var connect = require('connect') 125 | var http = require('http') 126 | var vhost = require('vhost') 127 | 128 | // create main app 129 | var app = connect() 130 | 131 | app.use(vhost('mail.example.com', function (req, res) { 132 | // handle req + res belonging to mail.example.com 133 | res.setHeader('Content-Type', 'text/plain') 134 | res.end('hello from mail!') 135 | })) 136 | 137 | // an external api server in any framework 138 | var httpServer = http.createServer(function (req, res) { 139 | res.setHeader('Content-Type', 'text/plain') 140 | res.end('hello from the api!') 141 | }) 142 | 143 | app.use(vhost('api.example.com', function (req, res) { 144 | // handle req + res belonging to api.example.com 145 | // pass the request to a standard Node.js HTTP server 146 | httpServer.emit('request', req, res) 147 | })) 148 | 149 | app.listen(3000) 150 | ``` 151 | 152 | ## License 153 | 154 | [MIT](LICENSE) 155 | 156 | [npm-image]: https://img.shields.io/npm/v/vhost.svg 157 | [npm-url]: https://npmjs.org/package/vhost 158 | [coveralls-image]: https://img.shields.io/coveralls/expressjs/vhost/master.svg 159 | [coveralls-url]: https://coveralls.io/r/expressjs/vhost 160 | [downloads-image]: https://img.shields.io/npm/dm/vhost.svg 161 | [downloads-url]: https://npmjs.org/package/vhost 162 | [github-actions-ci-image]: https://img.shields.io/github/actions/workflow/status/expressjs/vhost/ci.yml?branch=master&label=ci 163 | [github-actions-ci-url]: https://github.com/expressjs/vhost/actions/workflows/ci.yml 164 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * vhost 3 | * Copyright(c) 2014 Jonathan Ong 4 | * Copyright(c) 2014-2015 Douglas Christopher Wilson 5 | * MIT Licensed 6 | */ 7 | 8 | 'use strict' 9 | 10 | /** 11 | * Module exports. 12 | * @public 13 | */ 14 | 15 | module.exports = vhost 16 | 17 | /** 18 | * Module variables. 19 | * @private 20 | */ 21 | 22 | var ASTERISK_REGEXP = /\*/g 23 | var ASTERISK_REPLACE = '([^.]+)' 24 | var END_ANCHORED_REGEXP = /(?:^|[^\\])(?:\\\\)*\$$/ 25 | var ESCAPE_REGEXP = /([.+?^=!:${}()|[\]/\\])/g 26 | var ESCAPE_REPLACE = '\\$1' 27 | 28 | /** 29 | * Create a vhost middleware. 30 | * 31 | * @param {string|RegExp} hostname 32 | * @param {function} handle 33 | * @return {Function} 34 | * @public 35 | */ 36 | 37 | function vhost (hostname, handle) { 38 | if (!hostname) { 39 | throw new TypeError('argument hostname is required') 40 | } 41 | 42 | if (!handle) { 43 | throw new TypeError('argument handle is required') 44 | } 45 | 46 | if (typeof handle !== 'function') { 47 | throw new TypeError('argument handle must be a function') 48 | } 49 | 50 | // create regular expression for hostname 51 | var regexp = hostregexp(hostname) 52 | 53 | return function vhost (req, res, next) { 54 | var vhostdata = vhostof(req, regexp) 55 | 56 | if (!vhostdata) { 57 | return next() 58 | } 59 | 60 | // populate 61 | req.vhost = vhostdata 62 | 63 | // handle 64 | handle(req, res, next) 65 | } 66 | } 67 | 68 | /** 69 | * Get hostname of request. 70 | * 71 | * @param (object} req 72 | * @return {string} 73 | * @private 74 | */ 75 | 76 | function hostnameof (req) { 77 | var host = req.headers.host 78 | 79 | if (!host) { 80 | return 81 | } 82 | 83 | var offset = host[0] === '[' 84 | ? host.indexOf(']') + 1 85 | : 0 86 | var index = host.indexOf(':', offset) 87 | 88 | return index !== -1 89 | ? host.substring(0, index) 90 | : host 91 | } 92 | 93 | /** 94 | * Determine if object is RegExp. 95 | * 96 | * @param (object} val 97 | * @return {boolean} 98 | * @private 99 | */ 100 | 101 | function isregexp (val) { 102 | return Object.prototype.toString.call(val) === '[object RegExp]' 103 | } 104 | 105 | /** 106 | * Generate RegExp for given hostname value. 107 | * 108 | * @param (string|RegExp} val 109 | * @private 110 | */ 111 | 112 | function hostregexp (val) { 113 | var source = !isregexp(val) 114 | ? String(val).replace(ESCAPE_REGEXP, ESCAPE_REPLACE).replace(ASTERISK_REGEXP, ASTERISK_REPLACE) 115 | : val.source 116 | 117 | // force leading anchor matching 118 | if (source[0] !== '^') { 119 | source = '^' + source 120 | } 121 | 122 | // force trailing anchor matching 123 | if (!END_ANCHORED_REGEXP.test(source)) { 124 | source += '$' 125 | } 126 | 127 | return new RegExp(source, 'i') 128 | } 129 | 130 | /** 131 | * Get the vhost data of the request for RegExp 132 | * 133 | * @param (object} req 134 | * @param (RegExp} regexp 135 | * @return {object} 136 | * @private 137 | */ 138 | 139 | function vhostof (req, regexp) { 140 | var host = req.headers.host 141 | var hostname = hostnameof(req) 142 | 143 | if (!hostname) { 144 | return 145 | } 146 | 147 | var match = regexp.exec(hostname) 148 | 149 | if (!match) { 150 | return 151 | } 152 | 153 | var obj = Object.create(null) 154 | 155 | obj.host = host 156 | obj.hostname = hostname 157 | obj.length = match.length - 1 158 | 159 | for (var i = 1; i < match.length; i++) { 160 | obj[i - 1] = match[i] 161 | } 162 | 163 | return obj 164 | } 165 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vhost", 3 | "description": "virtual domain hosting", 4 | "version": "3.0.2", 5 | "contributors": [ 6 | "Douglas Christopher Wilson ", 7 | "Jonathan Ong (http://jongleberry.com)" 8 | ], 9 | "license": "MIT", 10 | "repository": "expressjs/vhost", 11 | "devDependencies": { 12 | "eslint": "8.32.0", 13 | "eslint-config-standard": "14.1.1", 14 | "eslint-plugin-import": "2.27.5", 15 | "eslint-plugin-markdown": "3.0.0", 16 | "eslint-plugin-node": "11.1.0", 17 | "eslint-plugin-promise": "6.1.1", 18 | "eslint-plugin-standard": "4.1.0", 19 | "mocha": "9.2.2", 20 | "nyc": "15.1.0", 21 | "supertest": "6.3.3" 22 | }, 23 | "files": [ 24 | "LICENSE", 25 | "HISTORY.md", 26 | "index.js" 27 | ], 28 | "engines": { 29 | "node": ">= 0.8.0" 30 | }, 31 | "scripts": { 32 | "lint": "eslint .", 33 | "test": "mocha --reporter spec --bail --check-leaks test/", 34 | "test-ci": "nyc --reporter=lcovonly --reporter=text npm test", 35 | "test-cov": "nyc --reporter=html --reporter=text npm test" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | mocha: true 3 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 2 | var assert = require('assert') 3 | var http = require('http') 4 | var request = require('supertest') 5 | var vhost = require('..') 6 | 7 | describe('vhost(hostname, server)', function () { 8 | it('should route by Host', function (done) { 9 | var vhosts = [] 10 | 11 | vhosts.push(vhost('tobi.com', tobi)) 12 | vhosts.push(vhost('loki.com', loki)) 13 | 14 | var app = createServer(vhosts) 15 | 16 | function tobi (req, res) { res.end('tobi') } 17 | function loki (req, res) { res.end('loki') } 18 | 19 | request(app) 20 | .get('/') 21 | .set('Host', 'tobi.com') 22 | .expect(200, 'tobi', done) 23 | }) 24 | 25 | it('should ignore port in Host', function (done) { 26 | var app = createServer('tobi.com', function (req, res) { 27 | res.end('tobi') 28 | }) 29 | 30 | request(app) 31 | .get('/') 32 | .set('Host', 'tobi.com:8080') 33 | .expect(200, 'tobi', done) 34 | }) 35 | 36 | it('should support IPv6 literal in Host', function (done) { 37 | var app = createServer('[::1]', function (req, res) { 38 | res.end('loopback') 39 | }) 40 | 41 | request(app) 42 | .get('/') 43 | .set('Host', '[::1]:8080') 44 | .expect(200, 'loopback', done) 45 | }) 46 | 47 | it('should 404 unless matched', function (done) { 48 | var vhosts = [] 49 | 50 | vhosts.push(vhost('tobi.com', tobi)) 51 | vhosts.push(vhost('loki.com', loki)) 52 | 53 | var app = createServer(vhosts) 54 | 55 | function tobi (req, res) { res.end('tobi') } 56 | function loki (req, res) { res.end('loki') } 57 | 58 | request(app) 59 | .get('/') 60 | .set('Host', 'ferrets.com') 61 | .expect(404, done) 62 | }) 63 | 64 | it('should 404 without Host header', function (done) { 65 | var vhosts = [] 66 | 67 | vhosts.push(vhost('tobi.com', tobi)) 68 | vhosts.push(vhost('loki.com', loki)) 69 | 70 | var server = createServer(vhosts) 71 | var listeners = server.listeners('request') 72 | 73 | server.removeAllListeners('request') 74 | listeners.unshift(function (req) { req.headers.host = undefined }) 75 | listeners.forEach(function (l) { server.addListener('request', l) }) 76 | 77 | function tobi (req, res) { res.end('tobi') } 78 | function loki (req, res) { res.end('loki') } 79 | 80 | request(server) 81 | .get('/') 82 | .expect(404, 'no vhost for "undefined"', done) 83 | }) 84 | 85 | describe('arguments', function () { 86 | describe('hostname', function () { 87 | it('should be required', function () { 88 | assert.throws(vhost.bind(), /hostname.*required/) 89 | }) 90 | 91 | it('should accept string', function () { 92 | assert.doesNotThrow(vhost.bind(null, 'loki.com', function () {})) 93 | }) 94 | 95 | it('should accept RegExp', function () { 96 | assert.doesNotThrow(vhost.bind(null, /loki\.com/, function () {})) 97 | }) 98 | }) 99 | 100 | describe('handle', function () { 101 | it('should be required', function () { 102 | assert.throws(vhost.bind(null, 'loki.com'), /handle.*required/) 103 | }) 104 | 105 | it('should accept function', function () { 106 | assert.doesNotThrow(vhost.bind(null, 'loki.com', function () {})) 107 | }) 108 | 109 | it('should reject plain object', function () { 110 | assert.throws(vhost.bind(null, 'loki.com', {}), /handle.*function/) 111 | }) 112 | }) 113 | }) 114 | 115 | describe('with string hostname', function () { 116 | it('should support wildcards', function (done) { 117 | var app = createServer('*.ferrets.com', function (req, res) { 118 | res.end('wildcard!') 119 | }) 120 | 121 | request(app) 122 | .get('/') 123 | .set('Host', 'loki.ferrets.com') 124 | .expect(200, 'wildcard!', done) 125 | }) 126 | 127 | it('should restrict wildcards to single part', function (done) { 128 | var app = createServer('*.ferrets.com', function (req, res) { 129 | res.end('wildcard!') 130 | }) 131 | 132 | request(app) 133 | .get('/') 134 | .set('Host', 'foo.loki.ferrets.com') 135 | .expect(404, done) 136 | }) 137 | 138 | it('should treat dot as a dot', function (done) { 139 | var app = createServer('a.b.com', function (req, res) { 140 | res.end('tobi') 141 | }) 142 | 143 | request(app) 144 | .get('/') 145 | .set('Host', 'aXb.com') 146 | .expect(404, done) 147 | }) 148 | 149 | it('should match entire string', function (done) { 150 | var app = createServer('.com', function (req, res) { 151 | res.end('commercial') 152 | }) 153 | 154 | request(app) 155 | .get('/') 156 | .set('Host', 'foo.com') 157 | .expect(404, done) 158 | }) 159 | 160 | it('should populate req.vhost', function (done) { 161 | var app = createServer('user-*.*.com', function (req, res) { 162 | var keys = Object.keys(req.vhost).sort() 163 | var arr = keys.map(function (k) { return [k, req.vhost[k]] }) 164 | res.end(JSON.stringify(arr)) 165 | }) 166 | 167 | request(app) 168 | .get('/') 169 | .set('Host', 'user-bob.foo.com:8080') 170 | .expect(200, '[["0","bob"],["1","foo"],["host","user-bob.foo.com:8080"],["hostname","user-bob.foo.com"],["length",2]]', done) 171 | }) 172 | }) 173 | 174 | describe('with RegExp hostname', function () { 175 | it('should match using RegExp', function (done) { 176 | var app = createServer(/[tl]o[bk]i\.com/, function (req, res) { 177 | res.end('tobi') 178 | }) 179 | 180 | request(app) 181 | .get('/') 182 | .set('Host', 'toki.com') 183 | .expect(200, 'tobi', done) 184 | }) 185 | 186 | it('should match entire hostname', function (done) { 187 | var vhosts = [] 188 | 189 | vhosts.push(vhost(/\.tobi$/, tobi)) 190 | vhosts.push(vhost(/^loki\./, loki)) 191 | 192 | var app = createServer(vhosts) 193 | 194 | function tobi (req, res) { res.end('tobi') } 195 | function loki (req, res) { res.end('loki') } 196 | 197 | request(app) 198 | .get('/') 199 | .set('Host', 'loki.tobi.com') 200 | .expect(404, done) 201 | }) 202 | 203 | it('should populate req.vhost', function (done) { 204 | var app = createServer(/user-(bob|joe)\.([^.]+)\.com/, function (req, res) { 205 | var keys = Object.keys(req.vhost).sort() 206 | var arr = keys.map(function (k) { return [k, req.vhost[k]] }) 207 | res.end(JSON.stringify(arr)) 208 | }) 209 | 210 | request(app) 211 | .get('/') 212 | .set('Host', 'user-bob.foo.com:8080') 213 | .expect(200, '[["0","bob"],["1","foo"],["host","user-bob.foo.com:8080"],["hostname","user-bob.foo.com"],["length",2]]', done) 214 | }) 215 | }) 216 | }) 217 | 218 | function createServer (hostname, server) { 219 | var vhosts = !Array.isArray(hostname) 220 | ? [vhost(hostname, server)] 221 | : hostname 222 | 223 | return http.createServer(function onRequest (req, res) { 224 | var index = 0 225 | 226 | function next (err) { 227 | var vhost = vhosts[index++] 228 | 229 | if (!vhost || err) { 230 | res.statusCode = err ? (err.status || 500) : 404 231 | res.end(err ? err.message : 'no vhost for "' + req.headers.host + '"') 232 | return 233 | } 234 | 235 | vhost(req, res, next) 236 | } 237 | 238 | next() 239 | }) 240 | } 241 | --------------------------------------------------------------------------------