├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── build.sh ├── config.js ├── package-lock.json ├── package.json ├── readme.md ├── scripts.js ├── src ├── api.js ├── logger.js ├── modules │ ├── mask.js │ └── proxy.js └── proxy │ └── configured-domains.js └── static-test └── index.html /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !src 3 | !config.js 4 | !LICENSE 5 | !package.json 6 | !package-lock.json 7 | !readme.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build node_modules to avoid installing NPM (additinal container size) 2 | 3 | FROM alpine AS stage 4 | 5 | WORKDIR /app 6 | COPY . /app 7 | 8 | RUN apk add --update 'npm<13.0.0' 9 | RUN npm install 10 | 11 | # Build aclual container 12 | 13 | FROM alpine 14 | 15 | COPY . /app 16 | COPY --from=stage /app/node_modules /app/node_modules 17 | WORKDIR /app 18 | 19 | RUN apk add --update 'nodejs<13.0.0' 20 | 21 | EXPOSE 80 22 | 23 | ENV APP__ENV_NAME=prod 24 | CMD node --max-http-header-size=1048576 -r esm src/api.js 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2019 Nikita Savchenko (https://nikita.tk) 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Usage: ./build.sh 1.0.9 4 | 5 | HUB=zitros/analytics-saviour 6 | 7 | docker build --rm -t image . 8 | 9 | if [ $# -eq 1 ] 10 | then 11 | docker tag image $HUB:$1 12 | fi 13 | docker tag image $HUB:latest 14 | 15 | docker push $HUB:latest 16 | if [ $# -eq 1 ] 17 | then 18 | docker push $HUB:$1 19 | fi -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | const MATCH_EVERYTHING_STRING = '.*'; 2 | const env = process.env.APP__ENV_NAME || "local"; 3 | const isLocal = env === "local" || env === "test"; 4 | const proxyDomain = process.env.APP__PROXY_DOMAIN || ''; 5 | const strippedPath = process.env.APP__STRIPPED_PATH || ''; 6 | const hostsWhitelistRegex = (() => { 7 | try { 8 | return new RegExp(process.env.APP__HOSTS_WHITELIST_REGEX || MATCH_EVERYTHING_STRING); 9 | } catch (e) { 10 | console.error(`APP__HOSTS_WHITELIST_REGEX=${process.env.APP__HOSTS_WHITELIST_REGEX} cannot be converted to RegExp:`, e, '\nUsing the default.'); 11 | return new RegExp(MATCH_EVERYTHING_STRING); 12 | } 13 | })(); 14 | 15 | console.log("Environment variables:"); 16 | console.log(`APP__STRIPPED_PATH=${strippedPath} (a prefix path added to the original host, which will be removed in the proxy request)`); 17 | console.log(`APP__PROXY_DOMAIN=${proxyDomain} (an optional proxy domain which will be used for client-side requests instead of the current domain)`); 18 | console.log(`APP__ENV_NAME=${env} (should not be local nor test in production)`); 19 | console.log(`APP__HOSTS_WHITELIST_REGEX=${hostsWhitelistRegex}${hostsWhitelistRegex.toString() === `/${MATCH_EVERYTHING_STRING}/` ? ' (YAY!! Anyone can use your proxy!)' : ''}`); 20 | 21 | export default { 22 | isLocalEnv: isLocal, 23 | httpPort: process.env.PORT || 80, 24 | strippedPath, 25 | hostsWhitelistRegex: hostsWhitelistRegex, 26 | proxyDomain, // Domain to proxy calls through. Leave it empty to use the requested domain as a proxy domain 27 | proxy: { // Proxy configuration is here 28 | domains: [ // These domains are replaced in any proxied response (including scripts, URLs and redirects) 29 | "adservice.google.com", 30 | "www.google-analytics.com", 31 | "analytics.google.com", 32 | "www.googleadservices.com", 33 | "www.googletagmanager.com", 34 | "google-analytics.bi.owox.com", 35 | "googleads.g.doubleclick.net", 36 | "stats.g.doubleclick.net", 37 | "ampcid.google.com", 38 | "www.google.%", 39 | "www.google.com", 40 | "bat.bing.com", 41 | "static.hotjar.com", 42 | "trackcmp.net", 43 | "connect.facebook.net", 44 | "www.facebook.com", 45 | "rum-static.pingdom.net", 46 | "s.adroll.com", 47 | "d.adroll.com", 48 | "bid.g.doubleclick.net", 49 | "rum-collector-2.pingdom.net", 50 | "script.hotjar.com", 51 | "vars.hotjar.com", 52 | "pixel.advertising.com", 53 | "dsum-sec.casalemedia.com", 54 | "pixel.rubiconproject.com", 55 | "sync.outbrain.com", 56 | "simage2.pubmatic.com", 57 | "trc.taboola.com", 58 | "eb2.3lift.com", 59 | "ads.yahoo.com", 60 | "x.bidswitch.net", 61 | "ib.adnxs.com", 62 | "idsync.rlcdn.com", 63 | "us-u.openx.net", 64 | "cm.g.doubleclick.net" 65 | ], 66 | specialContentReplace: { // Special regex rules for domains 67 | "www.googletagmanager.com": [ 68 | { 69 | regex: /"https\:\/\/s","http:\/\/a","\.adroll\.com/, 70 | replace: ({ host }) => `"https://${ host }/s","http://${ host }/a",".adroll.com` 71 | } 72 | ], 73 | "eb2.3lift.com": [ 74 | { // Because eb2.3lift.com/xuid?mid=_&xuid=_&dongle=_ redirects to "/xuid" which doesn't exists 75 | regex: /^\/xuid/, 76 | replace: "https://eb2.3lift.com/xuid" // eb2.3lift.com is replaced then again with the correct proxy 77 | } 78 | ] 79 | }, 80 | ipOverrides: { // IP override rules for domains 81 | "google-analytics.bi.owox.com": { // Currently, this is useless as owox.com is having problems with overriding IP addresses, even though they state that they support everything from the Google Measurement Protocol. 82 | urlMatch: /\/collect/, 83 | queryParameterName: ["uip", "device.ip"] 84 | }, 85 | "www.google-analytics.com": { 86 | urlMatch: /\/collect/, 87 | queryParameterName: ["uip", "_uip"] 88 | } 89 | }, 90 | maskPaths: [ // Paths which are masked in URLs and redirects in order to avoid firing ad-blocking rules 91 | "/google-analytics", 92 | "/www.google-analytics.com", 93 | "/adsbygoogle", 94 | "/gtag/js", 95 | "/googleads", 96 | "/log_event\\?", 97 | "/r/collect", 98 | "/j/collect", 99 | "/g/collect", // As of Dec 2020, Google Measurement Protocol v2 doesn't yet support location overwriting. 100 | "/collect", 101 | "/pageread/conversion", 102 | "/pagead/conversion", 103 | "/googleads", 104 | "/prum", 105 | "/beacon", 106 | "/pixel", 107 | "/AdServer", 108 | "/ads/", 109 | "/gtm.js", 110 | "openx\\." 111 | ], 112 | } 113 | }; 114 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "google-tag-manager-proxy", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.3.8", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 10 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 11 | "requires": { 12 | "mime-types": "~2.1.34", 13 | "negotiator": "0.6.3" 14 | } 15 | }, 16 | "array-flatten": { 17 | "version": "1.1.1", 18 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 19 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" 20 | }, 21 | "body-parser": { 22 | "version": "1.20.1", 23 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", 24 | "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", 25 | "requires": { 26 | "bytes": "3.1.2", 27 | "content-type": "~1.0.4", 28 | "debug": "2.6.9", 29 | "depd": "2.0.0", 30 | "destroy": "1.2.0", 31 | "http-errors": "2.0.0", 32 | "iconv-lite": "0.4.24", 33 | "on-finished": "2.4.1", 34 | "qs": "6.11.0", 35 | "raw-body": "2.5.1", 36 | "type-is": "~1.6.18", 37 | "unpipe": "1.0.0" 38 | }, 39 | "dependencies": { 40 | "bytes": { 41 | "version": "3.1.2", 42 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 43 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" 44 | }, 45 | "depd": { 46 | "version": "2.0.0", 47 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 48 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" 49 | }, 50 | "http-errors": { 51 | "version": "2.0.0", 52 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 53 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 54 | "requires": { 55 | "depd": "2.0.0", 56 | "inherits": "2.0.4", 57 | "setprototypeof": "1.2.0", 58 | "statuses": "2.0.1", 59 | "toidentifier": "1.0.1" 60 | } 61 | }, 62 | "inherits": { 63 | "version": "2.0.4", 64 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 65 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 66 | }, 67 | "raw-body": { 68 | "version": "2.5.1", 69 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", 70 | "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", 71 | "requires": { 72 | "bytes": "3.1.2", 73 | "http-errors": "2.0.0", 74 | "iconv-lite": "0.4.24", 75 | "unpipe": "1.0.0" 76 | } 77 | }, 78 | "setprototypeof": { 79 | "version": "1.2.0", 80 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 81 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 82 | }, 83 | "statuses": { 84 | "version": "2.0.1", 85 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 86 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" 87 | }, 88 | "toidentifier": { 89 | "version": "1.0.1", 90 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 91 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" 92 | } 93 | } 94 | }, 95 | "bytes": { 96 | "version": "3.1.0", 97 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", 98 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" 99 | }, 100 | "call-bind": { 101 | "version": "1.0.2", 102 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", 103 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", 104 | "requires": { 105 | "function-bind": "^1.1.1", 106 | "get-intrinsic": "^1.0.2" 107 | } 108 | }, 109 | "content-disposition": { 110 | "version": "0.5.4", 111 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 112 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 113 | "requires": { 114 | "safe-buffer": "5.2.1" 115 | } 116 | }, 117 | "content-type": { 118 | "version": "1.0.4", 119 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 120 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 121 | }, 122 | "cookie": { 123 | "version": "0.5.0", 124 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", 125 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" 126 | }, 127 | "cookie-signature": { 128 | "version": "1.0.6", 129 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 130 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" 131 | }, 132 | "debug": { 133 | "version": "2.6.9", 134 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 135 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 136 | "requires": { 137 | "ms": "2.0.0" 138 | } 139 | }, 140 | "depd": { 141 | "version": "1.1.2", 142 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 143 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 144 | }, 145 | "destroy": { 146 | "version": "1.2.0", 147 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 148 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" 149 | }, 150 | "ee-first": { 151 | "version": "1.1.1", 152 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 153 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 154 | }, 155 | "encodeurl": { 156 | "version": "1.0.2", 157 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 158 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" 159 | }, 160 | "es6-promise": { 161 | "version": "4.2.8", 162 | "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", 163 | "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" 164 | }, 165 | "escape-html": { 166 | "version": "1.0.3", 167 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 168 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 169 | }, 170 | "esm": { 171 | "version": "3.2.25", 172 | "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", 173 | "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" 174 | }, 175 | "etag": { 176 | "version": "1.8.1", 177 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 178 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" 179 | }, 180 | "express": { 181 | "version": "4.18.2", 182 | "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", 183 | "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", 184 | "requires": { 185 | "accepts": "~1.3.8", 186 | "array-flatten": "1.1.1", 187 | "body-parser": "1.20.1", 188 | "content-disposition": "0.5.4", 189 | "content-type": "~1.0.4", 190 | "cookie": "0.5.0", 191 | "cookie-signature": "1.0.6", 192 | "debug": "2.6.9", 193 | "depd": "2.0.0", 194 | "encodeurl": "~1.0.2", 195 | "escape-html": "~1.0.3", 196 | "etag": "~1.8.1", 197 | "finalhandler": "1.2.0", 198 | "fresh": "0.5.2", 199 | "http-errors": "2.0.0", 200 | "merge-descriptors": "1.0.1", 201 | "methods": "~1.1.2", 202 | "on-finished": "2.4.1", 203 | "parseurl": "~1.3.3", 204 | "path-to-regexp": "0.1.7", 205 | "proxy-addr": "~2.0.7", 206 | "qs": "6.11.0", 207 | "range-parser": "~1.2.1", 208 | "safe-buffer": "5.2.1", 209 | "send": "0.18.0", 210 | "serve-static": "1.15.0", 211 | "setprototypeof": "1.2.0", 212 | "statuses": "2.0.1", 213 | "type-is": "~1.6.18", 214 | "utils-merge": "1.0.1", 215 | "vary": "~1.1.2" 216 | }, 217 | "dependencies": { 218 | "depd": { 219 | "version": "2.0.0", 220 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 221 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" 222 | }, 223 | "http-errors": { 224 | "version": "2.0.0", 225 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 226 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 227 | "requires": { 228 | "depd": "2.0.0", 229 | "inherits": "2.0.4", 230 | "setprototypeof": "1.2.0", 231 | "statuses": "2.0.1", 232 | "toidentifier": "1.0.1" 233 | } 234 | }, 235 | "inherits": { 236 | "version": "2.0.4", 237 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 238 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 239 | }, 240 | "setprototypeof": { 241 | "version": "1.2.0", 242 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 243 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 244 | }, 245 | "statuses": { 246 | "version": "2.0.1", 247 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 248 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" 249 | }, 250 | "toidentifier": { 251 | "version": "1.0.1", 252 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 253 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" 254 | } 255 | } 256 | }, 257 | "express-http-proxy": { 258 | "version": "1.6.0", 259 | "resolved": "https://registry.npmjs.org/express-http-proxy/-/express-http-proxy-1.6.0.tgz", 260 | "integrity": "sha512-7Re6Lepg96NA2wiv7DC5csChAScn4K76/UgYnC71XiITCT1cgGTJUGK6GS0pIixudg3Fbx3Q6mmEW3mZv5tHFQ==", 261 | "requires": { 262 | "debug": "^3.0.1", 263 | "es6-promise": "^4.1.1", 264 | "raw-body": "^2.3.0" 265 | }, 266 | "dependencies": { 267 | "debug": { 268 | "version": "3.2.6", 269 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", 270 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", 271 | "requires": { 272 | "ms": "^2.1.1" 273 | } 274 | }, 275 | "ms": { 276 | "version": "2.1.2", 277 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 278 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 279 | } 280 | } 281 | }, 282 | "finalhandler": { 283 | "version": "1.2.0", 284 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", 285 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", 286 | "requires": { 287 | "debug": "2.6.9", 288 | "encodeurl": "~1.0.2", 289 | "escape-html": "~1.0.3", 290 | "on-finished": "2.4.1", 291 | "parseurl": "~1.3.3", 292 | "statuses": "2.0.1", 293 | "unpipe": "~1.0.0" 294 | }, 295 | "dependencies": { 296 | "statuses": { 297 | "version": "2.0.1", 298 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 299 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" 300 | } 301 | } 302 | }, 303 | "forwarded": { 304 | "version": "0.2.0", 305 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 306 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" 307 | }, 308 | "fresh": { 309 | "version": "0.5.2", 310 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 311 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" 312 | }, 313 | "function-bind": { 314 | "version": "1.1.1", 315 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 316 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 317 | }, 318 | "get-intrinsic": { 319 | "version": "1.1.3", 320 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", 321 | "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", 322 | "requires": { 323 | "function-bind": "^1.1.1", 324 | "has": "^1.0.3", 325 | "has-symbols": "^1.0.3" 326 | } 327 | }, 328 | "has": { 329 | "version": "1.0.3", 330 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 331 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 332 | "requires": { 333 | "function-bind": "^1.1.1" 334 | } 335 | }, 336 | "has-symbols": { 337 | "version": "1.0.3", 338 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 339 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" 340 | }, 341 | "http-errors": { 342 | "version": "1.7.2", 343 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", 344 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", 345 | "requires": { 346 | "depd": "~1.1.2", 347 | "inherits": "2.0.3", 348 | "setprototypeof": "1.1.1", 349 | "statuses": ">= 1.5.0 < 2", 350 | "toidentifier": "1.0.0" 351 | } 352 | }, 353 | "iconv-lite": { 354 | "version": "0.4.24", 355 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 356 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 357 | "requires": { 358 | "safer-buffer": ">= 2.1.2 < 3" 359 | } 360 | }, 361 | "inherits": { 362 | "version": "2.0.3", 363 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 364 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 365 | }, 366 | "ipaddr.js": { 367 | "version": "1.9.1", 368 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 369 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" 370 | }, 371 | "media-typer": { 372 | "version": "0.3.0", 373 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 374 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" 375 | }, 376 | "merge-descriptors": { 377 | "version": "1.0.1", 378 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 379 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" 380 | }, 381 | "methods": { 382 | "version": "1.1.2", 383 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 384 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" 385 | }, 386 | "mime": { 387 | "version": "1.6.0", 388 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 389 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 390 | }, 391 | "mime-db": { 392 | "version": "1.52.0", 393 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 394 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" 395 | }, 396 | "mime-types": { 397 | "version": "2.1.35", 398 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 399 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 400 | "requires": { 401 | "mime-db": "1.52.0" 402 | } 403 | }, 404 | "ms": { 405 | "version": "2.0.0", 406 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 407 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 408 | }, 409 | "negotiator": { 410 | "version": "0.6.3", 411 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 412 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" 413 | }, 414 | "object-inspect": { 415 | "version": "1.12.2", 416 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", 417 | "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" 418 | }, 419 | "on-finished": { 420 | "version": "2.4.1", 421 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 422 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 423 | "requires": { 424 | "ee-first": "1.1.1" 425 | } 426 | }, 427 | "parseurl": { 428 | "version": "1.3.3", 429 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 430 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 431 | }, 432 | "path-to-regexp": { 433 | "version": "0.1.7", 434 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 435 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" 436 | }, 437 | "proxy-addr": { 438 | "version": "2.0.7", 439 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 440 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 441 | "requires": { 442 | "forwarded": "0.2.0", 443 | "ipaddr.js": "1.9.1" 444 | } 445 | }, 446 | "qs": { 447 | "version": "6.11.0", 448 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", 449 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", 450 | "requires": { 451 | "side-channel": "^1.0.4" 452 | } 453 | }, 454 | "range-parser": { 455 | "version": "1.2.1", 456 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 457 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 458 | }, 459 | "raw-body": { 460 | "version": "2.4.0", 461 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", 462 | "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", 463 | "requires": { 464 | "bytes": "3.1.0", 465 | "http-errors": "1.7.2", 466 | "iconv-lite": "0.4.24", 467 | "unpipe": "1.0.0" 468 | } 469 | }, 470 | "safe-buffer": { 471 | "version": "5.2.1", 472 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 473 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 474 | }, 475 | "safer-buffer": { 476 | "version": "2.1.2", 477 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 478 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 479 | }, 480 | "send": { 481 | "version": "0.18.0", 482 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", 483 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", 484 | "requires": { 485 | "debug": "2.6.9", 486 | "depd": "2.0.0", 487 | "destroy": "1.2.0", 488 | "encodeurl": "~1.0.2", 489 | "escape-html": "~1.0.3", 490 | "etag": "~1.8.1", 491 | "fresh": "0.5.2", 492 | "http-errors": "2.0.0", 493 | "mime": "1.6.0", 494 | "ms": "2.1.3", 495 | "on-finished": "2.4.1", 496 | "range-parser": "~1.2.1", 497 | "statuses": "2.0.1" 498 | }, 499 | "dependencies": { 500 | "depd": { 501 | "version": "2.0.0", 502 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 503 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" 504 | }, 505 | "http-errors": { 506 | "version": "2.0.0", 507 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 508 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 509 | "requires": { 510 | "depd": "2.0.0", 511 | "inherits": "2.0.4", 512 | "setprototypeof": "1.2.0", 513 | "statuses": "2.0.1", 514 | "toidentifier": "1.0.1" 515 | } 516 | }, 517 | "inherits": { 518 | "version": "2.0.4", 519 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 520 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 521 | }, 522 | "ms": { 523 | "version": "2.1.3", 524 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 525 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 526 | }, 527 | "setprototypeof": { 528 | "version": "1.2.0", 529 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 530 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 531 | }, 532 | "statuses": { 533 | "version": "2.0.1", 534 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 535 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" 536 | }, 537 | "toidentifier": { 538 | "version": "1.0.1", 539 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 540 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" 541 | } 542 | } 543 | }, 544 | "serve-static": { 545 | "version": "1.15.0", 546 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", 547 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", 548 | "requires": { 549 | "encodeurl": "~1.0.2", 550 | "escape-html": "~1.0.3", 551 | "parseurl": "~1.3.3", 552 | "send": "0.18.0" 553 | } 554 | }, 555 | "setprototypeof": { 556 | "version": "1.1.1", 557 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 558 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 559 | }, 560 | "side-channel": { 561 | "version": "1.0.4", 562 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", 563 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", 564 | "requires": { 565 | "call-bind": "^1.0.0", 566 | "get-intrinsic": "^1.0.2", 567 | "object-inspect": "^1.9.0" 568 | } 569 | }, 570 | "statuses": { 571 | "version": "1.5.0", 572 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 573 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 574 | }, 575 | "toidentifier": { 576 | "version": "1.0.0", 577 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 578 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" 579 | }, 580 | "type-is": { 581 | "version": "1.6.18", 582 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 583 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 584 | "requires": { 585 | "media-typer": "0.3.0", 586 | "mime-types": "~2.1.24" 587 | } 588 | }, 589 | "unpipe": { 590 | "version": "1.0.0", 591 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 592 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 593 | }, 594 | "utils-merge": { 595 | "version": "1.0.1", 596 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 597 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" 598 | }, 599 | "vary": { 600 | "version": "1.1.2", 601 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 602 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" 603 | } 604 | } 605 | } 606 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "google-tag-manager-proxy", 3 | "version": "1.0.0", 4 | "description": "Proxying request to google analytics and tag manager", 5 | "module": "src/api.js", 6 | "scripts": { 7 | "test": "exit 0", 8 | "start": "node -r esm src/api.js", 9 | "mask": "node -r esm scripts.js mask", 10 | "unmask": "node -r esm scripts.js unmask" 11 | }, 12 | "keywords": [ 13 | "proxy" 14 | ], 15 | "author": "nikita.tk", 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/ZitRos/save-analytics-from-content-blockers.git" 19 | }, 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/ZitRos/save-analytics-from-content-blockers/issues" 23 | }, 24 | "homepage": "https://github.com/ZitRos/save-analytics-from-content-blockers#readme", 25 | "dependencies": { 26 | "esm": "^3.2.7", 27 | "express": "^4.18.2", 28 | "express-http-proxy": "^1.5.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | > ℹ️ This repository is no longer actively maintained, as focus has shifted to the SaaS version at [dataunlocker.com](https://www.dataunlocker.com). 2 | > However, contributions are still welcome and appreciated — we'll review them as time allows. 3 | 4 | # Google Tag Manager (Google Analytics) Proxy 5 | 6 | A proxy back end for Google Analytics / Google Tag Manager, which allows to avoid ad-blockers blocking client-side analytics tools. 7 | 8 | ``` 9 | docker pull zitros/analytics-saviour 10 | ``` 11 | 12 | **Note**: this repository is an open-sourced alternative (former PoC) of [**dataunlocker.com**](https://dataunlocker.com/). [DataUnlocker](https://dataunlocker.com/) is a fully-managed solution (SaaS) for fixing ad blockers' impact on the client-side analytics tools such as Google Analytics, Google Tag Manager, Segment, Facebook Pixel and any other client-side tools, also without code changes in your web application. [DataUnlocker](https://dataunlocker.com/) uses a very different (and a better) approach to what this repository offers. 13 | 14 | Originally introduced as an example in the article ["How to prevent your analytics data from being blocked by ad blockers"](https://www.freecodecamp.org/news/save-your-analytics-from-content-blockers-7ee08c6ec7ee/), this open-sourced application is now a complete stand-alone solution for Google Tag Manager and Google Analytics. 15 | 16 | ## How It Works 17 | 18 | Google Tag Manager (or plain Google Analytics) is a set of scripts used on the **front end** to track user actions (button clicks, page hits, device analytics, etc). Google's out-of-the-box solution works well, however, almost all ad-blocking software block Google tag manager / Google analytics by default. Hence, companies that are just on their start may loose a big 19 | portion of valuable information about their customers - how to they use the product? What do they like/dislike? Where do they stuck? And so on - an individual precision in analytics is crucial to understand the behavior of users. 20 | 21 | In order to solve ad-blocking issues, we have to introduce **a proxy which will forward front-end requests to Google domain through our own domain**. Also, we have to modify Google Tag Manager scripts "on-the-fly" to request our own domains instead of Google's ones, because all ad-blocking software block requests to domains (and some particular URLs!) which they have in their filters. Furthermore, some requests require additional data modifications which can't be done using standard proxying. 22 | 23 | The next diagram demonstrates the problem with Google Tag Manager / Google Analytics being blocked by ad blockers. 24 | 25 |

