Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proxy Requests for Unknown Paths #56 #84

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -295,6 +314,7 @@ Release History
---------------
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

after the https://github.com/sideshowcoder/canned/blob/feature-56_proxy_request/README.md#variable-responses section you could add a new section to the README detailing how the proxy stuff works, this would need to contain some examples on how to use it with the commands to enter and the flow of a request through canned with a proxy enabled.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sideshowcoder yep i will do that, but give me till this weekend, because i am travelling till wednesday

### 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
Expand Down
11 changes: 10 additions & 1 deletion bin/canned
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -29,6 +31,7 @@ var dir = ''
, port = argv.p
, cors = argv.cors
, cors_headers = argv.headers
, proxy = argv.proxy
, logger
, cannedDir
, wildcard = argv.wildcard
Expand All @@ -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)

2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
65 changes: 37 additions & 28 deletions lib/canned.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'))
}
}

Expand Down Expand Up @@ -278,21 +279,21 @@ 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
httpObj.path = this.dir + httpObj.pathname.join('/')
httpObj.query = parsedurl.query
httpObj.method = req.method.toLowerCase()
httpObj.res = res
httpObj.req = req
httpObj.ctype = ''

this._log('request: ' + httpObj.method + ' ' + req.url)
Expand All @@ -313,15 +314,24 @@ 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
if (paths.length > 0) {
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)
Expand Down Expand Up @@ -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':
Expand All @@ -377,18 +386,18 @@ Canned.prototype.responseFilter = function (req, res) {
that._log('Invalid json content')
}
}
that.responder(responderBody, req, res)
cb(null, responderBody);
})
break
case 'GET':
var query = url.parse(req.url).query
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
}
}
Expand Down
34 changes: 34 additions & 0 deletions lib/proxyResponse.js
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
52 changes: 52 additions & 0 deletions spec/canned.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really like the test making use of an actual server makes this some much more useful! 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sideshowcoder glad you like it, it was almost impossible to test it otherwise

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)
})
})

})