diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000..d3146d2 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,3 @@ +# see available options / defaults at https://github.com/probot/stale/#usage +daysUntilStale: 60 +daysUntilClose: 7 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b60b42e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +sudo: false +language: node_js +cache: + directories: + - node_modules +notifications: + email: false +node_js: + - '6' +before_install: + - npm i -g npm@^3.0.0 + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start +script: TEST_CLIENT=selenium:firefox npm test +before_script: + - npm prune +after_success: + - npm run semantic-release diff --git a/CAKEFILE b/CAKEFILE deleted file mode 100644 index 7288a0d..0000000 --- a/CAKEFILE +++ /dev/null @@ -1,9 +0,0 @@ -{print} = require 'util' -{spawn} = require 'child_process' - -task 'build', 'compile', -> - coffee = spawn 'coffee', ['-c', '-b', '-o', './lib', 'src'] - coffee.stderr.on 'data', (data) -> - process.stderr.write data.toString() - coffee.stdout.on 'data', (data) -> - print data.toString() \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ac00bc8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM node:8 + +RUN npm install -g corsproxy + +EXPOSE 1337 + +ENV CORSPROXY_HOST 0.0.0.0 + +ENTRYPOINT ["corsproxy"] \ No newline at end of file diff --git a/Procfile b/Procfile deleted file mode 100644 index 6f86b16..0000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: node server.js \ No newline at end of file diff --git a/README.md b/README.md index 8133054..14a8219 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,48 @@ -## Installation +# corsproxy -As a standalone tool: +> standalone CORS proxy - $ npm install -g corsproxy +[![Build Status](https://travis-ci.org/gr2m/CORS-Proxy.svg?branch=master)](https://travis-ci.org/gr2m/CORS-Proxy) +[![Dependency Status](https://david-dm.org/gr2m/CORS-Proxy.svg)](https://david-dm.org/gr2m/CORS-Proxy) +[![devDependency Status](https://david-dm.org/gr2m/CORS-Proxy/dev-status.svg)](https://david-dm.org/gr2m/CORS-Proxy#info=devDependencies) -As a dependency: +[![NPM](https://nodei.co/npm/corsproxy.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/corsproxy/) - $ npm install corsproxy +## Setup -## Running +``` +npm install -g corsproxy +corsproxy +# with custom port: CORSPROXY_PORT=1234 corsproxy +# with custom host: CORSPROXY_HOST=localhost corsproxy +# with debug server: DEBUG=1 corsproxy +# with custom payload max bytes set to 10MB (1MB by default): CORSPROXY_MAX_PAYLOAD=10485760 corsproxy +``` -Standalone: +### Docker - $ corsproxy - CORS Proxy started on localhost:9292 +It is also possible to run the cors proxy in a docker container: -Standalone with custom host/port: +``` +# Build image +docker build -t corsproxy . - $ corsproxy 0.0.0.0 1234 - CORS Proxy started on 0.0.0.0:1234 +# Run container +docker run -p 1337:1337 --name corsproxy -d corsproxy +``` -As a dependency: - - var cors_proxy = require("corsproxy"); - var http_proxy = require("http-proxy"); - http_proxy.createServer(cors_proxy).listen(1234); - -With custom target: +## Usage - var cors_proxy = require("corsproxy"); - var http_proxy = require("http-proxy"); - cors_proxy.options = { - target: { - host:"0.0.0.0", - port:5984 - } - }; - http_proxy.createServer(cors_proxy).listen(1234); +The cors proxy will start at http://localhost:1337. +To access another domain, use the domain name (including port) as the first folder, e.g. +- http://localhost:1337/localhost:3000/sign_in +- http://localhost:1337/my.domain.com/path/to/resource +- etc etc -## Usage +By default the cors proxy will only answer requests sent to localhost. To use another domain (e.g. machine name) set an enviroment variable CORSPROXY_HOST to the required value before launching. -The cors proxy will start at http://localhost:9292. To access another domain, use the domain name (including port) as the first folder, e.g. +## License - http://localhost:9292/localhost:3000/sign_in - http://localhost:9292/my.domain.com/path/to/resource - etc etc \ No newline at end of file +MIT diff --git a/bin/corsproxy b/bin/corsproxy new file mode 100755 index 0000000..c82f3a5 --- /dev/null +++ b/bin/corsproxy @@ -0,0 +1,120 @@ +#!/usr/bin/env node + +var Hapi = require('hapi') +var plugin = require('../index') +var good = require('good') +var loggerOptions = require('../lib/logger-options') + +var server = new Hapi.Server({}) +var port = parseInt(process.env.CORSPROXY_PORT || process.env.PORT || 1337, 10) +var host = (process.env.CORSPROXY_HOST || 'localhost'); +var maxPayload = parseInt(process.env.CORSPROXY_MAX_PAYLOAD || 1048576, 10) +var proxy = server.connection({ port: port, labels: ['proxy'], host: host}) + +server.register(require('inert'), function () {}) +server.register(require('h2o2'), function () {}) + +// cors plugin +server.register(plugin, { + select: ['proxy'] +}, function (error) { + if (error) server.log('error', error) +}) + +// logger plugin +server.register({ + register: good, + options: loggerOptions +}, function (error) { + if (error) server.log('error', error) +}) + +// proxy route +proxy.route({ + method: '*', + path: '/{host}/{path*}', + handler: { + proxy: { + passThrough: true, + mapUri: function (request, callback) { + request.host = request.params.host + request.path = request.path.substr(request.params.host.length + 1) + request.headers['host'] = request.host + var query = request.url.search ? request.url.search : '' + console.log('proxy to http://' + request.host + request.path) + callback(null, 'http://' + request.host + request.path + query, request.headers) + } + } + }, + config: { + payload: { + maxBytes: maxPayload + } + } +}) + +// default route +proxy.route({ + method: 'GET', + path: '/', + handler: { + file: 'public/index.html' + } +}) +proxy.route({ + method: 'GET', + path: '/favicon.ico', + handler: { + file: 'public/favicon.ico' + } +}) + +if (process.env.DEBUG) { + var testport = port + 1 + var test = server.connection({ port: testport, labels: ['test'], host: host }) + + server.register(require('vision'), function (error) { + if (error) { + throw error + } + + server.views({ + engines: { ejs: require('ejs') }, + path: 'public/test' + }) + }) + + test.route({ + method: 'GET', + path: '/favicon.ico', + handler: { + file: 'public/favicon.ico' + } + }) + test.route({ + method: 'GET', + path: '/test.json', + handler: { + file: 'public/test/test.json' + } + }) + + test.route({ + method: 'GET', + path: '/', + handler: function (request, reply) { + reply.view('index', { + proxyPort: proxy.info.port, + testPort: test.info.port + }) + } + }) + + server.log('info', 'Debug server starting at: ' + test.info.uri) +} + +server.start(function (error) { + if (error) server.log('error', error) + + server.log('info', 'CORS Proxy running at: ' + proxy.info.uri) +}) diff --git a/bin/index.js b/bin/index.js deleted file mode 100755 index d8858f3..0000000 --- a/bin/index.js +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env node -var corsproxy = require("../lib/corsproxy"); -var httpProxy = require("http-proxy"); - -var host = process.env.HOST || process.argv[2] || "127.0.0.1"; -var port = process.env.PORT || process.argv[3] || 9292; - -httpProxy.createServer(corsproxy).listen(port, host, function() { - console.log("CORS Proxy started on %s:%d", host, port); -}); diff --git a/bin/test-background b/bin/test-background new file mode 100755 index 0000000..8a7466f --- /dev/null +++ b/bin/test-background @@ -0,0 +1,48 @@ +#!/bin/bash + +set -e + +function cleanup { + set +e + + npm run kill_selenium + + echo "\nYou can safely ignore errors below, if you see any" + + # kill chrome driver + CHROMEDRIVER_PID=`ps -ef | grep chromedriver | grep -v grep | awk '{print $2}'` + if [ "$CHROMEDRIVER_PID" ] ; then + kill -9 $CHROMEDRIVER_PID + fi + # kill test app + APP_PID=`ps -ef | grep 'corsproxy' | grep -v grep | awk '{print $2}'` + if [ "$APP_PID" ] ; then + kill -9 $APP_PID + fi +} + +# always kill selenium, no matter if tests pass or exit +trap cleanup EXIT + +if [ ! -d "/tmp/sv-selenium" ]; then + echo 'Selenium not yet installed, downloading & installing ...' + npm run install_selenium_and_chromedriver +fi + +npm run start_selenium_with_chromedriver & +DEBUG=1 npm start & + +COUNTER=0 +until $(curl --output /dev/null --silent --head --fail http://localhost:1338); do + let COUNTER=COUNTER+1 + echo "Waiting for app ... $COUNTER / 60" + sleep 1 + if [[ COUNTER -eq 60 ]] ; then + echo "" + echo "App start timeout" + exit 1 + fi +done +echo 'App started :)' + +npm run test:mocha diff --git a/index.js b/index.js new file mode 100644 index 0000000..8cb231e --- /dev/null +++ b/index.js @@ -0,0 +1,12 @@ +var addCorsHeaders = require('hapi-cors-headers') +var pkg = require('./package.json') + +function corsPlugin (server, options, next) { + server.ext('onPreResponse', addCorsHeaders) + next() +} + +corsPlugin.attributes = { + pkg: pkg +} +module.exports = corsPlugin diff --git a/lib/corsproxy.js b/lib/corsproxy.js deleted file mode 100644 index fb6a331..0000000 --- a/lib/corsproxy.js +++ /dev/null @@ -1,66 +0,0 @@ -// Generated by CoffeeScript 1.4.0 -var http, httpProxy, url; - -url = require('url'); - -http = require('http'); - -httpProxy = require('http-proxy'); - -module.exports = function(req, res, proxy) { - var cors_headers, header, headers, host, hostname, ignore, key, path, port, target, target0, value, _i, _len, _ref, _ref1, _ref2; - if (req.headers['access-control-request-headers']) { - headers = req.headers['access-control-request-headers']; - } else { - headers = 'accept, accept-charset, accept-encoding, accept-language, authorization, content-length, content-type, host, origin, proxy-connection, referer, user-agent, x-requested-with'; - _ref = req.headers; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - header = _ref[_i]; - if (req.indexOf('x-') === 0) { - headers += ", " + header; - } - } - } - cors_headers = { - 'access-control-allow-methods': 'HEAD, POST, GET, PUT, PATCH, DELETE', - 'access-control-max-age': '86400', - 'access-control-allow-headers': headers, - 'access-control-allow-credentials': 'true', - 'access-control-allow-origin': req.headers.origin || '*' - }; - if (req.method === 'OPTIONS') { - console.log('responding to OPTIONS request'); - res.writeHead(200, cors_headers); - res.end(); - } else { - module.exports.options = module.exports.options || {}; - if (module.exports.options.target) { - target0 = url.parse(module.exports.options.target); - target = { - host: target0.hostname, - port: target0.port - }; - path = req.url; - req.headers.host = target0.hostname; - } else { - _ref1 = req.url.match(/\/([^\/]+)(.*)/), ignore = _ref1[0], hostname = _ref1[1], path = _ref1[2]; - _ref2 = hostname.split(':'), host = _ref2[0], port = _ref2[1]; - target = { - host: host, - port: port - }; - req.headers.host = hostname; - } - if (!target) { - res.write("Cannot determine target host\n"); - res.end(); - return; - } - for (key in cors_headers) { - value = cors_headers[key]; - res.setHeader(key, value); - } - req.url = path; - return proxy.proxyRequest(req, res, target); - } -}; diff --git a/lib/logger-options.js b/lib/logger-options.js new file mode 100644 index 0000000..7af9fcf --- /dev/null +++ b/lib/logger-options.js @@ -0,0 +1,12 @@ +var goodConsole = require('good-console') +module.exports = { + opsInterval: 1000, + reporters: [{ + reporter: goodConsole, + events: { + log: '*', + request: '*', + response: '*' + } + }] +} diff --git a/package.json b/package.json index 01ff738..edc16ac 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,50 @@ { "name": "corsproxy", - "description": "standalone CORS proxy and library for your convenience", - "version": "0.2.9", - "main": "./lib/corsproxy.js", + "description": "standalone CORS proxy", + "main": "./index.js", "author": { "name": "Gregor Martynus", "email": "gregor@martynus.net" }, - "contributors": { - "name": "Jan Lehnardt", - "email": "jan@apache.org" - }, "dependencies": { - "http-proxy": "~0.8" + "ejs": "^2.3.3", + "good": "^6.3.0", + "good-console": "^5.0.3", + "h2o2": "^4.0.1", + "hapi": "^9.0.2", + "hapi-cors-headers": "^1.0.0", + "http-proxy": "~1.11", + "inert": "^3.0.1", + "vision": "^3.0.0" + }, + "devDependencies": { + "chai": "^3.2.0", + "chai-as-promised": "^5.1.0", + "colors": "^1.1.2", + "mocha": "^2.2.5", + "request": "^2.60.0", + "sauce-connect-launcher": "^0.12.0", + "semantic-release": "^4.0.3", + "standard": "^5.1.0", + "sv-selenium": "^0.2.6", + "wd": "^0.3.12" }, "bin": { - "corsproxy": "./bin/index.js" + "corsproxy": "./bin/corsproxy" }, - "engines": { - "node": ">=0.6.x", - "npm": ">=1.1.x" + "scripts": { + "install_selenium_and_chromedriver": "install_chromedriver && install_selenium", + "kill_selenium": "kill_selenium", + "semantic-release": "semantic-release pre && npm publish && semantic-release post", + "start": "./bin/corsproxy", + "start_app": "./bin/start-app", + "start_selenium_with_chromedriver": "start_selenium_with_chromedriver", + "test": "standard && ./bin/test-background", + "test:mocha": "mocha test/test-*.js" }, "repository": { "type": "git", "url": "https://github.com/gr2m/CORS-Proxy" - } -} \ No newline at end of file + }, + "license": "MIT" +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..85a4d9f Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..3dd4c69 --- /dev/null +++ b/public/index.html @@ -0,0 +1,33 @@ + + + + + CORS Proxy + + + +

