diff --git a/README.md b/README.md index 2d0d405..22ff941 100644 --- a/README.md +++ b/README.md @@ -200,8 +200,27 @@ directories, so that if for given a request like: you don't have a file in `./canned/api/users/1/profile/index.get.json` then it would look for a file in `./canned/api/users/any/index.get.json` or similar. Wildcards can be specified on the command line via +``` +canned --wildcard iamawildcard +``` - canned --wildcard iamawildcard +Proxy unknown requests +---------------------- + +You can configure canned to forward requests that dont have a response defined to a proxy domain, this is helpful when you want to mock some api, but forward the not defined ones to the actual API server. + +Proxy can be specified on the command line via +``` +canned --proxy http://api.domain.com +``` + +Proxy can also be configured when using canned programatically by passing `proxy` as an option, for example +``` +canned(apiDir, { + proxy: 'http://api.domain.com', + ... +}); +``` How about some docs inside for the responses? @@ -295,6 +314,7 @@ Release History --------------- ### next * fix improper handling of carriage return in windows #79 (@git-jiby-me) +* support to proxy unknown paths to another domain #56 (@git-jiby-me) ### 0.3.7 * The regex for matching request, was not considering arrays in the request JSON diff --git a/bin/canned b/bin/canned index 6b83918..63984c8 100755 --- a/bin/canned +++ b/bin/canned @@ -14,6 +14,8 @@ var canned = require('../index') .describe('cors', 'disable cors support') .default('headers', false) .describe('headers', 'add custom headers allowed in cors requests') + .default('proxy', false) + .describe('proxy', 'proxy unknown paths to this domain') .default('h', false) .alias('h', 'help') .describe('h', 'show the help') @@ -29,6 +31,7 @@ var dir = '' , port = argv.p , cors = argv.cors , cors_headers = argv.headers +, proxy = argv.proxy , logger , cannedDir , wildcard = argv.wildcard @@ -42,6 +45,12 @@ if (argv.q) { process.stdout.write('starting canned on port ' + port + ' for ' + cannedDir + '\n') } -var can = canned(dir, { logger: logger, cors: cors, cors_headers: cors_headers, wildcard: wildcard}) +var can = canned(dir, { + logger: logger, + cors: cors, + cors_headers: cors_headers, + wildcard: wildcard, + proxy: proxy +}) http.createServer(can).listen(port) diff --git a/index.js b/index.js index eb3c1cc..c0c2b53 100644 --- a/index.js +++ b/index.js @@ -7,7 +7,7 @@ var canned = function (dir, options) { if (!options) options = {} dir = path.relative(process.cwd(), dir) var c = new Canned(dir, options) - return c.responseFilter.bind(c) + return c.responder.bind(c) } module.exports = canned diff --git a/lib/canned.js b/lib/canned.js index 087dd57..5476224 100644 --- a/lib/canned.js +++ b/lib/canned.js @@ -4,15 +4,17 @@ var url = require('url') var fs = require('fs') var util = require('util') var Response = require('./response') +var ProxyResponse = require('./proxyResponse') var querystring = require('querystring') -var url = require('url') var cannedUtils = require('./utils') var lookup = require('./lookup') var _ = require('lodash') +var request = require('request') function Canned(dir, options) { this.logger = options.logger this.wildcard = options.wildcard || 'any' + this.proxy = options.proxy this.response_opts = { cors_enabled: options.cors, cors_headers: options.cors_headers @@ -224,27 +226,26 @@ Canned.prototype._responseForFile = function (httpObj, files, cb) { fs.readFile(filePath, { encoding: 'utf8' }, function (err, data) { var response if (err) { - response = new Response(getContentType('html'), '', 404, httpObj.res, that.response_opts) - cb('Not found', response) + cb(new Error('Not found')) } else { - var _data = that.getVariableResponse(data, httpObj.content, httpObj.headers) - data = _data.data - var statusCode = _data.statusCode - var content = that.sanatizeContent(data, fileObject) - - if (content !== false) { - response = new Response(_data.contentType || getContentType(fileObject.mimetype), content, statusCode, httpObj.res, that.response_opts, _data.customHeaders) - cb(null, response) - } else { - content = 'Internal Server error invalid input file' - response = new Response(getContentType('html'), content, 500, httpObj.res, that.response_opts) - cb(null, response) - } + that._extractRequestContent(httpObj.req, function (err, content) { + var _data = that.getVariableResponse(data, content, httpObj.headers) + data = _data.data + var statusCode = _data.statusCode + content = that.sanatizeContent(data, fileObject) + if (content !== false) { + response = new Response(_data.contentType || getContentType(fileObject.mimetype), content, statusCode, httpObj.res, that.response_opts, _data.customHeaders) + cb(null, response) + } else { + content = 'Internal Server error invalid input file' + response = new Response(getContentType('html'), content, 500, httpObj.res, that.response_opts) + cb(null, response) + } + }); } }) } else { - var response = new Response(getContentType('html'), '', 404, httpObj.res, that.response_opts) - cb('Not found', response) + cb(new Error('Not found')) } } @@ -278,14 +279,13 @@ Canned.prototype.respondWithAny = function (httpObj, files, cb) { }) } -Canned.prototype.responder = function(body, req, res) { - var responseHandler +Canned.prototype.responder = function(req, res) { + var responseHandler, proxyErrorHandler var httpObj = {} var that = this var parsedurl = url.parse(req.url) httpObj.headers = req.headers httpObj.accept = (req.headers && req.headers.accept) ? req.headers.accept.trim().split(',') : [] - httpObj.content = body httpObj.pathname = parsedurl.pathname.split('/') httpObj.dname = httpObj.pathname.pop() httpObj.fname = '_' + httpObj.dname @@ -293,6 +293,7 @@ Canned.prototype.responder = function(body, req, res) { httpObj.query = parsedurl.query httpObj.method = req.method.toLowerCase() httpObj.res = res + httpObj.req = req httpObj.ctype = '' this._log('request: ' + httpObj.method + ' ' + req.url) @@ -313,6 +314,11 @@ Canned.prototype.responder = function(body, req, res) { var paths = lookup(httpObj.pathname.join('/'), that.wildcard); paths.splice(0,1); // The first path is the default + proxyErrorHandler = function (err) { + that._log(' proxy gave error ' + err.code + '\n'); + var resp = new Response(getContentType('html'), '', 404, httpObj.res, that.response_opts) + resp.send(); + } responseHandler = function (err, resp) { if (err) { // Try more paths, if there are any still @@ -320,8 +326,12 @@ Canned.prototype.responder = function(body, req, res) { httpObj.path = that.dir + paths.splice(0, 1)[0]; httpObj.fname = '_' + httpObj.dname; return that.findResponse(httpObj, responseHandler); - } else { + } else if (that.proxy){ + that._log(' proxying request to ' + that.proxy + '\n'); + resp = new ProxyResponse(that.proxy, httpObj.req, httpObj.res, proxyErrorHandler) + }else { that._log(' not found\n'); + resp = new Response(getContentType('html'), '', 404, httpObj.res, that.response_opts) } } else { that._logHTTPObject(httpObj) @@ -357,11 +367,10 @@ Canned.prototype.findResponse = function(httpObj, cb) { }) } -Canned.prototype.responseFilter = function (req, res) { +Canned.prototype._extractRequestContent = function (req, cb) { var that = this var body = '' - - // assemble response body if GET/POST/PUT + // assemble request body if GET/POST/PUT switch(req.method) { case 'PUT': case 'POST': @@ -377,7 +386,7 @@ Canned.prototype.responseFilter = function (req, res) { that._log('Invalid json content') } } - that.responder(responderBody, req, res) + cb(null, responderBody); }) break case 'GET': @@ -385,10 +394,10 @@ Canned.prototype.responseFilter = function (req, res) { if (query && query.length > 0) { body = querystring.parse(query) } - that.responder(body, req, res) + cb(null, body) break default: - that.responder(body, req, res) + cb(null, body) break } } diff --git a/lib/proxyResponse.js b/lib/proxyResponse.js new file mode 100644 index 0000000..3843e71 --- /dev/null +++ b/lib/proxyResponse.js @@ -0,0 +1,34 @@ +"use strict"; +var fs = require('fs') +var request = require('request'); +var url = require('url'); + +function buildProxyUrl(originalUrl, proxy) { + var parsedurl = url.parse(originalUrl), proxyUrl; + proxyUrl = proxy + + (parsedurl.path || '') + + (parsedurl.query || '')+ + (parsedurl.hash || ''); + return proxyUrl; + +} + +function ProxyResponse(proxy, req, res, errorHandler) { + var that = this; + that.proxyUrl = buildProxyUrl(req.url, proxy); + that.req = req; + that.res = res; + that.errorHandler = errorHandler; +} + +ProxyResponse.prototype.send = function () { + var that = this; + var proxy = request(that.proxyUrl); + that.req.pipe(proxy) + .on('error', function(err) { + that.errorHandler() + }) + .pipe(that.res); +} + +module.exports = ProxyResponse diff --git a/package.json b/package.json index 3283531..59e88fc 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ }, "dependencies": { "lodash": "^3.10.1", - "optimist": "^0.6.0" + "optimist": "^0.6.0", + "request": "^2.67.0" }, "devDependencies": { "jasmine-node": "^1.14.2", diff --git a/spec/canned.spec.js b/spec/canned.spec.js index 860eb4d..9b1a62b 100644 --- a/spec/canned.spec.js +++ b/spec/canned.spec.js @@ -702,4 +702,56 @@ describe('canned', function () { }) }) + describe("proxy requests for unknown paths", function () { + it('should return 404 if path unknown and proxy not configured', function (done) { + req.url = '/unkown_path' + res.end = function (content) { + expect(res.statusCode).toEqual(404); + done() + } + can(req, res) + }) + + it('should return mock if path known and proxy configured', function (done) { + var can = canned('./spec/test_responses', { + proxy: 'http://localhost:9615' + }) + + req.url = '/a' + res.end = function (content) { + expect(res.statusCode).toEqual(200); + done() + } + can(req, res) + }) + + it('should proxy request if path unknown and proxy is configured', function (done) { + var proxy = require('http').createServer(function (req, res) { + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.end('OK'); + req.on('data', function (data) { + expect(data.toString()).toEqual('test'); + }) + }).listen(9615); + + var can = canned('./spec/test_responses', { + proxy: 'http://localhost:9615' + }) + + var req = new require('stream').Readable(); + req._read = function noop() {}; + req.push('test'); + req.method = 'POST' + req.url = '/unkown_path' + + var res = new require('stream').Writable(); + res._write = function noop(data) { + expect(data.toString()).toEqual('OK'); + proxy.close() + done() + }; + can(req, res) + }) + }) + })