26 | Google Tag Manager Proxy - Without Proxy 27 |

28 | 29 | In general, all ad blocks work the same way: they block requests to Google Analytics servers and some URLs which match their blacklists. In order to avoid blocking Google analytics, all such requests must be proxied through URLs that aren't blacklisted. Furthermore, some URLs have to be masked in order for ad-blocker not to recognize the URL. 30 | 31 | Thus, this proxy service: 32 | 33 | 1. Works as a proxy for configured domains (see below). 34 | 2. Modifies the response when proxying scripts to replace Google domains with custom ones. 35 | 3. Modifies the response and replaces URLs containing blacklisted paths like `/google-analytics`. 36 | 4. Modifies proxied request to Google Measurement Protocol and overwrites user's IP address. 37 | 38 |

39 | Google Tag Manager Proxy - With Proxy 40 |

41 | 42 | This repository contains a NodeJS-based proxy server which does the smart proxying magic for you. All you need is to 43 | run this proxy server on your end and figure out how to combine it with your application. Read more on this below. 44 | 45 | Technically, NodeJS proxy API works as follows: 46 | 47 | 1. Request to `/` returns sample application (see [src/static-test/index.html](src/static-test/index.html)) if enabled (see config). 48 | 2. Request to `/domain-name-or-masked-name/*` proxies requests to `domain-name-or-masked-name` with path `*`. 49 | 3. You can run the application using `npm install && npm run start` and request [http://localhost/www.googletagmanager.com/gtag/js?id=GTM-1234567](http://localhost/www.googletagmanager.com/gtag/js?id=GTM-1234567) (replace `GTM-1234567` with your GTM tag). That's it! 50 | 51 | ## Prerequisites 52 | 53 | In order to enable analytics proxying, you have to perform some DevOps in your infrastructure. Assuming you're using microservices: 54 | 55 | 1. Run a dedicated back end (container) with proxy (NodeJS application / container in this repository) - see setup instructions below. 56 | 2. Create forwarding rule from your front end to hit this back end. 57 | 1. For instance, proxy all calls requesting `/gtm-proxy/*` to this back end. In this case you must also specify env variable `APP__STRIPPED_PATH=/gtm-proxy`. Ultimately, the request path `https://your-domain.com/gtm-proxy/www.google-analytics.com/analytics.js` should land as `/www.google-analytics.com/analytics.js` at the NodeJS proxy application/container (this repository), stripping `/gtm-proxy` from the URL. 58 | 2. It is important to use your own domain, as using centralized domains might one day appear at the ad-blocking databases. 59 | 3. **Modify your initial Google Tag Manager / Google Analytics script to request the proxied file** 60 | 1. Replace `https://www.googletagmanager.com/gtag/js?id=UA-123456-7` there to use `https://your-domain.com/gtm-proxy/www.googletagmanager.com/gtag/js?id=UA-123456-7` (or whatever path you've set up). Also, mask the URL by running `npm run mask ` in this repository so that ad-blockers won't block it right away. 61 | 2. For instance, if you run `npm run mask www.google-analytics.com/analytics.js`, you get this masked URL: `*(d3d3Lmdvb2dsZS1hbmFseXRpY3MuY29t)*/*(YW5hbHl0aWNzLmpz)*`. Use it in your script tag now: ``. 62 | 3. The [example](src/static-test/index.html) in this repository uses unmasked `/www.googletagmanager.com/gtm.js` (which is equivalent of `http://localhost/www.googletagmanager.com/gtm.js`). 63 | 4. Test the thing! 64 | 65 | **This to consider before implementing the solution**: 66 | 67 | 1. Your third-parties in Google Tag Manager can rate-limit your requests if you have many users, as now they're all going from the same IP address (your back end). If you've faced rate-limiting, please let me know by creating an issue in this repository! So far, we didn't. 68 | 2. Some third-parties like owox.com (yet) does not support IP overriding like Google Analytics does, meaning that all the users in your reports may appear on a map near your office/server. That's apparently their fault, but anyway you have to deal with this somehow. 69 | 3. Not all the third-parties are covered by the current solution. This repository is open for your PRs if you've found more third-parties that require proxying! 70 | 71 | ## Setup 72 | 73 | ### In Docker 74 | 75 | The [light Docker container](https://hub.docker.com/r/zitros/analytics-saviour) of 41.5MB is available and ready to be run in your infrastructure. 76 | 77 | ```bash 78 | docker pull zitros/analytics-saviour 79 | docker run -p 80:80 zitros/analytics-saviour 80 | # Now open http://localhost and check the proxy. 81 | ``` 82 | 83 | Available environment variables: 84 | 85 | ```bash 86 | # Below are the environment variables that can configure this proxy. 87 | # The proxy URL requested by the browser is expected to be 88 | # protocol://$APP__PROXY_DOMAIN$APP__STRIPPED_PATH/*(masked-url)*. 89 | 90 | APP__STRIPPED_PATH=/gtm-proxy 91 | # A prefix which has been stripped in the request path reaching analytics-saviour. 92 | # If your ingress/router/etc strips the prefix you are required to set this variable. 93 | # 94 | # On your website, most likely you'll decide to route analytics using f.e. `/gtm-proxy` 95 | # prefix. Your "entry URL" in case of Google Analytics case will be 96 | # example.com/gtm-proxy/*(d3d3Lmdvb2dsZS1hbmFseXRpY3MuY29t)*/*(YW5hbHl0aWNzLmpz)* 97 | # (masked example.com/gtm-proxy/www.google-analytics.com/analytics.js). 98 | # Your ingress/router/etc must strip the `/gtm-proxy` path and thus analytics-saviour 99 | # gets localhost/*(d3d3Lmdvb2dsZS1hbmFseXRpY3MuY29t)*/*(YW5hbHl0aWNzLmpz)* hit. 100 | # However, many scripts which are proxied reference external domains. Normally, these 101 | # domains are blocked by adblockers, but luckily analytics-saviour finds and replaces 102 | # those domains with your (request) domain and the appropriate path to handle again later. 103 | # THE ONLY THING it cannot figure out is which part of the URL has been stripped before 104 | # reaching analytics-saviour so that next front end requests land to the same prefixed path 105 | # on your domain e.g. example.com/gtm-proxy/*(d3d3Lmdvb2dsZS1hbmFseXRpY3MuY29t)*/collect?.. 106 | # Because of this, the path you strip must be explicitly provided. 107 | 108 | APP__PROXY_DOMAIN= 109 | # The domain name used as a proxy for analytics scripts (optional). 110 | # When set, the traffic will be proxied via this domain. 111 | # When not set, the current request domain (host) is used as a proxy domain. 112 | # This is useful to proxy traffic via f.e. subdomain or another domain, so that you don't need to 113 | # strip the prefix path. 114 | 115 | APP__ENV_NAME=local 116 | # APP__ENV_NAME=local or APP__ENV_NAME=test (default for local NodeJS app) 117 | # will display static content from `static-test`. 118 | # APP__ENV_NAME=prod is used inside the Docker container unless overwritten. 119 | 120 | APP__HOSTS_WHITELIST_REGEX="^(example\\.com|mysecondwebsite\\.com)$" 121 | # A JavaScript regular expression that the host must match. By default, it matches ANY HOST, MAKING 122 | # YOUR PROXY AVAILABLE TO ANYONE. Make sure you screen all special regexp characters here. Examples: 123 | # APP__HOSTS_WHITELIST_REGEX="^example\\.com$" (only the domain example.com is allowed to access the proxy) 124 | # APP__HOSTS_WHITELIST_REGEX="\\.example\\.com$" (only subdomains of example.com are allowed) 125 | # APP__HOSTS_WHITELIST_REGEX="(^|\\.)example\\.com$" (example.com and all its subdomains are allowed) 126 | # APP__HOSTS_WHITELIST_REGEX="^(example\\.com|mysecondwebsite\\.com)$" (multiple specified domains are allowed) 127 | ``` 128 | 129 | ### NodeJS Application 130 | 131 | To run the NodeJS application, simply clone the repository, navigate to its directory and run: 132 | 133 | ```bash 134 | npm install && npm run start 135 | ``` 136 | 137 | By default, this will run a proxy with a test front end on [http://localhost](http://localhost). You can get there and check how the request `http://localhost/www.google-analytics.com/collect?v=1&_v=j73&a=...` was proxied and that the ad-blocker didn't block the request. If the start is successful, after visiting [http://localhost](http://localhost) you'll see this: 138 | 139 | ``` 140 | Web server is listening on port 80 141 | Proxied: www.google-analytics.com/analytics.js 142 | Proxied: www.google-analytics.com/collect?v=1&_v=j73&a=531530768&t=pageview&_s=1&dl=http%3A%2F%2Flocalhost%2F&ul=ru&de=UTF-8&dt=Test&sd=24-bit&sr=1500x1000&vp=744x880&je=0&_u=AACAAEAB~&jid=&gjid=&cid=2E31579F-EE30-482F-9888-554A248A9495&tid=UA-98253329-1&_gid=1276054211.1554658225&z=1680756830&uip=1 143 | ``` 144 | 145 | Check the [static-test/index.html](static-test/index.html) file's code to see how to bind the proxied analytics to your front end. 146 | 147 | ### Proxy in Front of the Proxy 148 | 149 | Before the request hits this NodeJS app / container, you have to proxy/assign some useful headers to it (`host` and `x-real-ip` or `x-forwarded-for`). Below is the example of the minimal Nginx proxy configuration. 150 | 151 | ``` 152 | location /gtm-proxy/ { 153 | proxy_set_header Host $host; 154 | proxy_set_header x-real-ip $remote_addr; 155 | proxy_set_header x-forwarded-for $proxy_add_x_forwarded_for; 156 | proxy_pass http://app-address-running-in-your-infrastructure; 157 | } 158 | ``` 159 | 160 | ## Configuration 161 | 162 | You can configure which third-parties to proxy/replace and how to do it in the config file. Find the actual configuration in [config.js](config.js) file: 163 | 164 | ```javascript 165 | proxy: { 166 | domains: [ // These domains are replaced in any proxied response (they are prefixed with your domain) 167 | "adservice.google.com", 168 | "www.google-analytics.com", 169 | "www.googleadservices.com", 170 | "www.googletagmanager.com", 171 | "google-analytics.bi.owox.com", 172 | "stats.g.doubleclick.net", 173 | "ampcid.google.com", 174 | "www.google.%", 175 | "www.google.com" 176 | ], 177 | ipOverrides: { // IP override rules for domains (which query parameter to add overriding IP with X-Forwarded-For header) 178 | "www.google-analytics.com": { 179 | urlMatch: /\/collect/, 180 | queryParameterName: "uip" 181 | } 182 | }, 183 | maskPaths: [ // Which paths to mask in URLs. Can be regular expressions as strings 184 | "/google-analytics", 185 | "/r/collect", 186 | "/j/collect", 187 | "/pageread/conversion", 188 | "/pagead/conversion" 189 | ] 190 | } 191 | ``` 192 | 193 | ## License 194 | 195 | + [MIT](LICENSE) © [Nikita Savchenko](https://nikita.tk/developer) 196 | + [MIT](LICENSE) © [dataunlocker.com](https://dataunlocker.com) 197 | 198 | ## Contributions 199 | 200 | Any contributions are very welcome! 201 | -------------------------------------------------------------------------------- /scripts.js: -------------------------------------------------------------------------------- 1 | import { mask, unmask } from './src/modules/mask'; 2 | 3 | const command = process.argv[2]; 4 | const args = process.argv.slice(3); 5 | 6 | const f = ({ 7 | 8 | "mask": (path) => { 9 | 10 | if (typeof path !== 'string') { 11 | console.log('Please provide an argument to mask'); 12 | return; 13 | } 14 | console.log(`Masking ${path}...`); 15 | console.log('Masked URL:', mask(path)); 16 | }, 17 | 18 | "unmask": (path) => { 19 | if (typeof path !== 'string') { 20 | console.log('Please provide an argument to unmask'); 21 | return; 22 | } 23 | console.log('Unmasked URL:', unmask(path)); 24 | }, 25 | 26 | })[command] || (() => console.log(`No script ${command}!`)); 27 | 28 | f(...args); -------------------------------------------------------------------------------- /src/api.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import config from "../config"; 3 | import { info, error } from "./logger"; 4 | import { enableDefaultProxy } from "./proxy/configured-domains"; 5 | 6 | const brokenApp = {}; 7 | 8 | let isReady = false, 9 | app; 10 | 11 | init(); 12 | 13 | /** 14 | * Initializes an express app and resolves once the app is initialized. It is safe to call this function 15 | * multiple times: It will always resolve to the same ready-to-use app or throw. 16 | * @returns {Object} - Express app. 17 | */ 18 | export async function init () { 19 | 20 | if (isReady) { 21 | return app; 22 | } else if (app) { 23 | await ready(); 24 | return app; 25 | } 26 | 27 | try { 28 | 29 | app = express(); 30 | app.disable("x-powered-by"); 31 | app.use("/robots.txt", (_, res) => res.status(200).set("Content-Type", "text/plain").send( 32 | 'User-agent: *\nDisallow: /' 33 | )); 34 | 35 | enableDefaultProxy(app); 36 | 37 | if (config.isLocalEnv) { 38 | app.use("/", express.static(`${ __dirname }/../static-test`)); 39 | } else { 40 | app.use("/", (_, res) => res.status(200).set("Content-Type", "text/html").send( 41 | 'Mirror' + 42 | 'It works! Try requesting something like ' + 43 | 'www.google-analytics.com/analytics.js.' 44 | )); 45 | } 46 | 47 | app.use((err, _, res, next) => { // Express error handler 48 | if (res.headersSent) { 49 | return next(err); 50 | } 51 | error(err); 52 | return res.status(500).send({ error: "An error ocurred. Error info was logged." }); 53 | }); 54 | 55 | app.listen(config.httpPort, function onReady () { 56 | info(`Analytics proxy web server is listening on port ${ config.httpPort }`); 57 | isReady = true; 58 | }); 59 | 60 | await new Promise((resolve) => setTimeout(() => isReady && resolve(), 25)); 61 | 62 | } catch (e) { 63 | error(e); 64 | isReady = false; 65 | app = brokenApp; 66 | throw e; 67 | } 68 | 69 | return app; 70 | 71 | } 72 | 73 | export async function ready () { 74 | return new Promise((resolve, reject) => { 75 | const tm = setTimeout(() => reject(), 60000); 76 | const int = setInterval(() => { 77 | if (app === brokenApp) { 78 | clearInterval(int); 79 | clearTimeout(tm); 80 | return reject(new Error("Express app failed to start")); 81 | } 82 | if (isReady) { 83 | clearInterval(int); 84 | clearTimeout(tm); 85 | return resolve(); 86 | } 87 | }, 200); 88 | }); 89 | } -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | export function info (...args) { 2 | console.info(new Date().toISOString(), ...args); 3 | } 4 | 5 | export function error (...args) { 6 | console.error(new Date().toISOString(), ...args); 7 | } -------------------------------------------------------------------------------- /src/modules/mask.js: -------------------------------------------------------------------------------- 1 | export const mask = (matchedString) => matchedString.replace( 2 | /[^\/\\]+/g, 3 | part => `*(${ encodeURIComponent(Buffer.from(part).toString("base64").replace(/==?$/, "")) })*` 4 | ); 5 | export const unmask = (string) => string.replace(/\*\(([^\)]+)\)\*/g, (_, masked) => 6 | Buffer.from(decodeURIComponent(masked), "base64").toString() 7 | ); -------------------------------------------------------------------------------- /src/modules/proxy.js: -------------------------------------------------------------------------------- 1 | import proxy from "express-http-proxy"; 2 | import config from "../../config"; 3 | import url from "url"; 4 | import { mask, unmask } from "./mask"; 5 | import { info } from "../logger"; 6 | 7 | const proxyDomains = new Set(config.proxy.domains); 8 | const maskPaths = new Set(config.proxy.maskPaths); 9 | 10 | const replaceDomainRegex = new RegExp( 11 | Array.from(proxyDomains).join("|").replace(/\./g, "\\."), 12 | "gi" 13 | ); 14 | const maskRegex = new RegExp( 15 | Array.from(maskPaths).join("|").replace(/\//g, "\\/"), 16 | "gi" 17 | ); 18 | const replaceDomainsForHost = (host) => (match, pos, str) => { 19 | const escapedSlashes = str[pos - 2] === "\\" && str[pos - 2] === "/" // wat? 20 | const r = `${ 21 | escapedSlashes 22 | ? (config.proxyDomain || host).replace(/\//g, "\\/") + "\\" 23 | : (config.proxyDomain || host) 24 | }${ config.strippedPath }/${ mask(match) }`; 25 | return r; 26 | }; 27 | 28 | export function createDefaultProxy (targetDomain, proxyOptionsOverride = {}) { 29 | let servername = targetDomain.replace(/^https?\:\/\//, ""); 30 | return proxy(targetDomain, { 31 | proxyReqOptDecorator: (proxyRequest, originalRequest) => { 32 | proxyRequest.headers["accept-encoding"] = "identity"; 33 | if (proxyRequest.headers["authorization"]) { 34 | delete proxyRequest.headers["authorization"]; 35 | } 36 | return proxyOptionsOverride["proxyReqOptDecorator"] instanceof Function 37 | ? proxyOptionsOverride["proxyReqOptDecorator"](proxyRequest, originalRequest) 38 | : proxyRequest; 39 | }, 40 | userResHeaderDecorator: (proxyHeaders, origninalHeaders) => { 41 | const { headers: { host } } = origninalHeaders; 42 | if (proxyHeaders.location) { 43 | if (config.proxy.specialContentReplace[servername]) { // Keep before other replacements 44 | const replacements = config.proxy.specialContentReplace[servername]; 45 | for (const r of replacements) { 46 | proxyHeaders.location = proxyHeaders.location.replace(r.regex, r.replace); 47 | } 48 | } 49 | proxyHeaders.location = proxyHeaders.location 50 | .replace(replaceDomainRegex, replaceDomainsForHost(host)) 51 | .replace(maskRegex, match => mask(match)); 52 | } 53 | return proxyHeaders; 54 | }, 55 | userResDecorator: (_, proxyResData, { headers: { host } }) => { 56 | if (_.req.res && _.req.res.client && _.req.res.client.servername) { 57 | servername = _.req.res.client.servername; 58 | } 59 | let pre = proxyResData.toString().replace(replaceDomainRegex, replaceDomainsForHost(host)); 60 | if (config.proxy.specialContentReplace[servername]) { 61 | const replacements = config.proxy.specialContentReplace[servername]; 62 | for (const r of replacements) { 63 | pre = pre.replace(r.regex, r.replace instanceof Function ? r.replace({ host }) : r.replace); 64 | } 65 | } 66 | pre = pre.replace(maskRegex, (match) => { // Mask configured URLs 67 | const r = /\\|\//.test(match[0]) 68 | ? match[0] + mask(match.slice(1)) 69 | : mask(match); 70 | return r; 71 | }); 72 | return pre; 73 | }, 74 | proxyReqPathResolver: (req) => { 75 | 76 | // Unmask URL parts that were masked 77 | let unmasked = unmask(req.url); 78 | 79 | // For Google measurement protocol hits, overwrite user's IP address in order for Google to determine location 80 | if ( 81 | config.proxy.ipOverrides[servername] 82 | && config.proxy.ipOverrides[servername].urlMatch instanceof RegExp 83 | && config.proxy.ipOverrides[servername].queryParameterName 84 | && config.proxy.ipOverrides[servername].urlMatch.test(unmasked) 85 | ) { 86 | 87 | const parsedUrl = url.parse(unmasked); 88 | const overwrittenIp = req.headers["x-forwarded-for"] || req.headers["x-real-ip"]; 89 | const clientIp = overwrittenIp 90 | ? overwrittenIp.split(/,\s?/g)[0] 91 | : req.connection.remoteAddress.split(":").pop(); 92 | const encodedIp = encodeURIComponent(clientIp); 93 | 94 | unmasked = parsedUrl.path + `${ 95 | parsedUrl.search ? "&" : "?" 96 | }${ 97 | config.proxy.ipOverrides[servername].queryParameterName instanceof Array 98 | ? config.proxy.ipOverrides[servername].queryParameterName.map(name => `${ name }=${ encodedIp }`).join("&") 99 | : `${ config.proxy.ipOverrides[servername].queryParameterName }=${ encodedIp }` 100 | }`; 101 | 102 | } 103 | 104 | // Apply overrides 105 | const finalPath = proxyOptionsOverride["proxyReqPathResolver"] instanceof Function 106 | ? proxyOptionsOverride["proxyReqPathResolver"](req, unmasked) 107 | : unmasked; 108 | 109 | info(`proxied: ${ servername }${ finalPath }`); 110 | 111 | return finalPath; 112 | 113 | } 114 | }); 115 | } -------------------------------------------------------------------------------- /src/proxy/configured-domains.js: -------------------------------------------------------------------------------- 1 | import { createDefaultProxy } from "../modules/proxy"; 2 | import config from "../../config"; 3 | import { unmask } from "../modules/mask"; 4 | import { info } from "../logger"; 5 | 6 | const domains = new Set(config.proxy.domains); 7 | const proxies = new Map(Array.from(domains).map((domain) => { 8 | return [domain, createDefaultProxy(`https://${ domain }`, { 9 | proxyReqPathResolver: (_, path) => path.replace(`/${ domain }`, "") // Strip domain from URL 10 | })]; 11 | })); 12 | 13 | export function enableDefaultProxy (expressApp) { 14 | expressApp.use("/", (req, res, next) => { 15 | if (!req.headers || !req.headers.host || !config.hostsWhitelistRegex.test(req.headers.host)) { 16 | info(`FORBIDDEN: proxy request from host "${req.headers.host}" was canceled as it doesn't match with ${config.hostsWhitelistRegex}.`); 17 | return res.status(403).send({ 18 | error: `Requests from host ${req.headers.host} are restricted.` 19 | }); 20 | } 21 | const domain = unmask(req.url.split(/\//g)[1]); 22 | return domains.has(domain) 23 | ? proxies.get(domain)(req, res, next) // Use proxy for configured domains 24 | : req.url === "/" 25 | ? next() 26 | : res.status(404).send({ 27 | error: `Proxy error: domain "${domain}" is not proxied. Requested URL: ${req.url}` 28 | }); 29 | }); 30 | } -------------------------------------------------------------------------------- /static-test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test 5 | 6 | 7 | 8 | 9 | 10 | 17 | 18 | 19 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 38 | 39 | 40 | This is Google Tag Manager test. You are being watched. Possibly.
41 | (check the developer tools - network) (check the code of this page) 42 | 43 | 44 | 45 | --------------------------------------------------------------------------------