├── .gitignore ├── README.md ├── lib ├── cli.js └── delegate.js ├── package.json └── test ├── behind-proxy-test.js ├── negative-test.js └── strict-ssl-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # npm-delegate 2 | A hierarchical npm-registry proxy to make private registries easier 3 | 4 | **Deprecation warning:** Plenty of people are using this with success, but I am no longer supporting it. Consider [Kappa](https://github.com/paypal/kappa), Paypal's fork of `npm-delegate` instead. 5 | 6 | npm registries all the way down. 7 | 8 | ## wherefore? 9 | 10 | Say you want to set up a local, private npm registry for certain modules, but 11 | you still want to be able to install public modules. Sure, thanks to couchdb, 12 | you could replicate the entire public registry down - but that's hundreds of 13 | gigabytes of extra disk space you'd need. 14 | 15 | 16 | +--+ 17 | +------------+ |p | 18 | | client | 'foo'? |r | +------------+ 19 | | | --------> |o | 'foo'? | | 20 | | | |x | ------> | private | 21 | | | |y | <------ | registry | 22 | | | | | 404 +------------+ 23 | | | | | 24 | | | | | 'foo'? +---------------+ 25 | | | | | -----------------> | | 26 | | | | | <----------------- | public | 27 | | | <-------- | | 'foo' | registry | 28 | +------------+ 'foo' +--+ +---------------+ 29 | 30 | 31 | ## Install 32 | 33 | npm install -g npm-delegate 34 | 35 | ## Usage 36 | 37 | Run `npm-delegate` somewhere - possibly on the server where you're running 38 | couchdb for your registry. 39 | 40 | npm-delegate registry1 registry2 registry3 41 | 42 | eg 43 | 44 | npm-delegate -p 1337 http://localhost:5984/registry https://registry.npmjs.org 45 | 46 | use timeout and retry in case of timeout 47 | 48 | npm-delegate --retry 3 --timeout 10000 -p 1337 http://localhost:5984/registry https://registry.npmjs.org 49 | 50 | setup your npm client: 51 | 52 | npm do-some-stuff --registry http://your-delegate-host:1337 53 | 54 | List as many registries as you want in fall-back order as command line 55 | arguments when starting `npm-delegate`. 56 | 57 | ## note: proxy is read-only 58 | 59 | Only GET requests are allowed. Strange things happens when you send 60 | state-changing requests around willy-nilly, and that's probably not what you 61 | want. For example, to publish a module, you probably want to specify which 62 | registry you're publishing it to, eg 63 | 64 | $ npm publish --registry http://mysweetregistry.com 65 | 66 | See also: [how to specify a registry to publish to in your package.json](https://npmjs.org/doc/registry.html#I-don-t-want-my-package-published-in-the-official-registry-It-s-private) 67 | 68 | ## faqs 69 | 70 | ### does this run a registry for me? 71 | no 72 | 73 | ### how do I set up my own private registry? 74 | read this: [https://github.com/isaacs/npmjs.org](https://github.com/isaacs/npmjs.org) 75 | 76 | ### What is it doing? 77 | Turn on more logging with NODE_DEBUG environment variable: 78 | 79 | NODE_DEBUG="npm-delegate,request" npm-delegate http://registry.npmjs.org http://internal/registry 80 | 81 | ### It complains about https certificates! 82 | Turn off strict SSL checking: 83 | 84 | npm-delegate --no-strictssl https://registry.npmjs.org http://internal/registry 85 | 86 | ### I'm behind a corporate proxy - help! 87 | Calm down: 88 | 89 | npm-delegate --proxy http://corp:8080 http://registry.npmjs.org http://internal/registry 90 | 91 | It will also pick up proxy settings from http_proxy environment variable if set. 92 | 93 | ## contributors 94 | 95 | - jden 96 | - Gareth Jones 97 | 98 | ## license 99 | MIT 100 | (c) 2012 jden - Jason Denizac 101 | http://jden.mit-license.org/2012 102 | -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var optimist = require('optimist') 3 | , delegate = require('./delegate') 4 | , http = require('http') 5 | , https = require('https') 6 | , argv = optimist 7 | .usage('Compose multiple npm registries in fallback order.\nUsage: $0 [opts] host1/registry host2/registry ... hostN/registry') 8 | .alias('p','port') 9 | .default('p',5983) 10 | .describe('p', 'port to listen on') 11 | .alias('r','retry') 12 | .default('r', 0) 13 | .describe('r', 'retry count in case of timeout') 14 | .default('timeout',3000) 15 | .describe('timeout', 'connection timeout') 16 | .alias('s','secure') 17 | .boolean('s') 18 | .default('s', false) 19 | .describe('s', 'run the proxy using https?') 20 | .string('proxy') 21 | .describe('proxy', 'use a proxy for all http requests') 22 | .boolean('strictssl') 23 | .default('strictssl', true) 24 | .describe('strictssl', 'only accept https certificates from known authorities (turn off with no-strictssl)') 25 | .check(function (argv) { 26 | if (!argv._.length) 27 | throw new Error('you must specify at least one registry (two to be useful)') 28 | }) 29 | .argv; 30 | 31 | var proxy = argv.proxy || process.env.http_proxy; 32 | 33 | (argv.secure ? https : http).createServer( 34 | delegate(argv._, proxy, argv.strictssl) 35 | ).listen(argv.port); 36 | console.log("npm-delegate listening on port ", argv.port); 37 | -------------------------------------------------------------------------------- /lib/delegate.js: -------------------------------------------------------------------------------- 1 | var fallback = require('fallback') 2 | var request = require('request') 3 | var path = require('path') 4 | var url = require('url2') 5 | 6 | function debug() { 7 | if (/npm-delegate/.test(process.env['NODE_DEBUG'])) { 8 | console.log.apply(console, arguments); 9 | debug = function() { 10 | console.log.apply(console, arguments); 11 | }; 12 | } else { 13 | debug = function() {}; 14 | } 15 | } 16 | 17 | module.exports = function setup(registryUrls, proxy, strictSSL, options) { 18 | var registries = registryUrls.map(parseRegistry); 19 | 20 | options = options || {}; 21 | if (proxy) { 22 | debug("Using proxy: ", proxy); 23 | } 24 | 25 | if (!strictSSL) { 26 | debug('Strict SSL turned OFF - will accept any certificate.'); 27 | } 28 | 29 | debug("Registries: ", registries.map(url.format)); 30 | 31 | return function delegate(req, resOut) { 32 | 33 | if (req.method !== 'GET') { 34 | debug('invalid method') 35 | resOut.statusCode = 405 36 | resOut.write(JSON.stringify({error: 'invalid method'})) 37 | resOut.end() 38 | return 39 | } 40 | 41 | fallback( 42 | registries, 43 | forwardReq(req, proxy, strictSSL, options), 44 | function (err, resIn, registry) { 45 | if (err) { 46 | debug("Err was ", err); 47 | resOut.write( 48 | JSON.stringify({ 49 | error: 'There was an error resolving your request:\n' + JSON.stringify(err, null, 2) 50 | }) 51 | ) 52 | return resOut.end() 53 | } 54 | else if (!resIn) { 55 | resOut.statusCode = 400 56 | resOut.write( 57 | JSON.stringify({ 58 | error: 'request could not be fulfilled by any of the registries on this proxy.' 59 | + 'perhaps the module you\'re looking for does not exist' 60 | }) 61 | ) 62 | resOut.end() 63 | } else { 64 | debug('proxying response from registry ', url.format(registry)) 65 | debug("setting header"); 66 | resOut.setHeader('x-registry', url.format(registry)) 67 | debug("piping to output"); 68 | 69 | resIn.pipe(resOut); 70 | resIn.on('error', function(err) { 71 | resOut.statusCode = 400 72 | resOut.write( 73 | JSON.stringify({ 74 | error: 'There was an error resolving your request:\n' + JSON.stringify(err, null, 2) 75 | }) 76 | ) 77 | //resOut.connection.destroy(); 78 | resOut.destroy(); 79 | debug('Got error during streaming: ' + (err.stack || err)); 80 | }); 81 | } 82 | }); 83 | 84 | }; 85 | }; 86 | 87 | function forwardReq(request, proxy, strictSSL, options) { 88 | return function(registry, cb) { 89 | forward(request, registry, proxy, strictSSL, options, cb); 90 | } 91 | } 92 | 93 | function forward(reqIn, registry, proxy, strictSSL, options, cb) { 94 | var reqOut = { 95 | protocol: registry.protocol 96 | , hostname: registry.hostname 97 | , port: registry.port 98 | , path: rebase(registry.path, reqIn.url) 99 | }; 100 | 101 | debug('fwd req', reqOut) 102 | 103 | reqIn.headers.target = reqIn.headers.host; 104 | delete reqIn.headers.host; 105 | delete reqIn.headers.authorization; 106 | 107 | var params = { 108 | url: url.format(reqOut), 109 | method: reqIn.method, 110 | headers: reqIn.headers, 111 | auth: registry.auth, 112 | proxy: proxy, 113 | strictSSL: strictSSL, 114 | timeout: options.timeout || 30000, 115 | retry: options.retry || 0, 116 | retryCount: 0 117 | }; 118 | 119 | lookBeforeYouLeap( 120 | params, 121 | function (err, res) { 122 | if (err) { 123 | debug("Error talking to %s, was: ", registry.hostname, err); 124 | return cb(); 125 | } 126 | 127 | if (res && res.statusCode >= 400) { 128 | debug( 129 | "Registry %s responded with %d for %s", 130 | registry.hostname, 131 | res.statusCode, 132 | reqOut.path 133 | ); 134 | return cb(); 135 | } 136 | 137 | return cb(null, res); 138 | } 139 | ); 140 | } 141 | 142 | function lookBeforeYouLeap(params, cb) { 143 | //do a HEAD request first 144 | params.method = "HEAD"; 145 | request(params, function(err, res) { 146 | debug("HEAD %s returned %d", params.url, res && res.statusCode); 147 | //nothing useful 148 | if (err || (res && res.statusCode >= 400)) { 149 | 150 | if (err && /TIMEDOUT$/.test(err.code) 151 | && params.retryCount++ <= params.retry) { 152 | 153 | debug("Got timeout will retry for: " + params.url); 154 | // retry 155 | process.nextTick(function() { 156 | lookBeforeYouLeap(params, cb); 157 | }); 158 | return; 159 | } 160 | 161 | return cb(err, res); 162 | } 163 | //we've got a useful response 164 | //make a get request 165 | debug("GET ", params.url); 166 | params.method = "GET"; 167 | return cb(null, request(params)); 168 | }); 169 | } 170 | 171 | function rebase(pathBase, pathExtra) { 172 | debug(pathBase, pathExtra); 173 | return path.join(pathBase, pathExtra); 174 | } 175 | 176 | function isNotHttp(protocol) { 177 | return "http:" !== protocol && "https:" !== protocol; 178 | } 179 | 180 | function parseRegistry(string) { 181 | var parsed = url.parse(string) 182 | if (isNotHttp(parsed.protocol)) { 183 | throw new Error('invalid registry address: specify a protocol (eg https://): ' + string); 184 | } 185 | 186 | return parsed; 187 | } 188 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "npm-delegate", 3 | "version": "0.2.0", 4 | "description": "a hierarchical npm-registry proxy to make private registries easier", 5 | "main": "./lib/delegate.js", 6 | "bin": { 7 | "npm-delegate": "./lib/cli.js" 8 | }, 9 | "preferGlobal": true, 10 | "scripts": { 11 | "test": "mocha -R spec" 12 | }, 13 | "keywords": [ 14 | "npm", 15 | "registry", 16 | "private", 17 | "proxy" 18 | ], 19 | "repository": "git@github.com:jden/npm-delegate.git", 20 | "author": "jden ", 21 | "contributors":["jden ", "Gareth Jones "], 22 | "license": "MIT", 23 | "dependencies": { 24 | "optimist": "~0.3.5", 25 | "fallback": "~1.0.0", 26 | "url2": "0.0.0", 27 | "request": "~2.21.0" 28 | }, 29 | "devDependencies": { 30 | "should": "~1.2.2", 31 | "mocha": "~1.10.0", 32 | "nock": "~0.18.2", 33 | "supertest": "~0.7.0", 34 | "sandboxed-module": "~0.2.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/behind-proxy-test.js: -------------------------------------------------------------------------------- 1 | var mocha = require('mocha') 2 | , should = require('should') 3 | , supertest = require('supertest') 4 | , nock = require('nock') 5 | , child_process = require('child_process') 6 | , path = require('path') 7 | , delegate = require('../lib/delegate'); 8 | 9 | describe('npm-delegate', function() { 10 | describe('with proxy specified', function() { 11 | var proxyRequests = nock('http://localhost:8080') 12 | .head('http://registry.npmjs.org/thing') 13 | .reply(404) 14 | .head('http://someotherregistry/thing') 15 | .reply(200) 16 | .get('http://someotherregistry/thing') 17 | .reply(200, {}); 18 | 19 | var server = delegate( 20 | ['http://registry.npmjs.org/', 'http://someotherregistry/'], 21 | 'http://localhost:8080' 22 | ); 23 | 24 | it('should use proxy for all requests', function(done) { 25 | supertest(server) 26 | .get('/thing') 27 | .expect(200, function(err) { 28 | proxyRequests.done(); 29 | done(err); 30 | }); 31 | }); 32 | }); 33 | 34 | describe('with no proxy specified', function() { 35 | var registryRequests = nock('http://registry.npmjs.org') 36 | .head('/thing') 37 | .reply(200) 38 | .get('/thing') 39 | .reply(200, {}); 40 | 41 | var server = delegate([ 'http://registry.npmjs.org' ]); 42 | 43 | it('should not try to use a proxy', function(done) { 44 | supertest(server) 45 | .get('/thing') 46 | .expect(200, function(err) { 47 | registryRequests.done(); 48 | done(err); 49 | }); 50 | }); 51 | }); 52 | 53 | describe('cli', function() { 54 | var npmDelegate; 55 | 56 | afterEach(function() { 57 | if (npmDelegate) { 58 | npmDelegate.kill(); 59 | } 60 | }); 61 | 62 | it('should pick up proxy settings from environment', function(done) { 63 | var messages = []; 64 | npmDelegate = child_process.spawn( 65 | process.execPath, 66 | [ path.join(__dirname, '../lib/cli.js'), 'http://reg1', 'http://reg2' ], 67 | { 68 | env: { 69 | 'http_proxy': 'http://proxy:8080', 70 | 'NODE_DEBUG': 'npm-delegate' 71 | } 72 | } 73 | ); 74 | npmDelegate.stdout.on('data', function(data) { 75 | messages.push(data.toString()); 76 | if (data.toString().indexOf('npm-delegate listening on port') > -1) { 77 | messages.should.include('Using proxy: http://proxy:8080\n'); 78 | done(); 79 | } 80 | }); 81 | }); 82 | 83 | it('should get proxy settings from arguments', function(done) { 84 | var messages = []; 85 | npmDelegate = child_process.spawn( 86 | process.execPath, 87 | [ 88 | path.join(__dirname, '../lib/cli.js'), 89 | 'http://reg1', 90 | 'http://reg2', 91 | '--proxy', 92 | 'http://another-proxy:8080' 93 | ], 94 | { 95 | env: { 96 | 'NODE_DEBUG': 'npm-delegate' 97 | } 98 | } 99 | ); 100 | npmDelegate.stdout.on('data', function(data) { 101 | messages.push(data.toString()); 102 | if (data.toString().indexOf('npm-delegate listening on port') > -1) { 103 | messages.should.include('Using proxy: http://another-proxy:8080\n'); 104 | done(); 105 | } 106 | }); 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /test/negative-test.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | , should = require('should') 3 | , child_process = require('child_process') 4 | , path = require('path') 5 | , request = require('request') 6 | , path = require('path') 7 | , delegate = require('../lib/delegate'); 8 | 9 | describe('npm-delegate negative', function() { 10 | 11 | process.env['NODE_DEBUG'] = 'npm-delegate'; 12 | 13 | var badServer; 14 | var server; 15 | var allowHead = true; 16 | before(function(done) { 17 | badServer = http.createServer(function(req, res) { 18 | // no response 19 | if (allowHead && req.method === 'HEAD') { 20 | res.writeHead(200); 21 | res.end(); 22 | } 23 | }); 24 | badServer.listen(8090); 25 | 26 | server = http.createServer( 27 | delegate( 28 | ['http://localhost:8090/'], null, false, { 29 | timeout: 1000, 30 | retry: 2 31 | })); 32 | server.listen(9090); 33 | 34 | done(); 35 | }); 36 | 37 | after(function() { 38 | badServer.close(); 39 | server.close(); 40 | }) 41 | 42 | it('should handle timeout and retry when server is not responsive to HEAD request on check before you leap', function(done) { 43 | this.timeout(10000); 44 | allowHead = false; 45 | 46 | request('http://localhost:9090/thing', function(err, res, body) { 47 | if (err) { 48 | done(err); 49 | return; 50 | } 51 | body.should.include("error"); 52 | done(); 53 | }); 54 | 55 | }); 56 | 57 | it('should handle timeout when server is not responsive to GET request during streaming', function(done) { 58 | this.timeout(10000); 59 | allowHead = true; 60 | 61 | request('http://localhost:9090/thing', function(err, res, body) { 62 | if (err) { 63 | done(err); 64 | return; 65 | } 66 | body.should.include('TIMEDOUT'); 67 | done(); 68 | }); 69 | 70 | }); 71 | 72 | }); 73 | 74 | describe('npm-delegate negative+positive', function() { 75 | 76 | process.env['NODE_DEBUG'] = 'npm-delegate'; 77 | 78 | var badServer, goodServer; 79 | var server; 80 | var allowHead = true; 81 | before(function(done) { 82 | badServer = http.createServer(function(req, res) { 83 | // no response 84 | if (allowHead && req.method === 'HEAD') { 85 | res.writeHead(200); 86 | res.end(); 87 | } 88 | }); 89 | badServer.listen(8090); 90 | 91 | goodServer = http.createServer(function(req, res) { 92 | // no response 93 | res.writeHead(200); 94 | if (req.method === 'HEAD') { 95 | res.end(); 96 | } 97 | res.end('you got it'); 98 | }); 99 | goodServer.listen(8091); 100 | 101 | server = http.createServer( 102 | delegate( 103 | ['http://localhost:8090/', 'http://localhost:8091/'], null, false, { 104 | timeout: 1000, 105 | retry: 2 106 | })); 107 | server.listen(9090); 108 | 109 | done(); 110 | }); 111 | 112 | after(function() { 113 | badServer.close(); 114 | goodServer.close(); 115 | server.close(); 116 | }) 117 | 118 | it('should handle timeout and retry when server is not responsive to HEAD request on check before you leap', function(done) { 119 | this.timeout(10000); 120 | allowHead = false; 121 | 122 | request('http://localhost:9090/thing', function(err, res, body) { 123 | if (err) { 124 | done(err); 125 | return; 126 | } 127 | res.statusCode.should.be.equal(200); 128 | body.should.include('you got it'); 129 | done(); 130 | }); 131 | 132 | }); 133 | 134 | it('should handle timeout when server is not responsive to GET request during streaming', function(done) { 135 | this.timeout(10000); 136 | allowHead = true; 137 | 138 | request('http://localhost:9090/thing', function(err, res, body) { 139 | if (err) { 140 | done(err); 141 | return; 142 | } 143 | console.log(body); 144 | done(); 145 | }); 146 | 147 | }); 148 | 149 | }); -------------------------------------------------------------------------------- /test/strict-ssl-test.js: -------------------------------------------------------------------------------- 1 | var mocha = require('mocha') 2 | , should = require('should') 3 | , child_process = require('child_process') 4 | , path = require('path') 5 | , supertest = require('supertest') 6 | , sandbox = require('sandboxed-module'); 7 | 8 | describe('npm-delegate', function() { 9 | describe('with no_strict_ssl option', function() { 10 | var requestOptions, delegate = sandbox.require( 11 | '../lib/delegate', 12 | { 13 | requires: { 14 | 'request': function(options, cb) { 15 | requestOptions = options; 16 | } 17 | } 18 | } 19 | ); 20 | 21 | supertest( 22 | delegate([ 'http://registry' ], null, false) 23 | ).get('/thing').end(); 24 | 25 | it('should pass the strictSSL:false option to request', function() { 26 | requestOptions.strictSSL.should.be.false; 27 | }); 28 | }); 29 | 30 | describe('cli', function() { 31 | var npmDelegate; 32 | 33 | afterEach(function() { 34 | if (npmDelegate) { 35 | npmDelegate.kill(); 36 | } 37 | }); 38 | 39 | it('should get no-strict-ssl option from arguments', function(done) { 40 | var messages = []; 41 | npmDelegate = child_process.spawn( 42 | process.execPath, 43 | [ path.join(__dirname, '../lib/cli.js'), '--no-strictssl', 'http://reg1', 'http://reg2' ], 44 | { 45 | env: { 46 | 'NODE_DEBUG': 'npm-delegate' 47 | } 48 | } 49 | ); 50 | npmDelegate.stdout.on('data', function(data) { 51 | messages.push(data.toString()); 52 | if (data.toString().indexOf('npm-delegate listening on port') > -1) { 53 | messages.should.include('Strict SSL turned OFF - will accept any certificate.\n'); 54 | done(); 55 | } 56 | }); 57 | }); 58 | }); 59 | }); 60 | --------------------------------------------------------------------------------