|
| 1 | +// © 2013 Rob W <[email protected]> |
| 2 | +// Released under the MIT license |
| 3 | + |
| 4 | +var httpProxy = require('http-proxy'); |
| 5 | +var net = require('net'); |
| 6 | +var regexp_tld = require('./regexp-top-level-domain'); |
| 7 | + |
| 8 | +var help_file = __dirname + '/help.txt'; |
| 9 | +var help_text; |
| 10 | +function showUsage(res) { |
| 11 | + if (help_text != null) { |
| 12 | + res.writeHead(200, {'content-type': 'text/plain'}); |
| 13 | + res.end(help_text); |
| 14 | + } else { |
| 15 | + require('fs').readFile(help_file, 'utf8', function(err, data) { |
| 16 | + if (err) { |
| 17 | + console.error(err); |
| 18 | + res.writeHead(500, {}); |
| 19 | + res.end(); |
| 20 | + } else { |
| 21 | + help_text = data; |
| 22 | + showUsage(res); // Recursive call, but since data is a string, the recursion will end |
| 23 | + } |
| 24 | + }); |
| 25 | + } |
| 26 | +} |
| 27 | + |
| 28 | +function hasNoContent(hostname) { |
| 29 | + // Show 404 for non-requests. For instance when hostname is favicon.ico, robots.txt, ... |
| 30 | + return !( |
| 31 | + regexp_tld.test(hostname) || |
| 32 | + net.isIPv4(hostname) || |
| 33 | + net.isIPv6(hostname) |
| 34 | + ); |
| 35 | +} |
| 36 | + |
| 37 | +function handleCookies(isAllowed, headers) { |
| 38 | + // Assumed that all headers' names are lowercase |
| 39 | + if (!isAllowed) { |
| 40 | + delete headers['set-cookie']; |
| 41 | + delete headers['set-cookie2']; |
| 42 | + return; |
| 43 | + } |
| 44 | + // TODO: Parse cookies, and change Domain and Secure flag to match the API domain, |
| 45 | + // and change Path to /<website>/.... |
| 46 | + //if (headers['set-cookie']) headers['set-cookie'] = _parseCookie(headers['set-cookie']); |
| 47 | + //if (headers['set-cookie2']) headers['set-cookie2'] = _parseCookie(headers['set-cookie']); |
| 48 | +} |
| 49 | + |
| 50 | +// Called on every request |
| 51 | +var handler = exports.handler = function(req, res, proxy) { |
| 52 | + |
| 53 | + var cors_headers = { |
| 54 | + 'access-control-allow-origin': req.headers.origin || '*' |
| 55 | + }; |
| 56 | + if (proxy.withCredentials) { |
| 57 | + // Allow sending of credentials ONLY if it's explicitly allowed on creation of the proxy. |
| 58 | + cors_headers['access-control-allow-credentials'] = 'true'; |
| 59 | + } |
| 60 | + if (req.headers['access-control-request-method']) { |
| 61 | + cors_headers['access-control-allow-methods'] = req.headers['access-control-request-method']; |
| 62 | + } |
| 63 | + if (req.headers['access-control-request-headers']) { |
| 64 | + cors_headers['access-control-allow-headers'] = req.headers['access-control-request-headers']; |
| 65 | + } |
| 66 | + |
| 67 | + if (req.method == 'OPTIONS') { |
| 68 | + // Pre-flight request. Reply successfully: |
| 69 | + res.writeHead(200, cors_headers); |
| 70 | + res.end(); |
| 71 | + return; |
| 72 | + } else { |
| 73 | + // Actual request. First, extract the desired URL from the request: |
| 74 | + var host, hostname, port, path, match; |
| 75 | + match = req.url.match(/^\/(?:(https?:)?\/\/)?(([^\/?]+?)(?::(\d{0,5})(?=[\/?]|$))?)([\/?][\S\s]*|$)/i); |
| 76 | + // ^^^^^^^ ^^^^^^^^ ^^^^^^^ ^^^^^^^^^^^^ |
| 77 | + // 1:protocol 3:hostname 4:port 5:path + query string |
| 78 | + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| 79 | + // 2:host |
| 80 | + if (!match || (match[2].indexOf('.') === -1 && match[2].indexOf(':') === -1) || match[4] > 65535) { |
| 81 | + // Incorrect usage. Show how to do it correctly. |
| 82 | + showUsage(res); |
| 83 | + return; |
| 84 | + } else if (match[2] === 'iscorsneeded') { |
| 85 | + // Is CORS needed? This path is provided so that API consumers can test whether it's necessary |
| 86 | + // to use CORS. The server's reply is always No, because if they can read it, then CORS headers |
| 87 | + // are not necessary. |
| 88 | + res.writeHead(200, {'Content-Type': 'text/plain'}); |
| 89 | + res.end('no'); |
| 90 | + return; |
| 91 | + } else if (match[4] > 65535) { |
| 92 | + // Port is higher than 65535 |
| 93 | + res.writeHead(400, 'Invalid port', cors_headers); |
| 94 | + res.end(); |
| 95 | + return; |
| 96 | + } else if ( hasNoContent(match[3]) ) { |
| 97 | + // Don't even try to proxy invalid hosts |
| 98 | + res.writeHead(404, cors_headers); |
| 99 | + res.end(); |
| 100 | + return; |
| 101 | + } else { |
| 102 | + host = match[2]; |
| 103 | + hostname = match[3]; |
| 104 | + // Read port from input: :<port> / 443 if https / 80 by default |
| 105 | + port = match[4] ? +match[4] : (match[1] && match[1].toLowerCase() === 'https:' ? 443 : 80); |
| 106 | + path = match[5]; |
| 107 | + } |
| 108 | + // Change the requested path: |
| 109 | + req.url = path; |
| 110 | + |
| 111 | + // Hook res.writeHead method to set the correct header |
| 112 | + var res_writeHead = res.writeHead; |
| 113 | + res.writeHead = function(statusCode, reasonPhrase, headers) { |
| 114 | + if (typeof reasonPhrase === 'object') { |
| 115 | + headers = reasonPhrase; |
| 116 | + } |
| 117 | + if (!headers) headers = cors_headers; |
| 118 | + else { |
| 119 | + var header; |
| 120 | + for (header in cors_headers) { |
| 121 | + // We define the cors_headers object, so we can be damn sure that hasOwnProperty is not a key of it. |
| 122 | + // and therefor we can use hOP directly instead of Object.prototype.hOP.call(...) |
| 123 | + if (cors_headers.hasOwnProperty(header)) { |
| 124 | + headers[header] = cors_headers[header]; |
| 125 | + } |
| 126 | + } |
| 127 | + |
| 128 | + if ((statusCode === 301 || statusCode === 302) && headers.location) { |
| 129 | + // Handle redirects |
| 130 | + // The X-Forwarded-Proto header is set by Heroku, and also by the http-proxy library when xforward is true) |
| 131 | + var proxy_base_url = (req.headers['x-forwarded-proto'] || 'http') + '://' + req.headers['host']; |
| 132 | + headers.location = proxy_base_url + '/' + headers.location; |
| 133 | + } |
| 134 | + handleCookies(proxy.withCredentials, headers); |
| 135 | + } |
| 136 | + return res_writeHead.apply(this, arguments); // headers are magically updated when variables are modified |
| 137 | + }; |
| 138 | + |
| 139 | + // Finally, proxy the request |
| 140 | + proxy.proxyRequest(req, res, { |
| 141 | + host: hostname, |
| 142 | + port: port |
| 143 | + }); |
| 144 | + } |
| 145 | +}; |
| 146 | + |
| 147 | +// Create server with default/recommended values |
| 148 | +// Creator still needs to call .listen() |
| 149 | +var createServer = exports.createServer = function() { |
| 150 | + if (arguments.length) { |
| 151 | + console.log('Warning: corsproxy.createServer ignores all arguments.'); |
| 152 | + } |
| 153 | + var options = { |
| 154 | + changeOrigin: true, |
| 155 | + xforward: true |
| 156 | + }; |
| 157 | + var server = httpProxy.createServer(options, handler); |
| 158 | + // When the server fails, just show a 404 instead of Internal server error |
| 159 | + server.proxy.on('proxyError', function(err, req, res) { |
| 160 | + res.writeHead(404, {}); |
| 161 | + res.end(); |
| 162 | + }); |
| 163 | + // Disable Cookies etc. If you want to enable cookies, please implement a cookie parser which |
| 164 | + // correctly uses the Path flag to separate cookies. |
| 165 | + server.proxy.withCredentials = false; |
| 166 | + return server; |
| 167 | +}; |
0 commit comments