CORS Proxy

+ +

+ To add CORS headers to another URL, set the target URL as the path for the CORS proxy's URL. + For example, if you wan to access http://my.domain.com/path/to/resource, and the CORS proxy's + URL is http://localhost:1337, the full proxy URL is http://localhost:1337/my.domain.com/path/to/resource. +

+ +

+ You do not open http://localhost:1337/my.domain.com/path/to/resource directly in your browser, + instead open it through XMLHttpRequest. + For example, if you are using jQuery, you can do $.get('http://localhost:1337/my.domain.com/path/to/resource') +

+ +

+ https://github.com/gr2m/CORS-Proxy +

+ + diff --git a/public/test/index.ejs b/public/test/index.ejs new file mode 100644 index 0000000..225b771 --- /dev/null +++ b/public/test/index.ejs @@ -0,0 +1,48 @@ + + + + CORS Proxy test + + +

CORS Proxy test

+ +
+

Example 1: try loading /test.json from different host

+
var url = 'http://127.0.0.1:<%= testPort %>/test.json'
+var request = new XMLHttpRequest();
+request.addEventListener('load', function () {
+  document.querySelector('#example-1 .result').textContent = 'Success'
+})
+request.addEventListener('error', function () {
+  document.querySelector('#example-1 .result').textContent = 'Error'
+})
+request.open('get', url, true)
+request.setRequestHeader('Content-Type', 'application/json')
+request.send()
+ +

