├── .dockerignore ├── .gitignore ├── .jshintrc ├── Dockerfile ├── README.md ├── bin └── aws-es-proxy ├── index.js ├── package-lock.json └── package.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.swp 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esversion": 6 4 | } 5 | 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:6 2 | 3 | ENV TINI_VERSION v0.14.0 4 | 5 | RUN mkdir -p /usr/src/app 6 | WORKDIR /usr/src/app 7 | 8 | ENV NODE_ENV production 9 | ADD package.json /usr/src/app/ 10 | RUN npm install && npm cache clean 11 | COPY . /usr/src/app 12 | 13 | # Add Tini 14 | ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini 15 | RUN chmod +x /tini 16 | ENTRYPOINT ["/tini", "bin/aws-es-proxy", "--"] 17 | 18 | EXPOSE 9200 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Local AWS ElasticSearch Service proxy 2 | 3 | Easily utilise `curl`, Sense and other tools of your liking to get answers from your AWS hosted ElasticSearch Service while developing or debugging. 4 | 5 | `aws-es-proxy` is a dead simple local proxy, that knows how to sign your requests and talk to a hosted AWS ElasticSearch Service. 6 | 7 | ## Prequisities 8 | 9 | * node >= v4.0.0 (ES6) 10 | * Make sure your Elasticsearch domain is configured with access policy template "Allow or deny access to one or more AWS accounts or IAM users". 11 | * Make sure your IAM credentials are discoverable: 12 | * via environment variables `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` 13 | * via `aws-cli` authentication profile (defaults to profile `default`) 14 | * via instance profile on EC2 instance (with IAM role granting access to ES domain) 15 | 16 | ## Usage 17 | 18 | ``` 19 | $ aws-es-proxy --port 9200 --profile default --region eu-west-1 20 | ``` 21 | 22 | Fires up simple node HTTP proxy on port 9200 and signs your requests using aws-sdk using your `default` local AWS profile. 23 | 24 | ``` 25 | $ curl http://localhost:9200 26 | { 27 | "status" : 200, 28 | "name" : "Superia", 29 | "cluster_name" : "123456789:search", 30 | "version" : { 31 | "number" : "1.5.2", 32 | "build_hash" : "20085dbc168df96c59c4be65f2999990762dfc6f", 33 | "build_timestamp" : "2016-04-20T15:51:59Z", 34 | "build_snapshot" : false, 35 | "lucene_version" : "4.10.4" 36 | }, 37 | "tagline" : "You Know, for Search" 38 | } 39 | ``` 40 | 41 | ## With Docker 42 | 43 | ``` 44 | docker build -t aws-es-proxy . 45 | ``` 46 | 47 | Run and specify credentials via ENV variables. 48 | 49 | ``` 50 | docker run -it --rm -p 9210:9200 \ 51 | -e AWS_ACCESS_KEY_ID=... \ 52 | -e AWS_SECRET_ACCESS_KEY=... \ 53 | aws-es-proxy -- 54 | ``` 55 | 56 | Utilise configuration and profiles from the host. 57 | 58 | ``` 59 | docker run -it -v $HOME/.aws:/root/.aws --rm -p 9210:9200 \ 60 | aws-es-proxy -- --profile 61 | ``` 62 | 63 | 64 | ## Related 65 | * [aws-es-curl](https://github.com/joona/aws-es-curl) 66 | 67 | -------------------------------------------------------------------------------- /bin/aws-es-proxy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var module = require('../index'); 3 | module(); 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const co = require('co'); 3 | const url = require('url'); 4 | const http = require('http'); 5 | const options = require('optimist') 6 | .argv; 7 | 8 | const context = {}; 9 | const profile = process.env.AWS_PROFILE || options.profile || 'default'; 10 | 11 | var creds = {}; 12 | AWS.CredentialProviderChain.defaultProviders = [ 13 | () => { return new AWS.EnvironmentCredentials('AWS'); }, 14 | () => { return new AWS.EnvironmentCredentials('AMAZON'); }, 15 | () => { return new AWS.SharedIniFileCredentials({ profile: profile }); }, 16 | () => { return new AWS.EC2MetadataCredentials(); } 17 | ]; 18 | 19 | var execute = function(endpoint, region, path, headers, method, body) { 20 | return new Promise((resolve, reject) => { 21 | var req = new AWS.HttpRequest(endpoint); 22 | 23 | if(options.quiet !== true) { 24 | console.log('>>>', method, path); 25 | } 26 | 27 | req.method = method || 'GET'; 28 | req.path = path; 29 | req.region = region; 30 | req.body = body; 31 | 32 | req.headers['presigned-expires'] = false; 33 | req.headers.Host = endpoint.host; 34 | 35 | var signer = new AWS.Signers.V4(req, 'es'); 36 | signer.addAuthorization(creds, new Date()); 37 | 38 | // Now we have signed the "headers", we add extra headers passing 39 | // from the browser. We must strip any connection control, transport encoding 40 | // incorrect Origin headers, and make sure we don't change the Host header from 41 | // the one used for signing 42 | var keys = Object.keys(headers) 43 | for (var i = 0, len = keys.length; i < len; i++) { 44 | if (keys[i] != "host" && 45 | keys[i] != "accept-encoding" && 46 | keys[i] != "connection" && 47 | keys[i] != "origin") { 48 | req.headers[keys[i]] = headers[keys[i]]; 49 | } 50 | } 51 | 52 | var client = new AWS.NodeHttpClient(); 53 | client.handleRequest(req, null, (httpResp) => { 54 | var body = ''; 55 | httpResp.on('data', (chunk) => { 56 | body += chunk; 57 | }); 58 | httpResp.on('end', (chunk) => { 59 | resolve({ 60 | statusCode: httpResp.statusCode, 61 | headers: httpResp.headers, 62 | body: body 63 | }); 64 | }); 65 | }, (err) => { 66 | console.log('Error: ' + err); 67 | reject(err); 68 | }); 69 | }); 70 | }; 71 | 72 | var readBody = function(request) { 73 | return new Promise(resolve => { 74 | var body = []; 75 | 76 | request.on('data', chunk => { 77 | body.push(chunk); 78 | }); 79 | 80 | request.on('end', _ => { 81 | resolve(Buffer.concat(body).toString()); 82 | }); 83 | }); 84 | }; 85 | 86 | var requestHandler = function(request, response) { 87 | var body = []; 88 | 89 | request.on('data', chunk => { 90 | body.push(chunk); 91 | }); 92 | 93 | request.on('end', _ => { 94 | var buf = Buffer.concat(body).toString(); 95 | 96 | co(function*(){ 97 | return yield execute(context.endpoint, context.region, request.url, request.headers, request.method, buf); 98 | }) 99 | .then(resp => { 100 | // We need to pass through the response headers from the origin 101 | // back to the UA, but strip any connection control and content encoding 102 | // headers 103 | var headers = {} 104 | var keys = Object.keys(resp.headers); 105 | for (var i = 0, klen = keys.length; i < klen; i++) { 106 | var k = keys[i]; 107 | if (k != undefined && k != "connection" && k != "content-encoding") { 108 | headers[k] = resp.headers[keys[i]]; 109 | } 110 | } 111 | 112 | response.writeHead(resp.statusCode, headers); 113 | response.end(resp.body); 114 | }) 115 | .catch(err => { 116 | console.log('Unexpected error:', err.message); 117 | console.log(err.stack); 118 | 119 | response.writeHead(500, { 'Content-Type': 'application/json' }); 120 | response.end(err); 121 | }); 122 | }); 123 | }; 124 | 125 | var server = http.createServer(requestHandler); 126 | 127 | var startServer = function() { 128 | return new Promise((resolve) => { 129 | server.listen(context.port, function(){ 130 | console.log('Listening on', context.port); 131 | resolve(); 132 | }); 133 | }); 134 | }; 135 | 136 | 137 | var main = function() { 138 | co(function*(){ 139 | var maybeUrl = options._[0]; 140 | context.region = options.region || 'eu-west-1'; 141 | context.port = options.port || 9200; 142 | 143 | if(!maybeUrl || (maybeUrl && maybeUrl == 'help') || options.help || options.h) { 144 | console.log('Usage: aws-es-proxy [options] '); 145 | console.log(); 146 | console.log('Options:'); 147 | console.log("\t--profile \tAWS profile \t(Default: default)"); 148 | console.log("\t--region \tAWS region \t(Default: eu-west-1)"); 149 | console.log("\t--port \tLocal port \t(Default: 9200)"); 150 | console.log("\t--quiet \tLog less"); 151 | process.exit(1); 152 | } 153 | 154 | if(maybeUrl && maybeUrl.indexOf('http') === 0) { 155 | var uri = url.parse(maybeUrl); 156 | context.endpoint = new AWS.Endpoint(uri.host); 157 | } 158 | 159 | var chain = new AWS.CredentialProviderChain(); 160 | yield chain.resolvePromise() 161 | .then(function (credentials) { 162 | creds = credentials; 163 | }) 164 | .catch(function (err) { 165 | console.log('Error while getting AWS Credentials.') 166 | console.log(err); 167 | process.exit(1); 168 | }); 169 | 170 | yield startServer(); 171 | }) 172 | .then(res => { 173 | // start service 174 | console.log('Service started!'); 175 | }) 176 | .catch(err => { 177 | console.error('Error:', err.message); 178 | console.log(err.stack); 179 | process.exit(1); 180 | }); 181 | }; 182 | 183 | if(!module.parent) { 184 | main(); 185 | } 186 | 187 | module.exports = main; 188 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-es-proxy", 3 | "version": "1.0.4", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "aws-sdk": { 8 | "version": "2.149.0", 9 | "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.149.0.tgz", 10 | "integrity": "sha1-dvU3Iqd4C9sxkeg/J8EBCMb+mBM=", 11 | "requires": { 12 | "buffer": "4.9.1", 13 | "crypto-browserify": "1.0.9", 14 | "events": "1.1.1", 15 | "jmespath": "0.15.0", 16 | "querystring": "0.2.0", 17 | "sax": "1.2.1", 18 | "url": "0.10.3", 19 | "uuid": "3.1.0", 20 | "xml2js": "0.4.17", 21 | "xmlbuilder": "4.2.1" 22 | } 23 | }, 24 | "base64-js": { 25 | "version": "1.2.1", 26 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz", 27 | "integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==" 28 | }, 29 | "buffer": { 30 | "version": "4.9.1", 31 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", 32 | "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", 33 | "requires": { 34 | "base64-js": "1.2.1", 35 | "ieee754": "1.1.8", 36 | "isarray": "1.0.0" 37 | } 38 | }, 39 | "co": { 40 | "version": "4.6.0", 41 | "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", 42 | "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" 43 | }, 44 | "crypto-browserify": { 45 | "version": "1.0.9", 46 | "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-1.0.9.tgz", 47 | "integrity": "sha1-zFRJaF37hesRyYKKzHy4erW7/MA=" 48 | }, 49 | "events": { 50 | "version": "1.1.1", 51 | "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", 52 | "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" 53 | }, 54 | "ieee754": { 55 | "version": "1.1.8", 56 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", 57 | "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" 58 | }, 59 | "isarray": { 60 | "version": "1.0.0", 61 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 62 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 63 | }, 64 | "jmespath": { 65 | "version": "0.15.0", 66 | "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", 67 | "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" 68 | }, 69 | "lodash": { 70 | "version": "4.17.4", 71 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", 72 | "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" 73 | }, 74 | "minimist": { 75 | "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 76 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" 77 | }, 78 | "optimist": { 79 | "version": "0.6.1", 80 | "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", 81 | "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", 82 | "requires": { 83 | "minimist": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 84 | "wordwrap": "0.0.3" 85 | }, 86 | "dependencies": { 87 | "wordwrap": { 88 | "version": "0.0.3", 89 | "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", 90 | "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" 91 | } 92 | } 93 | }, 94 | "punycode": { 95 | "version": "1.3.2", 96 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", 97 | "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" 98 | }, 99 | "querystring": { 100 | "version": "0.2.0", 101 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", 102 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" 103 | }, 104 | "sax": { 105 | "version": "1.2.1", 106 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", 107 | "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" 108 | }, 109 | "url": { 110 | "version": "0.10.3", 111 | "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", 112 | "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", 113 | "requires": { 114 | "punycode": "1.3.2", 115 | "querystring": "0.2.0" 116 | } 117 | }, 118 | "uuid": { 119 | "version": "3.1.0", 120 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", 121 | "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" 122 | }, 123 | "xml2js": { 124 | "version": "0.4.17", 125 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.17.tgz", 126 | "integrity": "sha1-F76T6q4/O3eTWceVtBlwWogX6Gg=", 127 | "requires": { 128 | "sax": "1.2.1", 129 | "xmlbuilder": "4.2.1" 130 | } 131 | }, 132 | "xmlbuilder": { 133 | "version": "4.2.1", 134 | "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.2.1.tgz", 135 | "integrity": "sha1-qlijBBoGb5DqoWwvU4n/GfP0YaU=", 136 | "requires": { 137 | "lodash": "4.17.4" 138 | } 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-es-proxy", 3 | "version": "1.0.4", 4 | "description": "Simple Local AWS ElasticSearch Service proxy", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/joona/aws-es-proxy.git" 9 | }, 10 | "bin": { 11 | "aws-es-proxy": "bin/aws-es-proxy" 12 | }, 13 | "scripts": { 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "author": "Joona Kulmala ", 17 | "license": "MIT", 18 | "dependencies": { 19 | "aws-sdk": "^2.149.0", 20 | "co": "^4.6.0", 21 | "optimist": "^0.6.1" 22 | }, 23 | "engines": { 24 | "node": ">=4.0.0" 25 | } 26 | } 27 | --------------------------------------------------------------------------------