├── .eslintignore ├── test ├── .eslintrc.yml └── test.js ├── .gitignore ├── .eslintrc.yml ├── .github ├── dependabot.yml └── workflows │ ├── codeql.yml │ ├── scorecards.yml │ └── ci.yml ├── HISTORY.md ├── LICENSE ├── package.json ├── index.js └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | -------------------------------------------------------------------------------- /test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | mocha: true 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | coverage/ 3 | node_modules/ 4 | npm-debug.log 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /.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/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: monthly 7 | 8 | - package-ecosystem: npm 9 | directory: / 10 | schedule: 11 | interval: monthly 12 | open-pull-requests-limit: 10 13 | ignore: 14 | - dependency-name: "*" 15 | update-types: ["version-update:semver-major"] 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | "funding": { 12 | "type": "opencollective", 13 | "url": "https://opencollective.com/express" 14 | }, 15 | "devDependencies": { 16 | "eslint": "8.32.0", 17 | "eslint-config-standard": "14.1.1", 18 | "eslint-plugin-import": "2.27.5", 19 | "eslint-plugin-markdown": "3.0.0", 20 | "eslint-plugin-node": "11.1.0", 21 | "eslint-plugin-promise": "6.1.1", 22 | "eslint-plugin-standard": "4.1.0", 23 | "mocha": "9.2.2", 24 | "nyc": "15.1.0", 25 | "supertest": "6.3.3" 26 | }, 27 | "files": [ 28 | "LICENSE", 29 | "HISTORY.md", 30 | "index.js" 31 | ], 32 | "engines": { 33 | "node": ">= 0.8.0" 34 | }, 35 | "scripts": { 36 | "lint": "eslint .", 37 | "test": "mocha --reporter spec --bail --check-leaks test/", 38 | "test-ci": "nyc --reporter=lcovonly --reporter=text npm test", 39 | "test-cov": "nyc --reporter=html --reporter=text npm test" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: ["master"] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: ["master"] 20 | schedule: 21 | - cron: "0 0 * * 1" 22 | 23 | permissions: 24 | contents: read 25 | 26 | jobs: 27 | analyze: 28 | name: Analyze 29 | runs-on: ubuntu-latest 30 | permissions: 31 | actions: read 32 | contents: read 33 | security-events: write 34 | 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | language: ["javascript"] 39 | # CodeQL supports [ $supported-codeql-languages ] 40 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 41 | 42 | steps: 43 | - name: Checkout repository 44 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 45 | 46 | # Initializes the CodeQL tools for scanning. 47 | - name: Initialize CodeQL 48 | uses: github/codeql-action/init@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.30.0 49 | with: 50 | languages: ${{ matrix.language }} 51 | # If you wish to specify custom queries, you can do so here or in a config file. 52 | # By default, queries listed here will override any specified in a config file. 53 | # Prefix the list here with "+" to use these queries and those in the config file. 54 | 55 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 56 | # If this step fails, then you should remove it and run the build manually (see below) 57 | - name: Autobuild 58 | uses: github/codeql-action/autobuild@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.30.0 59 | 60 | # ℹ️ Command-line programs to run using the OS shell. 61 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 62 | 63 | # If the Autobuild fails above, remove it and uncomment the following three lines. 64 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 65 | 66 | # - run: | 67 | # echo "Run, Build Application using script" 68 | # ./location_of_script_within_repo/buildscript.sh 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.30.0 72 | with: 73 | category: "/language:${{matrix.language}}" 74 | -------------------------------------------------------------------------------- /.github/workflows/scorecards.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '20 7 * * 2' 14 | push: 15 | branches: ["master"] 16 | 17 | # Declare default permissions as read only. 18 | permissions: read-all 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # Needed to upload the results to code-scanning dashboard. 26 | security-events: write 27 | # Needed to publish results and get a badge (see publish_results below). 28 | id-token: write 29 | contents: read 30 | actions: read 31 | # To allow GraphQL ListCommits to work 32 | issues: read 33 | pull-requests: read 34 | # To detect SAST tools 35 | checks: read 36 | 37 | steps: 38 | - name: "Checkout code" 39 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 40 | with: 41 | persist-credentials: false 42 | 43 | - name: "Run analysis" 44 | uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 45 | with: 46 | results_file: results.sarif 47 | results_format: sarif 48 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 49 | # - you want to enable the Branch-Protection check on a *public* repository, or 50 | # - you are installing Scorecards on a *private* repository 51 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 52 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 53 | 54 | # Public repositories: 55 | # - Publish results to OpenSSF REST API for easy access by consumers 56 | # - Allows the repository to include the Scorecard badge. 57 | # - See https://github.com/ossf/scorecard-action#publishing-results. 58 | # For private repositories: 59 | # - `publish_results` will always be set to `false`, regardless 60 | # of the value entered here. 61 | publish_results: true 62 | 63 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 64 | # format to the repository Actions tab. 65 | - name: "Upload artifact" 66 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 67 | with: 68 | name: SARIF file 69 | path: results.sarif 70 | retention-days: 5 71 | 72 | # Upload the results to GitHub's code scanning dashboard. 73 | - name: "Upload to code-scanning" 74 | uses: github/codeql-action/upload-sarif@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.30.0 75 | with: 76 | sarif_file: results.sarif 77 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - "*.md" 9 | pull_request: 10 | branches: 11 | - "**" 12 | paths-ignore: 13 | - "*.md" 14 | workflow_dispatch: 15 | 16 | # Cancel in progress workflows 17 | # in the scenario where we already had a run going for that PR/branch/tag but then triggered a new run 18 | concurrency: 19 | group: "${{ github.workflow }} ✨ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" 20 | cancel-in-progress: true 21 | 22 | permissions: 23 | contents: read 24 | 25 | jobs: 26 | lint: 27 | name: Lint 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 31 | with: 32 | persist-credentials: false 33 | - name: Setup Node.js 34 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 35 | with: 36 | node-version: "lts/*" 37 | 38 | - name: Install dependencies 39 | run: npm install --ignore-scripts --include=dev 40 | 41 | - name: Run lint 42 | run: node --run lint # Use `node --run` to run the script in package.json 43 | 44 | test: 45 | name: Test 46 | runs-on: ubuntu-latest 47 | strategy: 48 | fail-fast: false 49 | matrix: 50 | name: 51 | - Node.js 0.8 52 | - Node.js 0.10 53 | - Node.js 0.12 54 | - io.js 1.x 55 | - io.js 2.x 56 | - io.js 3.x 57 | - Node.js 4.x 58 | - Node.js 5.x 59 | - Node.js 6.x 60 | - Node.js 7.x 61 | - Node.js 8.x 62 | - Node.js 9.x 63 | - Node.js 10.x 64 | - Node.js 11.x 65 | - Node.js 12.x 66 | - Node.js 13.x 67 | - Node.js 14.x 68 | - Node.js 15.x 69 | - Node.js 16.x 70 | - Node.js 17.x 71 | - Node.js 18.x 72 | - Node.js 19.x 73 | - Node.js 20.x 74 | - Node.js 21.x 75 | - Node.js 22.x 76 | - Node.js 23.x 77 | - Node.js 24.x 78 | 79 | include: 80 | - name: Node.js 0.8 81 | node-version: "0.8" 82 | npm-i: mocha@2.5.3 supertest@1.1.0 83 | npm-rm: nyc 84 | 85 | - name: Node.js 0.10 86 | node-version: "0.10" 87 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 88 | 89 | - name: Node.js 0.12 90 | node-version: "0.12" 91 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 92 | 93 | - name: io.js 1.x 94 | node-version: "1.8" 95 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 96 | 97 | - name: io.js 2.x 98 | node-version: "2.5" 99 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 100 | 101 | - name: io.js 3.x 102 | node-version: "3.3" 103 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 104 | 105 | - name: Node.js 4.x 106 | node-version: "4" 107 | npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 108 | 109 | - name: Node.js 5.x 110 | node-version: "5" 111 | npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 112 | 113 | - name: Node.js 6.x 114 | node-version: "6" 115 | npm-i: mocha@6.2.3 nyc@14.1.1 supertest@6.1.6 116 | 117 | - name: Node.js 7.x 118 | node-version: "7" 119 | npm-i: mocha@6.2.3 nyc@14.1.1 supertest@6.1.6 120 | 121 | - name: Node.js 8.x 122 | node-version: "8" 123 | npm-i: mocha@7.2.0 nyc@14.1.1 supertest@6.1.6 124 | 125 | - name: Node.js 9.x 126 | node-version: "9" 127 | npm-i: mocha@7.2.0 nyc@14.1.1 supertest@6.1.6 128 | 129 | - name: Node.js 10.x 130 | node-version: "10" 131 | npm-i: mocha@8.4.0 supertest@6.1.6 132 | 133 | - name: Node.js 11.x 134 | node-version: "11" 135 | npm-i: mocha@8.4.0 supertest@6.1.6 136 | 137 | - name: Node.js 12.x 138 | node-version: "12" 139 | npm-i: "supertest@6.1.6" 140 | 141 | - name: Node.js 13.x 142 | node-version: "13" 143 | npm-i: "supertest@6.1.6" 144 | 145 | - name: Node.js 14.x 146 | node-version: "14" 147 | 148 | - name: Node.js 15.x 149 | node-version: "15" 150 | npm-i: "supertest@6.1.6" 151 | 152 | - name: Node.js 16.x 153 | node-version: "16" 154 | 155 | - name: Node.js 17.x 156 | node-version: "17" 157 | 158 | - name: Node.js 18.x 159 | node-version: "18" 160 | 161 | - name: Node.js 19.x 162 | node-version: "19" 163 | 164 | - name: Node.js 20.x 165 | node-version: "20" 166 | 167 | - name: Node.js 21.x 168 | node-version: "21" 169 | 170 | - name: Node.js 22.x 171 | node-version: "22" 172 | 173 | - name: Node.js 23.x 174 | node-version: "23" 175 | 176 | - name: Node.js 24.x 177 | node-version: "24" 178 | 179 | steps: 180 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 181 | with: 182 | persist-credentials: false 183 | 184 | - name: Install Node.js ${{ matrix.node-version }} 185 | shell: bash -eo pipefail -l {0} 186 | run: | 187 | nvm install --default ${{ matrix.node-version }} 188 | if [[ "${{ matrix.node-version }}" == 0.* && "$(cut -d. -f2 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then 189 | nvm install --alias=npm 0.10 190 | nvm use ${{ matrix.node-version }} 191 | sed -i '1s;^.*$;'"$(printf '#!%q' "$(nvm which npm)")"';' "$(readlink -f "$(which npm)")" 192 | npm config set strict-ssl false 193 | fi 194 | dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" 195 | 196 | - name: Configure npm 197 | run: | 198 | if [[ "$(npm config get package-lock)" == "true" ]]; then 199 | npm config set package-lock false 200 | else 201 | npm config set shrinkwrap false 202 | fi 203 | 204 | - name: Remove npm module(s) ${{ matrix.npm-rm }} 205 | run: npm rm --silent --save-dev ${{ matrix.npm-rm }} 206 | if: matrix.npm-rm != '' 207 | 208 | - name: Install npm module(s) ${{ matrix.npm-i }} 209 | run: npm install --save-dev ${{ matrix.npm-i }} 210 | if: matrix.npm-i != '' 211 | 212 | - name: Setup Node.js version-specific dependencies 213 | shell: bash 214 | run: | 215 | # eslint for linting 216 | # - remove on Node.js < 10 217 | if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then 218 | node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \ 219 | grep -E '^eslint(-|$)' | \ 220 | sort -r | \ 221 | xargs -n1 npm rm --silent --save-dev 222 | fi 223 | 224 | - name: Install Node.js dependencies 225 | run: npm install 226 | 227 | - name: List environment 228 | id: list_env 229 | shell: bash 230 | run: | 231 | echo "node@$(node -v)" 232 | echo "npm@$(npm -v)" 233 | npm -s ls ||: 234 | (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print "::set-output name=" $2 "::" $3 }' 235 | 236 | - name: Run tests 237 | shell: bash 238 | run: | 239 | if npm -ps ls nyc | grep -q nyc; then 240 | npm run test-ci 241 | cp coverage/lcov.info "coverage/${{ matrix.node-version }}.lcov" 242 | else 243 | npm test 244 | fi 245 | 246 | - name: Collect code coverage 247 | if: steps.list_env.outputs.nyc != '' 248 | run: | 249 | if [[ -d ./coverage ]]; then 250 | mv ./coverage "./${{ matrix.node-version }}" 251 | mkdir ./coverage 252 | mv "./${{ matrix.node-version }}" "./coverage/${{ matrix.node-version }}" 253 | fi 254 | 255 | - name: Upload code coverage 256 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 257 | if: steps.list_env.outputs.nyc != '' 258 | with: 259 | name: coverage-${{ matrix.node-version }} # to avoid conflicts 260 | path: "./coverage/${{ matrix.node-version }}" 261 | retention-days: 1 262 | 263 | coverage: 264 | name: Coverage 265 | permissions: 266 | checks: write # for coverallsapp/github-action to create new checks 267 | contents: read # for actions/checkout to fetch code 268 | needs: test 269 | runs-on: ubuntu-latest 270 | steps: 271 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 272 | with: 273 | persist-credentials: false 274 | 275 | - name: Install lcov 276 | shell: bash 277 | run: sudo apt-get -y install lcov 278 | 279 | - name: Collect coverage reports 280 | uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 281 | with: 282 | path: ./coverage 283 | 284 | - name: Merge coverage reports 285 | shell: bash 286 | run: find ./coverage -name lcov.info -exec printf '-a %q\n' {} \; | xargs lcov -o ./coverage/lcov.info 287 | 288 | - name: Upload coverage report 289 | uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # v2.3.6 290 | with: 291 | file: ./coverage/lcov.info 292 | github-token: ${{ secrets.GITHUB_TOKEN }} 293 | --------------------------------------------------------------------------------