Result:

+
+ +
+

Example 2: try loading /test.json from different host via proxy

+
var url = 'http://localhost:<%= proxyPort %>/127.0.0.1:<%= testPort %>/test.json'
+var request = new XMLHttpRequest();
+request.addEventListener('load', function () {
+  document.querySelector('#example-2 .result').textContent = 'Success'
+})
+request.addEventListener('error', function () {
+  document.querySelector('#example-2 .result').textContent = 'Error'
+})
+request.open('get', url, true)
+request.setRequestHeader('Content-Type', 'application/json')
+request.send()
+ +

Result:

+
+ + + + diff --git a/public/test/test.json b/public/test/test.json new file mode 100644 index 0000000..82dae19 --- /dev/null +++ b/public/test/test.json @@ -0,0 +1,3 @@ +{ + "ok": true +} \ No newline at end of file diff --git a/src/corsproxy.coffee b/src/corsproxy.coffee deleted file mode 100644 index 2edac02..0000000 --- a/src/corsproxy.coffee +++ /dev/null @@ -1,64 +0,0 @@ -url = require('url'); -http = require('http'); -httpProxy = require('http-proxy'); - - -module.exports = (req, res, proxy) -> - - if req.headers['access-control-request-headers'] - headers = req.headers['access-control-request-headers'] - else - headers = 'accept, accept-charset, accept-encoding, accept-language, authorization, content-length, content-type, host, origin, proxy-connection, referer, user-agent, x-requested-with' - headers += ", #{header}" for header in req.headers when req.indexOf('x-') is 0 - - cors_headers = - 'access-control-allow-methods' : 'HEAD, POST, GET, PUT, PATCH, DELETE' - 'access-control-max-age' : '86400' # 24 hours - 'access-control-allow-headers' : headers - 'access-control-allow-credentials' : 'true' - 'access-control-allow-origin' : req.headers.origin || '*' - - - if req.method is 'OPTIONS' - console.log 'responding to OPTIONS request' - res.writeHead(200, cors_headers); - res.end(); - return - - else - # Handle CORS proper. - - # do we have a target configured? - module.exports.options = module.exports.options || {} - if module.exports.options.target - target0 = url.parse module.exports.options.target - target = { - host: target0.hostname, - port: target0.port - } - path = req.url - req.headers.host = target0.hostname; - else - [ignore, hostname, path] = req.url.match(/\/([^\/]+)(.*)/) - [host, port] = hostname.split(':') - target = { - host: host, - port: port - } - req.headers.host = hostname - - unless target - res.write "Cannot determine target host\n" - res.end(); - return; - - # console.log "proxying to #{target.host}:#{target.port}#{path}" - - - res.setHeader(key, value) for key, value of cors_headers - - # req.headers.host = hostname - req.url = path - - # Put your custom server logic here, then proxy - proxy.proxyRequest(req, res, target); diff --git a/test/test-debug-page.js b/test/test-debug-page.js new file mode 100644 index 0000000..3c4c398 --- /dev/null +++ b/test/test-debug-page.js @@ -0,0 +1,31 @@ +/* global describe, it, beforeEach*/ +'use strict' + +describe('example app', function () { + this.timeout(60000) + + beforeEach(function () { + return this.browser + .get('http://localhost:1338') + }) + + it('Heading is "CORS Proxy test"', function () { + return this.browser + .elementByCssSelector('h1').text() + .should.eventually.equal('CORS Proxy test') + }) + + it('Example 1 result is Error', function () { + return this.browser + .waitForConditionInBrowser('!! document.querySelector("#example-1 .result").textContent', 10000) + .elementByCssSelector('#example-1 .result').text() + .should.eventually.equal('Error') + }) + + it('Example 2 result is Error', function () { + return this.browser + .waitForConditionInBrowser('!! document.querySelector("#example-2 .result").textContent', 10000) + .elementByCssSelector('#example-2 .result').text() + .should.eventually.equal('Success') + }) +}) diff --git a/test/test-init.js b/test/test-init.js new file mode 100644 index 0000000..5cbeedc --- /dev/null +++ b/test/test-init.js @@ -0,0 +1,118 @@ +/* global before, after*/ +'use strict' + +require('colors') +var wd = require('wd') +var sauceConnectLauncher = require('sauce-connect-launcher') + +var chai = require('chai') +var chaiAsPromised = require('chai-as-promised') +chai.use(chaiAsPromised) +chai.should() +var request = require('request').defaults({json: true}) + +// enables chai assertion chaining +chaiAsPromised.transferPromiseness = wd.transferPromiseness + +var SELENIUM_HUB = 'http://localhost:4444/wd/hub/status' + +var username = process.env.SAUCE_USERNAME +var accessKey = process.env.SAUCE_ACCESS_KEY +var tunnelId = process.env.TRAVIS_JOB_NUMBER || 'tunnel-' + Date.now() + +// process.env.TEST_CLIENT is a colon seperated list of +// (saucelabs|selenium):browserName:browserVerion:platform +var tmp = (process.env.TEST_CLIENT || 'selenium:chrome').split(':') +var client = { + runner: tmp[0] || 'selenium', + browser: tmp[1] || 'chrome', + version: tmp[2] || null, // Latest + platform: tmp[3] || null +} + +wd.configureHttp({timeout: 180000}) // 3 minutes + +before(function (done) { + var self = this + this.timeout(180000) + + var retries = 0 + var started = function () { + if (++retries > 60) { + done('Unable to connect to selenium') + return + } + + if (client.runner === 'saucelabs') { + startSauceConnect(startTest) + } else { + startSelenium(startTest) + } + + function startSelenium (callback) { + request(SELENIUM_HUB, function (error, resp) { + if (error) throw error + + if (resp && resp.statusCode === 200) { + self.browser = wd.promiseChainRemote() + callback() + } else { + console.log('.') + setTimeout(started, 1000) + } + }) + } + + function startSauceConnect (callback) { + console.log('connecting to saucelabs') + var options = { + username: username, + accessKey: accessKey, + tunnelIdentifier: tunnelId + } + + sauceConnectLauncher(options, function (err, process) { + if (err) { + console.error('Failed to connect to saucelabs') + console.error(err) + return process.exit(1) + } + self.browser = wd.promiseChainRemote('localhost', 4445, username, accessKey) + callback() + }) + } + + function startTest () { + // optional extra logging + self.browser.on('status', function (info) { + console.log(info.cyan) + }) + self.browser.on('command', function (eventType, command, response) { + if (eventType === 'RESPONSE') { + return console.log(' <', (response || 'ok').grey) + } + console.log(' > ' + eventType.cyan, command) + }) + + var options = { + browserName: client.browser, + version: client.version, + platform: client.platform, + tunnelTimeout: 30 * 60 * 1000, + name: client.browser + ' - ' + tunnelId, + 'max-duration': 60 * 45, + 'command-timeout': 599, + 'idle-timeout': 599, + 'tunnel-identifier': tunnelId + } + + self.browser.init(options, done) + } + } + + started() +}) + +after(function () { + return this.browser.quit() +})