├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_STORE 3 | npm-debug.* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 0.10 5 | - 0.12 6 | - iojs -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Divshot, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Superstatic Proxy Service [![Travis](https://img.shields.io/travis/divshot/superstatic-proxy.svg?style=flat-square)](https://travis-ci.org/divshot/superstatic-proxy) 2 | 3 | The Superstatic AJAX proxy lets you make requests to other domians without 4 | violating the Same-Origin Policy. For instance, you could mount a proxy 5 | called `api` that pointed to `https://api.your-app.com`. Once you've done 6 | so, a request to e.g. `/__/proxy/api/users.json` would be proxied through to 7 | `https://api.your-app.com/users.json`. 8 | 9 | ## Configuration 10 | 11 | The configuration for `superstatic-proxy` is an object where the keys are 12 | reference names for the service to which you're proxying and configuration 13 | details. An example (comments for clarity despite JSON syntax): 14 | 15 | ```js 16 | { 17 | "api": { 18 | // the origin of the server to which you want to make requests 19 | "origin": "https://api.your-app.com", 20 | 21 | // set default headers present on every request. these can be 22 | // overridden on individual AJAX requests 23 | "headers": { 24 | "Accept": "application/json" 25 | }, 26 | 27 | // if true, send the cookies from the static app along to the server 28 | "cookies": false, 29 | 30 | // set a timeout for requests sent to the proxy (defaults to 30 seconds) 31 | "timeout": 30 32 | } 33 | } 34 | ``` -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var https = require('https'); 3 | 4 | var join = require('join-path'); 5 | var httpProxy = require('http-proxy'); 6 | var url = require('fast-url-parser'); 7 | 8 | var PROXY = httpProxy.createProxyServer({ 9 | changeOrigin: true 10 | }); 11 | var DEFAULT_TIMEOUT = 30000; 12 | 13 | module.exports = function () { 14 | 15 | return function (req, res, next) { 16 | 17 | if (!req.service || !req.service.config) return next(); 18 | 19 | var requestUrlValues = (req.service.path || req.url).split('/'); 20 | var proxyName = requestUrlValues[2]; 21 | var config = getEndpointConfig(proxyName); 22 | 23 | if (!config || !config.origin) return next(); 24 | 25 | var endpointUri = requestUrlValues.slice(3).join('/'); 26 | 27 | // Set headers 28 | Object.keys(config.headers || {}).forEach(function (key) { 29 | 30 | var val = config.headers[key]; 31 | 32 | req.headers[key.toLowerCase()] = 33 | (req.headers[key.toLowerCase()] || config.headers[key]); 34 | }); 35 | 36 | if (config.cookies === false) delete req.headers.cookie; 37 | 38 | // Set relative path 39 | req.url = join('/', endpointUri); 40 | 41 | PROXY.web(req, res, proxyConfig()); 42 | 43 | function getEndpointConfig (name) { 44 | 45 | return req.service.config[name] || req.service.config[name.toLowerCase()] 46 | } 47 | 48 | function proxyConfig () { 49 | 50 | // Set up proxy agent 51 | var proxyAgent = http; 52 | var _proxyConfig = { 53 | target: config.origin 54 | // timeout: config.timeout || DEFAULT_TIMEOUT 55 | }; 56 | 57 | try { 58 | var parsed = url.parse(config.origin); 59 | agent = (parsed._protocol === 'https') ? https : http; 60 | _proxyConfig.headers = { 61 | host: parsed.host 62 | }; 63 | } 64 | catch (e) {} 65 | 66 | _proxyConfig.agent = agent.globalAgent; 67 | 68 | return _proxyConfig; 69 | } 70 | }; 71 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "superstatic-proxy", 3 | "version": "2.1.0", 4 | "description": "A Superstatic service for HTTP proxying of AJAX requests", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "mocha test/index.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/divshot/superstatic-proxy.git" 15 | }, 16 | "keywords": [ 17 | "superstatic", 18 | "proxy", 19 | "static", 20 | "hosting", 21 | "divshot" 22 | ], 23 | "author": "Divshot", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/divshot/superstatic-proxy/issues" 27 | }, 28 | "homepage": "https://github.com/divshot/superstatic-proxy", 29 | "devDependencies": { 30 | "connect": "~2.14.5", 31 | "cookie-parser": "~1.3.4", 32 | "deap": "^1.0.0", 33 | "expect.js": "~0.3.1", 34 | "mocha": "^2.1.0", 35 | "mocksy": "^1.0.0", 36 | "supertest": "^0.15.0" 37 | }, 38 | "dependencies": { 39 | "fast-url-parser": "^1.0.6-0", 40 | "http-proxy": "^1.5.3", 41 | "join-path": "^1.0.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var PORT = 9999; 2 | 3 | var proxy = require('../index.js'); 4 | var expect = require('expect.js'); 5 | var request = require('supertest'); 6 | var connect = require('connect'); 7 | var Mocksy = require('mocksy'); 8 | var clone = require('deap').clone; 9 | var cookieParser = require('cookie-parser'); 10 | var server = new Mocksy({port: PORT}); 11 | 12 | var proxySettings = { 13 | api: { 14 | origin: "http://localhost:" + PORT, 15 | headers: { 16 | 'Accept': 'application/json' 17 | }, 18 | timeout: 30, 19 | cookies: true 20 | } 21 | }; 22 | 23 | var configSetup = function (req, res, next) { 24 | req.service = { 25 | config: clone(proxySettings) 26 | }; 27 | 28 | req.service.path = req.url.replace('/__', ''); 29 | next(); 30 | }; 31 | 32 | describe('Superstatic Proxy', function () { 33 | var app; 34 | 35 | beforeEach(function (done) { 36 | app = connect() 37 | .use(clone(configSetup)) 38 | .use(proxy()); 39 | 40 | server.start(done); 41 | }); 42 | 43 | afterEach(function (done) { 44 | server.stop(done); 45 | }); 46 | 47 | // it.only('test', function (done) { 48 | // request(app) 49 | // .get('/__/proxy/api/users.json') 50 | // .expect(200) 51 | // .expect(function (data) { 52 | // expect(data.res.body.url).to.equal('/users.json'); 53 | // }) 54 | // .end(done); 55 | // }); 56 | 57 | it('skips middleware if config is not defined', function (done) { 58 | var app = connect() 59 | .use(proxy()); 60 | 61 | request(app) 62 | .get('/__/proxy/api/users.json') 63 | .expect(404) 64 | .end(done); 65 | }); 66 | 67 | it('skips the middleware if the endpiont name is not in the configuration', function (done) { 68 | var app = connect() 69 | .use(clone(configSetup)) 70 | .use(proxy()); 71 | 72 | request(app) 73 | .get('/__/proxy/not/users.json') 74 | .expect(404) 75 | .end(done); 76 | }); 77 | 78 | it('proxies a request', function (done) { 79 | request(app) 80 | .get('/__/proxy/api/users.json') 81 | .expect(200) 82 | .expect(function (data) { 83 | 84 | expect(data.res.body.url).to.equal('/users.json'); 85 | }) 86 | .end(done); 87 | }); 88 | 89 | it('proxies a request with the requested method', function (done) { 90 | request(app) 91 | .post('/__/proxy/api/users.json') 92 | .expect(200) 93 | .expect(function (data) { 94 | expect(data.res.body.method).to.equal('POST'); 95 | }) 96 | .end(done); 97 | }); 98 | 99 | it('proxies a DELETE request', function (done) { 100 | 101 | request(app) 102 | .delete('/__/proxy/api/users.json') 103 | .expect(200) 104 | .expect(function (data) { 105 | expect(data.res.body.method).to.equal('DELETE'); 106 | }) 107 | .end(done); 108 | }); 109 | 110 | it('proxies a request, ignoring the proxy name case', function (done) { 111 | request(app) 112 | .post('/__/proxy/Api/users.json') 113 | .expect(200) 114 | .expect(function (data) { 115 | expect(data.res.body.method).to.equal('POST'); 116 | }) 117 | .end(done); 118 | }); 119 | 120 | it('removes the host/origin from the original request', function (done) { 121 | request(app) 122 | .get('/__/proxy/api/users.json') 123 | .set('host', 'http://localhost') 124 | .expect(function (data) { 125 | expect(data.body.headers.host).to.not.equal('http://localhost'); 126 | }) 127 | .end(done); 128 | }); 129 | 130 | it('passes through the headers', function (done) { 131 | request(app) 132 | .get('/__/proxy/api/users.json') 133 | .expect(200) 134 | .expect(function (data) { 135 | expect(data.res.body.headers['accept']).to.equal('application/json'); 136 | }) 137 | .end(done); 138 | }); 139 | 140 | it('ignores headers in config if there are not any', function (done) { 141 | var app = connect() 142 | .use(configSetup) 143 | .use(function (req, res, next) { 144 | next(); 145 | }) 146 | .use(proxy()); 147 | 148 | request(app) 149 | .get('/__/proxy/api/users.json') 150 | .expect(200) 151 | .end(done); 152 | }); 153 | 154 | it('proxies a request with no config headers', function (done) { 155 | 156 | var app = connect() 157 | .use(configSetup) 158 | .use(function (req, res, next) { 159 | delete req.service.config.api.headers; 160 | next(); 161 | }) 162 | .use(proxy()); 163 | 164 | request(app) 165 | .get('/__/proxy/api/users.json') 166 | .expect(200) 167 | .end(done); 168 | }); 169 | 170 | it('overrides the config headers with any headers sent in the ajax request', function (done) { 171 | request(app) 172 | .get('/__/proxy/api/users.json') 173 | .set('Accept', 'text/html') 174 | .expect(200) 175 | .expect(function (data) { 176 | expect(data.res.body.headers['accept']).to.equal('text/html'); 177 | }) 178 | .end(done); 179 | }); 180 | 181 | it('configures request body pass through', function (done) { 182 | request(app) 183 | .post('/__/proxy/api/users.json') 184 | .send({key: 'value'}) 185 | .expect(200) 186 | .expect(function (data) { 187 | expect(data.res.body.body).to.eql({key: 'value'}); 188 | }) 189 | .end(done); 190 | }); 191 | 192 | it('strips the cookies if config cookies equal false', function (done) { 193 | var app = connect() 194 | .use(configSetup) 195 | .use(function (req, res, next) { 196 | req.service.config.api.cookies = false; 197 | next(); 198 | }) 199 | .use(cookieParser()) 200 | .use(cookieSetter) 201 | .use(proxy()); 202 | 203 | var agent = request.agent(app); 204 | 205 | agent 206 | .get('/set-cookie') 207 | .end(function () { 208 | setTimeout(function () { 209 | agent 210 | .get('/__/proxy/api/users.json') 211 | .expect(function (data) { 212 | expect(data.res.body.headers.cookie).to.equal(undefined); 213 | }) 214 | .end(done); 215 | }, 0); 216 | }); 217 | 218 | function cookieSetter (req, res, next) { 219 | if (req.url === '/set-cookie') { 220 | res.writeHead(200, [ 221 | ['Set-Cookie', 'cookie1=test1; Path=/'], 222 | ['Set-Cookie', 'cookie2=test2; Path=/'] 223 | ]); 224 | return res.end(); 225 | } 226 | next(); 227 | } 228 | }); 229 | 230 | it('configures cookie pass through', function (done) { 231 | var app = connect() 232 | .use(configSetup) 233 | .use(cookieParser()) 234 | .use(cookieSetter) 235 | .use(proxy()); 236 | 237 | var agent = request.agent(app); 238 | 239 | agent 240 | .get('/set-cookie') 241 | .end(function () { 242 | setTimeout(function () { 243 | agent 244 | .get('/__/proxy/api/users.json') 245 | .expect(function (data) { 246 | expect(data.res.body.headers.cookie).to.eql('cookie1=test1;cookie2=test2'); 247 | }) 248 | .end(done); 249 | }, 0); 250 | }); 251 | 252 | function cookieSetter (req, res, next) { 253 | if (req.url === '/set-cookie') { 254 | 255 | res.setHeader('Set-Cookie','cookie1=test1; Path=/'); 256 | res.setHeader('Set-Cookie','cookie2=test2; Path=/'); 257 | 258 | return res.end(); 259 | } 260 | next(); 261 | } 262 | }); 263 | 264 | // TODO: reimplement when we can figure out how to test 265 | // a request timeout 266 | it.skip('configures request timeout', function (done) { 267 | var domain = require('domain'); 268 | var d = domain.create(); 269 | var app = connect() 270 | .use(clone(configSetup)) 271 | .use(function (req, res, next) { 272 | req.service.config.api.timeout = 1; 273 | next(); 274 | }) 275 | .use(proxy()); 276 | 277 | d.run(function () { 278 | request(app) 279 | .get('/__/proxy/api/users.json') 280 | .end(function (err, data) { 281 | console.log(data); 282 | if (err && err.code === 'ECONNRESET') return; 283 | throw new Error('Timeout not set or did not timeout'); 284 | }); 285 | }); 286 | 287 | d.on('error', function (err) { 288 | if (err.code === 'ECONNRESET') done(); 289 | if (err.message === 'Timeout not set or did not timeout') throw new Error(err.message); 290 | }); 291 | }); 292 | 293 | }); --------------------------------------------------------------------------------