├── .gitignore ├── .travis.yml ├── .jshintrc ├── Gruntfile.js ├── LICENCE-MIT ├── package.json ├── index.js ├── README.md └── test └── middleware_test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | tmp 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.1" 4 | before_script: 5 | - npm install -g grunt-cli 6 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "evil": false, 4 | "regexdash": true, 5 | 6 | "curly": true, 7 | "eqeqeq": true, 8 | "freeze": true, 9 | "immed": true, 10 | "indent": 4, 11 | "latedef": true, 12 | "newcap": true, 13 | "forin": true, 14 | "noarg": true, 15 | "noempty": true, 16 | "nonew": true, 17 | "quotmark": "single", 18 | "plusplus": false, 19 | "undef": true, 20 | "unused": true, 21 | "strict": false, 22 | "trailing": true, 23 | 24 | "maxdepth": 5, 25 | "maxlen": 140, 26 | 27 | "sub": false, 28 | "wsh": false, 29 | 30 | "camelcase": false 31 | } 32 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | * http-rewrite-middleware 3 | * https://github.com/viart/http-rewrite-middleware 4 | * 5 | * Copyright (c) 2013 Artem Vitiuk 6 | * Licensed under the MIT license. 7 | */ 8 | 'use strict'; 9 | 10 | module.exports = function (grunt) { 11 | 12 | // Project configuration. 13 | grunt.initConfig({ 14 | jshint: { 15 | all: [ 16 | 'Gruntfile.js', 17 | 'index.js', 18 | '<%= nodeunit.tests %>' 19 | ], 20 | options: { 21 | jshintrc: '.jshintrc', 22 | } 23 | }, 24 | 25 | nodeunit: { 26 | tests: ['test/*_test.js'] 27 | } 28 | }); 29 | 30 | // These plugins provide necessary tasks. 31 | grunt.loadNpmTasks('grunt-contrib-jshint'); 32 | grunt.loadNpmTasks('grunt-contrib-nodeunit'); 33 | 34 | grunt.registerTask('test', ['nodeunit']); 35 | grunt.registerTask('default', ['jshint', 'test']); 36 | }; 37 | -------------------------------------------------------------------------------- /LICENCE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Artem Vitiuk 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "http-rewrite-middleware", 3 | "description": "Nginx-inspired (RegExp-based) HTTP Rewrite Middleware for the Connect and Express webservers.", 4 | "version": "0.1.6", 5 | "homepage": "https://github.com/viart/http-rewrite-middleware", 6 | "author": { 7 | "name": "Artem Vitiuk", 8 | "email": "artem@devart.in.ua" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:viart/http-rewrite-middleware.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/viart/http-rewrite-middleware/issues" 16 | }, 17 | "licenses": [ 18 | { 19 | "type": "MIT", 20 | "url": "https://github.com/viart/http-rewrite-middleware/blob/master/LICENSE-MIT" 21 | } 22 | ], 23 | "main": "index.js", 24 | "engines": { 25 | "node": ">= 0.8.0" 26 | }, 27 | "scripts": { 28 | "test": "grunt test" 29 | }, 30 | "devDependencies": { 31 | "grunt-contrib-jshint": "~0.7.1", 32 | "grunt-contrib-nodeunit": "~0.1.2", 33 | "grunt": "~0.4.1" 34 | }, 35 | "keywords": [ 36 | "gruntplugin", 37 | "grunt-contrib-connect", 38 | "grunt-express", 39 | "modrewrite", 40 | "rewriterule", 41 | "rewrite", 42 | "route", 43 | "connect", 44 | "express" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * http-rewrite-middleware 3 | * https://github.com/viart/http-rewrite-middleware 4 | * 5 | * Copyright (c) 2014 Artem Vitiuk 6 | * Licensed under the MIT license. 7 | */ 8 | 'use strict'; 9 | 10 | function Rewriter (rules, options) { 11 | options = options || {}; 12 | 13 | var nop = function () {}; 14 | if (options.silent) { 15 | this.log = { 16 | ok: nop, 17 | error: nop, 18 | verbose: nop 19 | }; 20 | } else { 21 | this.log = { 22 | ok: console.log, 23 | error: console.error, 24 | verbose: options.verbose ? console.log : nop 25 | }; 26 | } 27 | 28 | this.rules = []; 29 | (rules || []).forEach(this.registerRule, this); 30 | } 31 | 32 | Rewriter.prototype = { 33 | registerRule: function (rule) { 34 | var type = 'rewrite'; 35 | 36 | rule = rule || {}; 37 | 38 | if (this.isRuleValid(rule)) { 39 | if (rule.redirect) { 40 | rule.redirect = rule.redirect === 'permanent' ? 301 : 302; 41 | type = 'redirect ' + rule.redirect; 42 | } 43 | 44 | this.rules.push({ 45 | from: new RegExp(rule.from), 46 | to: rule.to, 47 | redirect: rule.redirect 48 | }); 49 | 50 | this.log.ok('Rewrite rule created for: [' + type.toUpperCase() + ': ' + rule.from + ' -> ' + rule.to + '].'); 51 | return true; 52 | } else { 53 | this.log.error('Wrong rule given.'); 54 | return false; 55 | } 56 | }, 57 | 58 | isRuleValid: function (rule) { 59 | return rule.from && rule.to && typeof rule.from === 'string' && typeof rule.to === 'string'; 60 | }, 61 | 62 | resetRules: function () { 63 | this.rules = []; 64 | }, 65 | 66 | getRules: function () { 67 | return this.rules; 68 | }, 69 | 70 | dispatcher: function (req, res, next) { 71 | var logger = this.log.verbose; 72 | return function (rule) { 73 | var toUrl, 74 | fromUrl = req.url; 75 | if (rule.from.test(req.url)) { 76 | toUrl = req.url.replace(rule.from, rule.to); 77 | if (!rule.redirect) { 78 | req.url = toUrl; 79 | next(); 80 | } else { 81 | res.statusCode = rule.redirect; 82 | res.setHeader('Location', toUrl); 83 | res.end(); 84 | } 85 | logger( 86 | (rule.redirect ? 'redirect ' + rule.redirect : 'rewrite').toUpperCase() + ' > ' + 87 | fromUrl + ' -> ' + toUrl + ' | By [' + rule.from + ' : ' + rule.to + ']' 88 | ); 89 | return true; 90 | } 91 | }; 92 | }, 93 | 94 | getMiddleware: function () { 95 | return function (req, res, next) { 96 | if (!this.rules.length || !this.rules.some(this.dispatcher(req, res, next))) { 97 | next(); 98 | } 99 | }.bind(this); 100 | } 101 | }; 102 | 103 | module.exports.getMiddleware = function (rules, options) { 104 | return (new Rewriter(rules, options)).getMiddleware(); 105 | }; 106 | 107 | module.exports.Rewriter = Rewriter; 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # http-rewrite-middleware v0.1.5 [![Build Status](https://travis-ci.org/viart/http-rewrite-middleware.png?branch=master)](https://travis-ci.org/viart/http-rewrite-middleware) 2 | 3 | > This module makes it possible to redirect (rewrite internally or redirect using HTTP codes) User to the specific URL based on RegExp Rules. 4 | > The designated successor of [grunt-connect-rewrite](https://github.com/viart/grunt-connect-rewrite). 5 | 6 | ## Getting Started 7 | * Install the middleware by running: 8 | 9 | ```shell 10 | npm install http-rewrite-middleware --save 11 | ``` 12 | 13 | * Include the module: 14 | 15 | ```js 16 | var rewriteModule = require('http-rewrite-middleware'); 17 | ``` 18 | 19 | * Define your rules like: 20 | 21 | ```js 22 | var rewriteMiddleware = rewriteModule.getMiddleware([ 23 | // Internal rewrite 24 | {from: '^/index_dev.html$', to: '/src/index.html'}, 25 | // Internal rewrite 26 | {from: '^/js/(.*)$', to: '/src/js/$1'}, 27 | // 301 Redirect 28 | {from: '^/old-stuff/(.*)$', to: '/new-cool-stuff/$1', redirect: 'permanent'}, 29 | // 302 Redirect 30 | {from: '^/stuff/(.*)$', to: '/temporary-stuff/$1', redirect: 'temporary'} 31 | ]); 32 | ``` 33 | 34 | * See examples of integration with Connect / Express / Grunt bellow. 35 | 36 | ### Options 37 | 38 | ##### Rule's format: 39 | 40 | `{from: '__from__', to: '__to__'[, redirect: 'permanent'|'temporary']}` 41 | 42 | Where: 43 | * `__from__` - RegExp string to match. 44 | * `__to__` - String that replaces matched URL. 45 | * `redirect` - Optional parameter: 46 | * When it is omitted then the Rule will be dispatched as an internal rewrite (aka proxified). 47 | * If the value is set then Browser will receive HTTP `Location` Header with value of parsed `__to__` (`permanent` value will give `HTTP 301`, any other value will give `HTTP 302`). 48 | 49 | ### Example of usage with Connect 50 | 51 | ```js 52 | var connect = require('connect'), 53 | http = require('http'), 54 | rewriteModule = require('http-rewrite-middleware'); 55 | 56 | var app = connect() 57 | .use(rewriteModule.getMiddleware([ 58 | // ... list of rules here 59 | ]) 60 | .use(connect.static('public')); 61 | 62 | http.createServer(app).listen(3000); 63 | ``` 64 | 65 | ### Example of usage with Express 66 | 67 | ```js 68 | var express = require('express'), 69 | app = express(), 70 | rewriteModule = require('http-rewrite-middleware'); 71 | 72 | app.use(rewriteModule.getMiddleware([ 73 | // ... list of rules here 74 | ]); 75 | 76 | //... 77 | app.listen(3000); 78 | ``` 79 | 80 | ### Example of usage with Grunt ([grunt-contrib-connect](https://github.com/gruntjs/grunt-contrib-connect)) 81 | 82 | ```js 83 | var rewriteModule = require('http-rewrite-middleware'); 84 | 85 | grunt.initConfig({ 86 | connect: { 87 | options: { 88 | port: 9000, 89 | hostname: 'localhost' 90 | }, 91 | development: { 92 | options: { 93 | middleware: function (connect, options) { 94 | var middlewares = []; 95 | 96 | // RewriteRules support 97 | middlewares.push(rewriteModule.getMiddleware([ 98 | // ... list of rules here 99 | ])); 100 | 101 | if (!Array.isArray(options.base)) { 102 | options.base = [options.base]; 103 | } 104 | 105 | var directory = options.directory || options.base[options.base.length - 1]; 106 | options.base.forEach(function (base) { 107 | // Serve static files. 108 | middlewares.push(connect.static(base)); 109 | }); 110 | 111 | // Make directory browse-able. 112 | middlewares.push(connect.directory(directory)); 113 | 114 | return middlewares; 115 | } 116 | } 117 | } 118 | } 119 | }); 120 | ``` 121 | 122 | 123 | ## Debugging 124 | 125 | In order to debug Rules just add 2nd parameter to the `getMiddleware(...)` call 126 | as `getMiddleware(..., {verbose: true})` this will enable logging of matched rules. 127 | The message will explain which `__from__` rule was matched and what was the result of the rewrite. 128 | 129 | 130 | ## Contributing 131 | 132 | In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code. 133 | 134 | 135 | ## Release History 136 | * 2014.02.13 `v0.1.6` Improve logging format 137 | * 2014.02.13 `v0.1.5` Fix NPM... 138 | * 2014.01.29 `v0.1.3` Add logging support 139 | * 2013.12.17 `v0.1.1` Initial Release 140 | -------------------------------------------------------------------------------- /test/middleware_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Rewriter = require('../index').Rewriter, 4 | res = { 5 | statusCode: 0, 6 | headers: {}, 7 | wasEnded: 0, 8 | setHeader: function (key, value) { 9 | this.headers[key] = value; 10 | }, 11 | end: function () { 12 | this.wasEnded++; 13 | }, 14 | _reset: function () { 15 | this.wasEnded = 0; 16 | this.statusCode = 0; 17 | this.headers = {}; 18 | } 19 | }; 20 | 21 | exports.middleware = { 22 | setUp: function (done) { 23 | this.rewriter = new Rewriter([], {silent: true}); 24 | done(); 25 | }, 26 | tearDown: function (done) { 27 | delete this.rewriter; 28 | res._reset(); 29 | done(); 30 | }, 31 | testWrongRuleRegistration: function (test) { 32 | test.expect(5); 33 | 34 | test.equal(this.rewriter.registerRule(), false); 35 | test.equal(this.rewriter.registerRule({}), false); 36 | test.equal(this.rewriter.registerRule({from: 1}), false); 37 | test.equal(this.rewriter.registerRule({to: 0}), false); 38 | test.equal(this.rewriter.getRules().length, 0); 39 | 40 | test.done(); 41 | }, 42 | testCorrectRuleRegistration: function (test) { 43 | test.expect(3); 44 | 45 | test.equal(this.rewriter.registerRule({from: '', to: ''}), false); 46 | test.equal(this.rewriter.registerRule({from: '/from', to: '/to'}), true); 47 | test.equal(this.rewriter.getRules().length, 1); 48 | 49 | test.done(); 50 | }, 51 | testWithoutRules: function (test) { 52 | var _d = this.rewriter.dispatcher, 53 | wasCompleted = 0, 54 | wasDispached = 0; 55 | 56 | test.expect(2); 57 | 58 | this.rewriter.dispatcher = function () { wasDispached++; }; 59 | 60 | this.rewriter.getMiddleware()({url: '/'}, null, function () { wasCompleted++; }); 61 | test.equal(wasCompleted, 1, 'Should not block other middlewares.'); 62 | test.equal(wasDispached, 0, 'Should not try to dispatch without RewriteRules.'); 63 | 64 | this.rewriter.dispatcher = _d; 65 | 66 | test.done(); 67 | }, 68 | testInternalRule: function (test) { 69 | var req = {}, 70 | wasCompleted = 0; 71 | 72 | test.expect(12); 73 | 74 | test.equal(this.rewriter.registerRule({from: '^/fr[o0]m-([^-]+)-(\\d+)\\.html$', to: '/to-$1-$2.html'}), true); 75 | test.equal(this.rewriter.getRules().length, 1); 76 | 77 | req.url = '/fr0m-s0me-123.html'; 78 | this.rewriter.getMiddleware()(req, res, function () { wasCompleted++; }); 79 | test.equal(req.url, '/to-s0me-123.html', 'Should change matched URI.'); 80 | test.equal(wasCompleted, 1, 'Should not block other middlewares.'); 81 | test.equal(res.statusCode, 0, 'Should not change HTTP Status Code.'); 82 | test.same(res.headers, {}, 'Should not change Headers.'); 83 | test.equal(res.wasEnded, 0, 'Response should not be ended.'); 84 | 85 | req.url = '/error-case.html'; 86 | wasCompleted = 0; 87 | res._reset(); 88 | this.rewriter.getMiddleware()(req, res, function () { wasCompleted++; }); 89 | test.equal(req.url, '/error-case.html', 'Should not change not matched URI.'); 90 | test.equal(wasCompleted, 1, 'Should not block other middlewares.'); 91 | test.equal(res.statusCode, 0, 'Should not change HTTP Status Code.'); 92 | test.same(res.headers, {}, 'Should not change Headers.'); 93 | test.equal(res.wasEnded, 0, 'Response should not be ended.'); 94 | 95 | test.done(); 96 | }, 97 | testRedirectRule: function (test) { 98 | var req = {}, 99 | wasCompleted = 0; 100 | 101 | test.expect(12); 102 | 103 | test.equal(true, 104 | this.rewriter.registerRule({from: '^/fr[o0]m-([^-]+)-(\\d+)\\.html$', to: '/to-$1-$2.html', redirect: 'permanent'}) 105 | ); 106 | test.equal(this.rewriter.getRules().length, 1); 107 | 108 | req.url = '/fr0m-s0me-123.html'; 109 | this.rewriter.getMiddleware()(req, res, function () { wasCompleted++; }); 110 | test.equal(wasCompleted, 0, 'Should block other middlewares.'); 111 | test.equal(req.url, '/fr0m-s0me-123.html', 'Should not change matched URI.'); 112 | test.equal(res.statusCode, 301, 'Should change HTTP Status Code.'); 113 | test.same(res.headers, {'Location': '/to-s0me-123.html'}, 'Should add the `Location` Header.'); 114 | test.equal(res.wasEnded, 1, 'Response should be ended.'); 115 | 116 | req.url = '/error-case.html'; 117 | wasCompleted = 0; 118 | res._reset(); 119 | this.rewriter.getMiddleware()(req, res, function () { wasCompleted++; }); 120 | test.equal(wasCompleted, 1, 'Should not block other middlewares.'); 121 | test.equal(req.url, '/error-case.html', 'Should not change not matched URI.'); 122 | test.equal(res.statusCode, 0, 'Should not change HTTP Status Code.'); 123 | test.same(res.headers, {}, 'Should not change Headers.'); 124 | test.equal(res.wasEnded, 0, 'Response should not be ended.'); 125 | 126 | test.done(); 127 | }, 128 | testLogging: function (test) { 129 | var req = {}, 130 | wasCalled = 0; 131 | 132 | this.rewriter.log.verbose = function () { 133 | wasCalled++; 134 | }; 135 | 136 | test.expect(1); 137 | 138 | this.rewriter.registerRule({from: '^/fr[o0]m-([^-]+)-(\\d+)\\.html$', to: '/to-$1-$2.html'}); 139 | 140 | req.url = '/fr0m-s0me-123.html'; 141 | this.rewriter.getMiddleware()(req, res, function () { }); 142 | 143 | req.url = '/error-case.html'; 144 | this.rewriter.getMiddleware()(req, res, function () { }); 145 | 146 | test.equal(wasCalled, 1); 147 | 148 | test.done(); 149 | } 150 | }; 151 | --------------------------------------------------------------------------------