diff --git a/README.md b/README.md index 1c119b3..ffc8fad 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,22 @@ ES|Spain|webservices.amazon.es UK|United Kingdom|webservices.amazon.co.uk US|United States|webservices.amazon.com +## Proxy + +To allow node-apac through proxy, set the proxy configuration + +```javascript +var opHelper = new OperationHelper({ + awsId: '[YOUR AWS ID HERE]', + awsSecret: '[YOUR AWS SECRET HERE]', + assocId: '[YOUR ASSOCIATE TAG HERE]', + proxyHostname: '[YOUR PROXY HOSTNAME HERE]', + proxyPort: '[YOUR PROXY PORT HERE]', # default 8080 + proxyUsername: '[YOUR PROXY USERNAME HERE]' + proxyPassword: '[YOUR PROXY PASSWORD HERE]' +}); +``` + ## Contributing Feel free to submit a pull request. If you'd like, you may discuss the change with me first by submitting an issue. diff --git a/lib/operation-helper.js b/lib/operation-helper.js index 6b69362..bba30e4 100644 --- a/lib/operation-helper.js +++ b/lib/operation-helper.js @@ -5,6 +5,7 @@ const Throttler = require('./throttler') const locale = require('./locale') const https = require('https') +const http = require('http') const xml2js = require('xml2js') const defaultXml2JsOptions = { @@ -34,6 +35,10 @@ class OperationHelper { this.baseUri = params.baseUri || OperationHelper.defaultBaseUri this.xml2jsOptions = Object.assign({}, defaultXml2JsOptions, params.xml2jsOptions) this.throttler = new Throttler(params.maxRequestsPerSecond) + if (typeof(params.proxyHostname) === 'string') this.proxyHostname = params.proxyHostname + this.proxyPort = params.proxyPort || 8080 + if (typeof(params.proxyUsername) === 'string') this.proxyUsername = params.proxyUsername + if (typeof(params.proxyPassword) === 'string') this.proxyPassword = params.proxyPassword // set version if (typeof(params.version) === 'string') OperationHelper.version = params.version @@ -91,13 +96,28 @@ class OperationHelper { var options = { hostname: host, path: uri, - method: 'GET' + method: 'GET', + } + if (this.proxyHostname) { + options = { + hostname: this.proxyHostname, + port: this.proxyPort, + path: 'https://' + host + uri, + method: 'GET', + headers: { + Host: host + } + } + if (this.proxyUsername) { + var auth = 'Basic ' + new Buffer(this.proxyUsername + ':' + this.proxyPassword).toString('base64') + options.headers["Proxy-Authorization"] = auth + } } var responseBody = '' const promise = new Promise((resolve, reject) => { - var request = https.request(options, function (response) { + var request = (this.proxyHostname? http: https).request(options, function (response) { response.setEncoding('utf8') response.on('data', function (chunk) { diff --git a/lib/operation-helper.specs.js b/lib/operation-helper.specs.js index 9753b5e..f35eb24 100644 --- a/lib/operation-helper.specs.js +++ b/lib/operation-helper.specs.js @@ -1,6 +1,7 @@ "use strict" const https = require('https') +const http = require('http') const EventEmitter = require('events') const xml2js = require('xml2js') const proxyquire = require('proxyquire') @@ -243,6 +244,123 @@ describe('OperationHelper', function () { }) }) + describe('#execute with proxy', () => { + let requestMock, responseMock, result, outputResponseBody + const responseBody = 'xml' + const xml2jsOptions = {foo: 'bar'} + const expectedXml2jsOptions = Object.assign({explicitArray: false}, xml2jsOptions) + const proxyHostname ='localhost' + const proxyPort = 8080 + const proxyUsername = 'username' + const proxyPassword = 'password' + const testURI = '/onca/xml'; + + context('happy path proxy', () => { + let opHelper + + beforeEach(() => { + opHelper = new OperationHelper({ + awsId: 'testAwsId', + awsSecret: 'testAwsSecret', + assocId: 'testAssocId', + proxyHostname: proxyHostname, + proxyPort: proxyPort, + proxyUsername: proxyUsername, + proxyPassword: proxyPassword, + xml2jsOptions + }) + + responseMock = new EventEmitter() + responseMock.setEncoding = sinon.spy() + + requestMock = new EventEmitter() + requestMock.end = () => { + responseMock.emit('data', responseBody.substr(0, 5)) + responseMock.emit('data', responseBody.substr(5)) + responseMock.emit('end') + } + + sinon.stub(http, 'request').returns(requestMock).callsArgWith(1, responseMock) + sinon.stub(opHelper, 'generateUri').returns(testURI) + sinon.spy(xml2js, 'parseString') + }) + + afterEach(() => { + http.request.restore() + xml2js.parseString.restore() + }) + + const doAssertions = () => { + it('should create an http request to proxy with the correct options', () => { + expect(http.request.callCount).to.equal(1) + expect(http.request.firstCall.args[0]).to.eql({ + hostname: proxyHostname, + port: proxyPort, + method: 'GET', + headers: { + Host: locale.DEFAULT_ENDPOINT, + "Proxy-Authorization": "Basic " + new Buffer(proxyUsername + ':' + proxyPassword).toString('base64') + }, + path: 'https://' + locale.DEFAULT_ENDPOINT + testURI, + }) + }) + + it('should set the response encoding to utf8 (via proxy)', () => { + expect(responseMock.setEncoding.calledWith('utf8')) + }) + + it('should provide the raw response body', () => { + expect(outputResponseBody).to.equal(responseBody) + }) + + it('should pass the xml2jsOptions to xml2js', () => { + expect(xml2js.parseString.firstCall.args[1]).to.eql(expectedXml2jsOptions) + }) + + it('should parse XML and return result as object', () => { + expect(result).to.eql({ + it: { + is: { + some: 'xml' + } + } + }) + }) + } + + context('(traditional callback)', () => { + beforeEach((done) => { + opHelper.execute('ItemSearch', { + 'SearchIndex': 'Books', + 'Keywords': 'harry potter', + 'ResponseGroup': 'ItemAttributes,Offers' + }, function (err, _results, _rawResponseBody) { + result = _results + outputResponseBody = _rawResponseBody + done() + }) + }) + + doAssertions() + }) + + context('(promise)', () => { + beforeEach(() => { + return opHelper.execute('ItemSearch', { + 'SearchIndex': 'Books', + 'Keywords': 'harry potter', + 'ResponseGroup': 'ItemAttributes,Offers' + }).then((response) => { + result = response.result + outputResponseBody = response.responseBody + }) + }) + + doAssertions() + }) + }) + }) + context('when the request has an error', () => { const error = new Error('testErrorMessage') let thrownError, opHelper