diff --git a/LICENSE b/LICENSE index 2e0a0fa..ecf58dc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017 Nikita +Copyright (c) 2017-2019 Nikita Savchenko (https://nikita.tk) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md deleted file mode 100644 index b4ab986..0000000 --- a/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# Save Analytics From Content Blockers - -This is a repository with demo application code for [my Medium article "Save Your Analytics from Content Blockers"](https://medium.com/@zitro/save-your-analytics-from-content-blockers-7ee08c6ec7ee). - -You can use modified analytics script directly from GitHub pages (until it appears in ad-blockers filters :smile:): - -```html - - -``` - -Running the Example -------------------- - -Having the latest [NodeJS and NPM](https://nodejs.org) installed, run the server with the following command: - -```bash -npm install -npm run start -``` - -Then go to the printed link. To explore the example completely, you need **static IP address** at least. - -UPDATE IS COMING ----------------- - -As of 3/14/2019, I've developed a more robust solution for Google Analytics / Google Tag Manager, which also comes with URL masking and advanced on-the-fly transformations for everything that may've been blocked by ad blockers. I'll give it a go soon. You can subscribe to [my GitHub](https://github.com/ZitRos) / [Medium](https://medium.com/@zitro) to stay tuned. diff --git a/analytics.js b/analytics.js deleted file mode 100644 index d743f71..0000000 --- a/analytics.js +++ /dev/null @@ -1,46 +0,0 @@ -(function(){var $c=function(a){this.w=a||[]};$c.prototype.set=function(a){this.w[a]=!0};$c.prototype.encode=function(){for(var a=[],b=0;b\x3c/script>')):(c=M.createElement("script"), -c.type="text/javascript",c.async=!0,c.src=a,d&&(c.onload=d),b&&(c.id=b),a=M.getElementsByTagName("script")[0],a.parentNode.insertBefore(c,a)))},Ud=function(){return"https:"==M.location.protocol},E=function(a,b){return(a=a.match("(?:&|#|\\?)"+K(b).replace(/([.*+?^=!:${}()|\[\]\/\\])/g,"\\$1")+"=([^&#]*)"))&&2==a.length?a[1]:""},xa=function(){var a=""+M.location.hostname;return 0==a.indexOf("www.")?a.substring(4):a},ya=function(a){var b=M.referrer;if(/^https?:\/\//i.test(b)){if(a)return b;a="//"+M.location.hostname; -var c=b.indexOf(a);if(5==c||6==c)if(a=b.charAt(c+a.length),"/"==a||"?"==a||""==a||":"==a)return;return b}},za=function(a,b){if(1==b.length&&null!=b[0]&&"object"===typeof b[0])return b[0];for(var c={},d=Math.min(a.length+1,b.length),e=0;e=b.length)wc(a,b,c);else if(8192>=b.length)x(a,b,c)||wd(a,b,c)||wc(a,b,c);else throw ge("len",b.length),new Da(b.length);},wc=function(a,b,c){var d=ta(a+"?"+b);d.onload=d.onerror=function(){d.onload=null;d.onerror=null;c()}},wd=function(a,b,c){var d=O.XMLHttpRequest;if(!d)return!1;var e=new d;if(!("withCredentials"in e))return!1; -e.open("POST",a,!0);e.withCredentials=!0;e.setRequestHeader("Content-Type","text/plain");e.onreadystatechange=function(){4==e.readyState&&(c(),e=null)};e.send(b);return!0},x=function(a,b,c){return O.navigator.sendBeacon?O.navigator.sendBeacon(a,b)?(c(),!0):!1:!1},ge=function(a,b,c){1<=100*Math.random()||G("?")||(a=["t=error","_e="+a,"_v=j52","sr=1"],b&&a.push("_f="+b),c&&a.push("_m="+K(c.substring(0,100))),a.push("aip=1"),a.push("z="+hd()),wc(oc()+"/collect",a.join("&"),ua))};var h=function(a){var b=O.gaData=O.gaData||{};return b[a]=b[a]||{}};var Ha=function(){this.M=[]};Ha.prototype.add=function(a){this.M.push(a)};Ha.prototype.D=function(a){try{for(var b=0;b=100*R(a,Ka))throw"abort";}function Ma(a){if(G(P(a,Na)))throw"abort";}function Oa(){var a=M.location.protocol;if("http:"!=a&&"https:"!=a)throw"abort";} -function Pa(a){try{O.navigator.sendBeacon?J(42):O.XMLHttpRequest&&"withCredentials"in new O.XMLHttpRequest&&J(40)}catch(c){}a.set(ld,Td(a),!0);a.set(Ac,R(a,Ac)+1);var b=[];Qa.map(function(c,d){d.F&&(c=a.get(c),void 0!=c&&c!=d.defaultValue&&("boolean"==typeof c&&(c*=1),b.push(d.F+"="+K(""+c))))});b.push("z="+Bd());a.set(Ra,b.join("&"),!0)} -function Sa(a){var b=P(a,gd)||oc()+"/collect",c=P(a,fa);!c&&a.get(Vd)&&(c="beacon");if(c){var d=P(a,Ra),e=a.get(Ia),e=e||ua;"image"==c?wc(b,d,e):"xhr"==c&&wd(b,d,e)||"beacon"==c&&x(b,d,e)||ba(b,d,e)}else ba(b,P(a,Ra),a.get(Ia));b=a.get(Na);b=h(b);c=b.hitcount;b.hitcount=c?c+1:1;b=a.get(Na);delete h(b).pending_experiments;a.set(Ia,ua,!0)} -function Hc(a){(O.gaData=O.gaData||{}).expId&&a.set(Nc,(O.gaData=O.gaData||{}).expId);(O.gaData=O.gaData||{}).expVar&&a.set(Oc,(O.gaData=O.gaData||{}).expVar);var b;var c=a.get(Na);if(c=h(c).pending_experiments){var d=[];for(b in c)c.hasOwnProperty(b)&&c[b]&&d.push(encodeURIComponent(b)+"."+encodeURIComponent(c[b]));b=d.join("!")}else b=void 0;b&&a.set(m,b,!0)}function cd(){if(O.navigator&&"preview"==O.navigator.loadPurpose)throw"abort";} -function yd(a){var b=O.gaDevIds;ka(b)&&0!=b.length&&a.set("&did",b.join(","),!0)}function vb(a){if(!a.get(Na))throw"abort";};var hd=function(){return Math.round(2147483647*Math.random())},Bd=function(){try{var a=new Uint32Array(1);O.crypto.getRandomValues(a);return a[0]&2147483647}catch(b){return hd()}};function Ta(a){var b=R(a,Ua);500<=b&&J(15);var c=P(a,Va);if("transaction"!=c&&"item"!=c){var c=R(a,Wa),d=(new Date).getTime(),e=R(a,Xa);0==e&&a.set(Xa,d);e=Math.round(2*(d-e)/1E3);0=c)throw"abort";a.set(Wa,--c)}a.set(Ua,++b)};var Ya=function(){this.data=new ee},Qa=new ee,Za=[];Ya.prototype.get=function(a){var b=$a(a),c=this.data.get(a);b&&void 0==c&&(c=ea(b.defaultValue)?b.defaultValue():b.defaultValue);return b&&b.Z?b.Z(this,a,c):c};var P=function(a,b){a=a.get(b);return void 0==a?"":""+a},R=function(a,b){a=a.get(b);return void 0==a||""===a?0:1*a};Ya.prototype.set=function(a,b,c){if(a)if("object"==typeof a)for(var d in a)a.hasOwnProperty(d)&&ab(this,d,a[d],c);else ab(this,a,b,c)}; -var ab=function(a,b,c,d){if(void 0!=c)switch(b){case Na:wb.test(c)}var e=$a(b);e&&e.o?e.o(a,b,c,d):a.data.set(b,c,d)},bb=function(a,b,c,d,e){this.name=a;this.F=b;this.Z=d;this.o=e;this.defaultValue=c},$a=function(a){var b=Qa.get(a);if(!b)for(var c=0;c=b?!1:!0},gc=function(a){var b={};if(Ec(b)||Fc(b)){var c=b[Eb];void 0==c||Infinity==c||isNaN(c)||(0c)a[b]=void 0},Fd=function(a){return function(b){if("pageview"==b.get(Va)&&!a.I){a.I=!0;var c= -aa(b);b=0b.length)){for(var c=[], -d=0;d=a&&d.push({hash:ca[0],R:e[g],O:ca})}if(0!=d.length)return 1==d.length?d[0]:Zc(b,d)||Zc(c,d)||Zc(null,d)||d[0]}function Zc(a,b){var c;null==a?c=a=1:(c=La(a),a=La(D(a,".")?a.substring(1):"."+a));for(var d=0;d=ca[0]||0>=ca[1]?"":ca.join("x");a.set(rb,c);a.set(tb,fc());a.set(ob,M.characterSet||M.charset);a.set(sb,b&&"function"===typeof b.javaEnabled&&b.javaEnabled()||!1);a.set(nb,(b&&(b.language||b.browserLanguage)||"").toLowerCase());if(d&&a.get(cc)&&(b=M.location.hash)){b=b.split(/[?&#]+/);d=[];for(c=0;carguments.length)){var b,c;"string"===typeof arguments[0]?(b=arguments[0],c=[].slice.call(arguments,1)):(b=arguments[0]&&arguments[0][Va],c=arguments);b&&(c=za(qc[b]||[],c),c[Va]=b,this.b.set(c,void 0,!0),this.filters.D(this.b),this.b.data.m={},Ed(this.ra,this.b)&&da(this.b.get(Na)))}};pc.prototype.ma=function(a,b){var c=this;u(a,c,b)||(v(a,function(){u(a,c,b)}),y(String(c.get(V)),a,void 0,b,!0))};var rc=function(a){if("prerender"==M.visibilityState)return!1;a();return!0},z=function(a){if(!rc(a)){J(16);var b=!1,c=function(){if(!b&&rc(a)){b=!0;var d=c,e=M;e.removeEventListener?e.removeEventListener("visibilitychange",d,!1):e.detachEvent&&e.detachEvent("onvisibilitychange",d)}};L(M,"visibilitychange",c)}};var td=/^(?:(\w+)\.)?(?:(\w+):)?(\w+)$/,sc=function(a){if(ea(a[0]))this.u=a[0];else{var b=td.exec(a[0]);null!=b&&4==b.length&&(this.c=b[1]||"t0",this.K=b[2]||"",this.C=b[3],this.a=[].slice.call(a,1),this.K||(this.A="create"==this.C,this.i="require"==this.C,this.g="provide"==this.C,this.ba="remove"==this.C),this.i&&(3<=this.a.length?(this.X=this.a[1],this.W=this.a[2]):this.a[1]&&(qa(this.a[1])?this.X=this.a[1]:this.W=this.a[1])));b=a[1];a=a[2];if(!this.C)throw"abort";if(this.i&&(!qa(b)||""==b))throw"abort"; -if(this.g&&(!qa(b)||""==b||!ea(a)))throw"abort";if(ud(this.c)||ud(this.K))throw"abort";if(this.g&&"t0"!=this.c)throw"abort";}};function ud(a){return 0<=a.indexOf(".")||0<=a.indexOf(":")};var Yd,Zd,$d,A;Yd=new ee;$d=new ee;A=new ee;Zd={ec:45,ecommerce:46,linkid:47}; -var u=function(a,b,c){b==N||b.get(V);var d=Yd.get(a);if(!ea(d))return!1;b.plugins_=b.plugins_||new ee;if(b.plugins_.get(a))return!0;b.plugins_.set(a,new d(b,c||{}));return!0},y=function(a,b,c,d,e){if(!ea(Yd.get(b))&&!$d.get(b)){Zd.hasOwnProperty(b)&&J(Zd[b]);if(p.test(b)){J(52);a=N.j(a);if(!a)return!0;c=d||{};d={id:b,B:c.dataLayer||"dataLayer",ia:!!a.get("anonymizeIp"),sync:e,G:!1};a.get(">m")==b&&(d.G=!0);var g=String(a.get("name"));"t0"!=g&&(d.target=g);G(String(a.get("trackingId")))||(d.ja=String(a.get(Q)), -d.ka=Number(a.get(n)),c=c.palindrome?r:q,c=(c=M.cookie.replace(/^|(; +)/g,";").match(c))?c.sort().join("").substring(1):void 0,d.la=c,d.qa=E(a.b.get(kb)||"","gclid"));a=d.B;c=(new Date).getTime();O[a]=O[a]||[];c={"gtm.start":c};e||(c.event="gtm.js");O[a].push(c);c=t(d)}!c&&Zd.hasOwnProperty(b)?(J(39),c=b+".js"):J(43);c&&(c&&0<=c.indexOf("/")||(c=(Ba||Ud()?"https:":"http:")+"//"+location.host+"/analytics/plugins/ua/"+c),d=ae(c),a=d.protocol,c=M.location.protocol,("https:"==a||a==c||("http:"!=a?0:"http:"== -c))&&B(d)&&(wa(d.url,void 0,e),$d.set(b,!0)))}},v=function(a,b){var c=A.get(a)||[];c.push(b);A.set(a,c)},C=function(a,b){Yd.set(a,b);b=A.get(a)||[];for(var c=0;ca.split("/")[0].indexOf(":")&&(a=ca+e[2].substring(0, -e[2].lastIndexOf("/"))+"/"+a);c.href=a;d=b(c);return{protocol:(c.protocol||"").toLowerCase(),host:d[0],port:d[1],path:d[2],query:c.search||"",url:a||""}};var Z={ga:function(){Z.f=[]}};Z.ga();Z.D=function(a){var b=Z.J.apply(Z,arguments),b=Z.f.concat(b);for(Z.f=[];0c;c++){var d=b[c].src;if(d&&0==d.indexOf("https://"+location.host+"/analytics/analytics")){J(33); -b=!0;break a}}b=!1}b&&(Ba=!0)}Ud()||Ba||!Ed(new Od(1E4))||(J(36),Ba=!0);(O.gaplugins=O.gaplugins||{}).Linker=Dc;b=Dc.prototype;C("linker",Dc);X("decorate",b,b.ca,20);X("autoLink",b,b.S,25);C("displayfeatures",fd);C("adfeatures",fd);a=a&&a.q;ka(a)?Z.D.apply(N,a):J(50)}};N.da=function(){for(var a=N.getAll(),b=0;b>21:b;return b};})(window); diff --git a/config.js b/config.js new file mode 100644 index 0000000..90b176d --- /dev/null +++ b/config.js @@ -0,0 +1,86 @@ +const env = process.env.APP__ENV_NAME || "local"; +const isLocal = env === "local" || env === "test"; +const proxyDomain = process.env.PROXY_DOMAIN || "localhost"; + +export default { + isLocalEnv: isLocal, + httpPort: process.env.PORT || 80, + proxyDomain: proxyDomain, // Your domain + proxy: { // Proxy configuration is here + domains: [ // These domains are replaced in any proxied response (including scripts, URLs and redirects) + "adservice.google.com", + "www.google-analytics.com", + "www.googleadservices.com", + "www.googletagmanager.com", + "google-analytics.bi.owox.com", + "googleads.g.doubleclick.net", + "stats.g.doubleclick.net", + "ampcid.google.com", + "www.google.%", + "www.google.com", + "bat.bing.com", + "static.hotjar.com", + "trackcmp.net", + "connect.facebook.net", + "www.facebook.com", + "rum-static.pingdom.net", + "s.adroll.com", + "d.adroll.com", + "bid.g.doubleclick.net", + "rum-collector-2.pingdom.net", + "script.hotjar.com", + "vars.hotjar.com", + "pixel.advertising.com", + "dsum-sec.casalemedia.com", + "pixel.rubiconproject.com", + "sync.outbrain.com", + "simage2.pubmatic.com", + "trc.taboola.com", + "eb2.3lift.com", + "ads.yahoo.com", + "x.bidswitch.net", + "ib.adnxs.com", + "idsync.rlcdn.com", + "us-u.openx.net", + "cm.g.doubleclick.net" + ], + specialContentReplace: { // Special regex rules for domains + "www.googletagmanager.com": [ + { + regex: /"https\:\/\/s","http:\/\/a","\.adroll\.com/, + replace: `"https://${ proxyDomain }/s","http://${ proxyDomain }/a",".adroll.com` + } + ], + "eb2.3lift.com": [ + { // Because eb2.3lift.com/xuid?mid=_&xuid=_&dongle=_ redirects to "/xuid" which doesn't exists + regex: /^\/xuid/, + replace: "https://eb2.3lift.com/xuid" // eb2.3lift.com is replaced then again with the correct proxy + } + ] + }, + ipOverrides: { // IP override rules for domains + "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. + urlMatch: /\/collect/, + queryParameterName: ["uip", "device.ip"] + }, + "www.google-analytics.com": { + urlMatch: /\/collect/, + queryParameterName: "uip" + } + }, + maskPaths: [ // Paths which are masked in URLs and redirects in order to avoid firing ad-blocking rules + "/google-analytics", + "/r/collect", + "/j/collect", + "/pageread/conversion", + "/pagead/conversion", + "/googleads", + "/prum", + "/beacon", + "/pixel", + "/AdServer", + "/ads/", + "openx\\." + ], + } +}; \ No newline at end of file diff --git a/index.html b/index.html deleted file mode 100644 index 59e5939..0000000 --- a/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - Google Analytics Content-Blockers-Free Test - - - - - Hello! Now check the Network tab in Developer Tools ;) - - \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a44a286 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,393 @@ +{ + "name": "google-tag-manager-proxy", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "requires": { + "mime-types": "~2.1.18", + "negotiator": "0.6.1" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "body-parser": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", + "requires": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "~1.6.3", + "iconv-lite": "0.4.23", + "on-finished": "~2.3.0", + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "~1.6.16" + } + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "es6-promise": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.6.tgz", + "integrity": "sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "esm": { + "version": "3.2.22", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.22.tgz", + "integrity": "sha512-z8YG7U44L82j1XrdEJcqZOLUnjxco8pO453gKOlaMD1/md1n/5QrscAmYG+oKUspsmDLuBFZrpbxI6aQ67yRxA==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "express": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", + "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", + "requires": { + "accepts": "~1.3.5", + "array-flatten": "1.1.1", + "body-parser": "1.18.3", + "content-disposition": "0.5.2", + "content-type": "~1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.1.1", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.4", + "qs": "6.5.2", + "range-parser": "~1.2.0", + "safe-buffer": "5.1.2", + "send": "0.16.2", + "serve-static": "1.13.2", + "setprototypeof": "1.1.0", + "statuses": "~1.4.0", + "type-is": "~1.6.16", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "express-http-proxy": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/express-http-proxy/-/express-http-proxy-1.5.1.tgz", + "integrity": "sha512-k1RdysZWZ8wdPnsLa4iyrrYyUFih/sYKkn6WfkU/q5A8eUdh3l+oXhrRuQmEYEsZmiexVvpiOCkogl03jYfcbg==", + "requires": { + "debug": "^3.0.1", + "es6-promise": "^4.1.1", + "raw-body": "^2.3.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "finalhandler": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.4.0", + "unpipe": "~1.0.0" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "iconv-lite": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ipaddr.js": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", + "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" + }, + "mime-db": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", + "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" + }, + "mime-types": { + "version": "2.1.22", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", + "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", + "requires": { + "mime-db": "~1.38.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "proxy-addr": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", + "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.8.0" + } + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" + }, + "raw-body": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", + "unpipe": "1.0.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + } + }, + "serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", + "send": "0.16.2" + } + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + }, + "type-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.18" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + } + } +} diff --git a/package.json b/package.json index 66ddd9e..e19125c 100644 --- a/package.json +++ b/package.json @@ -1,25 +1,28 @@ { - "name": "save-analytics-from-content-blockers", + "name": "google-tag-manager-proxy", "version": "1.0.0", - "description": "A repository with code sample for the Medium article (will be released soon).", - "main": "server.js", - "dependencies": { - "express": "^4.15.2", - "express-http-proxy": "^1.0.1" - }, - "devDependencies": {}, + "description": "Proxying request to google analytics and tag manager", + "module": "src/api.js", "scripts": { - "test": "echo \"No test specified\" && exit 0", - "start": "node server.js" + "test": "exit 0", + "start": "node -r esm src/api.js" }, + "keywords": [ + "proxy" + ], + "author": "nikita.tk", "repository": { "type": "git", "url": "git+https://github.com/ZitRos/save-analytics-from-content-blockers.git" }, - "author": "nikita.tk", - "license": "ISC", + "license": "MIT", "bugs": { "url": "https://github.com/ZitRos/save-analytics-from-content-blockers/issues" }, - "homepage": "https://github.com/ZitRos/save-analytics-from-content-blockers#readme" + "homepage": "https://github.com/ZitRos/save-analytics-from-content-blockers#readme", + "dependencies": { + "esm": "^3.2.7", + "express": "^4.16.4", + "express-http-proxy": "^1.5.1" + } } diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..9edbc97 --- /dev/null +++ b/readme.md @@ -0,0 +1,104 @@ +# Google Tag Manager (Google Analytics) Proxy + +Back end for proxying Google Analytics / Google Tag Manager stuff, which primarily enables ad-blocking avoidance. + +> Save your analytics from content blockers! +> +> This is the repository with the application code for [my Medium article "Save Your Analytics from Content Blockers"](https://medium.com/@zitro/save-your-analytics-from-content-blockers-7ee08c6ec7ee), which allows you to launch a proxy of Google Tag Manager / Google Analytics stuff avoiding ad-blocking. + +## How Does It Work + +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 +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. + +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. + +The next diagram demonstrates the problem with Google Tag Manager / Google Analytics being blocked by ad blockers. + +![Google Tag Manager Proxy - No proxy](https://user-images.githubusercontent.com/4989256/55686876-52fc0200-596f-11e9-8d17-399b97486a02.png) + +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. + +Thus, this proxy service: + +1. Works as a proxy for configured domains (see below). +2. Modifies the response when proxying scripts to replace Google domains with custom ones. +3. Modifies the response and replaces URLs containing blacklisted paths like `/google-analytics`. +4. Modifies proxied request to Google Measurement Protocol and overwrites user's IP address. + +![Google Tag Manager Proxy - With Proxy](https://user-images.githubusercontent.com/4989256/55686879-542d2f00-596f-11e9-8313-5837af75cc2e.png) + +## Prerequisites + +In order to enable analytics proxying, you need two things: + +1. A dedicated back end to launch your own proxy (NodeJS application in this repository). +2. To forward a particular path (for example, `/gtm-proxy*`) within your domain to this back end (stripping the path itself). Ultimately, for example, the request path `https://your-domain.com/gtm-proxy/www.google-analytics.com/analytics.js` should resolve to `/www.google-analytics.com/analytics.js` request to the NodeJS proxy application (this repository). +3. Modify your front end Google Tag Manager / Google Analytics script to request the proxied file - you're all set! + +**This to consider before implementing the solution**: + +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! +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. +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! + +## Setup + +To run the NodeJS application, simply clone the repository, navigate to its directory and run: + +```bash +npm install && npm run start +``` + +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: + +``` +Web server is listening on port 80 +Proxied: www.google-analytics.com/analytics.js +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 +``` + +Check the [test-static/index.html](test-static/index.html) file's code to see how to bind the proxied analytics to your front end. + +## Configuration + +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: + +```javascript + proxy: { + domains: [ // These domains are replaced in any proxied response (they are prefixed with your domain) + "adservice.google.com", + "www.google-analytics.com", + "www.googleadservices.com", + "www.googletagmanager.com", + "google-analytics.bi.owox.com", + "stats.g.doubleclick.net", + "ampcid.google.com", + "www.google.%", + "www.google.com" + ], + ipOverrides: { // IP override rules for domains (which query parameter to add overriding IP with X-Forwarded-For header) + "www.google-analytics.com": { + urlMatch: /\/collect/, + queryParameterName: "uip" + } + }, + maskPaths: [ // Which paths to mask in URLs. Can be regular expressions as strings + "/google-analytics", + "/r/collect", + "/j/collect", + "/pageread/conversion", + "/pagead/conversion" + ] + } +``` + +Note the environment variables you can set or replace. + +## License + +[MIT](LICENSE) © [Nikita Savchenko](https://nikita.tk) + +## Contributions + +Any contributions are very welcome! \ No newline at end of file diff --git a/server.js b/server.js deleted file mode 100644 index 7844d39..0000000 --- a/server.js +++ /dev/null @@ -1,22 +0,0 @@ -var express = require("express"), proxy = require("express-http-proxy"), app = express(); - -app.use(express.static(__dirname)); // serve files in current directory - -function getIpFromReq (req) { // get the client's IP address from request - var bareIP = ":" + ((req.connection.socket && req.connection.socket.remoteAddress) - || req.headers["x-forwarded-for"] || req.connection.remoteAddress || ""); - return (bareIP.match(/:([^:]+)$/) || [])[1] || "127.0.0.1"; -} - -// proxying requests from /analytics to www.google-analytics.com. -app.use("/analytics", proxy("www.google-analytics.com", { - proxyReqPathResolver: function (req) { - var url = req.url + (req.url.indexOf("?") === -1 ? "?" : "&") - + "uip=" + encodeURIComponent(getIpFromReq(req)); - console.log("Proxying www.google-analytics.com" + url); - return url; - } -})); - -app.listen(1280); -console.log("Web application ready on http://localhost:1280"); \ No newline at end of file diff --git a/src/api.js b/src/api.js new file mode 100644 index 0000000..ffb242e --- /dev/null +++ b/src/api.js @@ -0,0 +1,80 @@ +import express from "express"; +import config from "../config"; +import { info, error } from "./logger"; +import { enableDefaultProxy } from "./proxy/configured-domains"; + +const brokenApp = {}; + +let isReady = false, + app; + +init(); + +/** + * Initializes an express app and resolves once the app is initialized. It is safe to call this function + * multiple times: It will always resolve to the same ready-to-use app or throw. + * @returns {Object} - Express app. + */ +export async function init () { + + if (isReady) { + return app; + } else if (app) { + await ready(); + return app; + } + + try { + + app = express(); + app.disable("x-powered-by"); + + if (config.isLocalEnv) { + app.use("/", express.static(`${ __dirname }/../test-static`)); + } + + enableDefaultProxy(app); + + app.use((err, _, res, next) => { // Express error handler + if (res.headersSent) { + return next(err); + } + error(err); + return res.status(500).send({ error: "An error ocurred. Error info was logged." }); + }); + + app.listen(config.httpPort, function onReady () { + info(`Web server is listening on port ${ config.httpPort }`); + isReady = true; + }); + + await new Promise((resolve) => setTimeout(() => isReady && resolve(), 25)); + + } catch (e) { + error(e); + isReady = false; + app = brokenApp; + throw e; + } + + return app; + +} + +export async function ready () { + return new Promise((resolve, reject) => { + const tm = setTimeout(() => reject(), 60000); + const int = setInterval(() => { + if (app === brokenApp) { + clearInterval(int); + clearTimeout(tm); + return reject(new Error("Express app failed to start")); + } + if (isReady) { + clearInterval(int); + clearTimeout(tm); + return resolve(); + } + }, 200); + }); +} \ No newline at end of file diff --git a/src/logger.js b/src/logger.js new file mode 100644 index 0000000..0d472c8 --- /dev/null +++ b/src/logger.js @@ -0,0 +1,7 @@ +export function info (...args) { + console.info(...args); +} + +export function error (...args) { + console.error(...args); +} \ No newline at end of file diff --git a/src/modules/mask.js b/src/modules/mask.js new file mode 100644 index 0000000..1cea0e1 --- /dev/null +++ b/src/modules/mask.js @@ -0,0 +1,7 @@ +export const mask = (matchedString) => matchedString.replace( + /[^\/\\]+/g, + part => `*(${ encodeURIComponent(Buffer.from(part).toString("base64").replace(/==?$/, "")) })*` +); +export const unmask = (string) => string.replace(/\*\(([^\)]+)\)\*/g, (_, masked) => + Buffer.from(decodeURIComponent(masked), "base64").toString() +) \ No newline at end of file diff --git a/src/modules/proxy.js b/src/modules/proxy.js new file mode 100644 index 0000000..338a88b --- /dev/null +++ b/src/modules/proxy.js @@ -0,0 +1,113 @@ +import proxy from "express-http-proxy"; +import config from "../../config"; +import url from "url"; +import { mask, unmask } from "./mask"; +import { info } from "../logger"; + +const proxyDomains = new Set(config.proxy.domains); +const maskPaths = new Set(config.proxy.maskPaths); + +const replaceDomainRegex = new RegExp( + Array.from(proxyDomains).join("|").replace(/\./g, "\\."), + "gi" +); +const maskRegex = new RegExp( + Array.from(maskPaths).join("|").replace(/\//g, "\\/"), + "gi" +); +const replaceDomains = (match, pos, str) => { + const escapedSlashes = str[pos - 2] === "\\" && str[pos - 2] === "/" + const r = `${ + escapedSlashes + ? config.proxyDomain.replace(/\//g, "\\/") + "\\" + : config.proxyDomain + }/${ match }` + return r; +}; + +export function createDefaultProxy (targetDomain, proxyOptionsOverride = {}) { + let servername = targetDomain.replace(/^https?\:\/\//, ""); + return proxy(targetDomain, { + proxyReqOptDecorator: (proxyRequest, originalRequest) => { + proxyRequest.headers["accept-encoding"] = "identity"; + if (proxyRequest.headers["authorization"]) { + delete proxyRequest.headers["authorization"]; + } + return proxyOptionsOverride["proxyReqOptDecorator"] instanceof Function + ? proxyOptionsOverride["proxyReqOptDecorator"](proxyRequest, originalRequest) + : proxyRequest; + }, + userResHeaderDecorator: (headers) => { + if (headers.location) { + if (config.proxy.specialContentReplace[servername]) { // Keep before other replacements + const replacements = config.proxy.specialContentReplace[servername]; + for (const r of replacements) { + headers.location = headers.location.replace(r.regex, r.replace); + } + } + headers.location = headers.location + .replace(replaceDomainRegex, replaceDomains) + .replace(maskRegex, match => mask(match)); + } + return headers; + }, + userResDecorator: (_, proxyResData) => { + if (_.req.res && _.req.res.client && _.req.res.client.servername) { + servername = _.req.res.client.servername; + } + let pre = proxyResData.toString().replace(replaceDomainRegex, replaceDomains); + if (config.proxy.specialContentReplace[servername]) { + const replacements = config.proxy.specialContentReplace[servername]; + for (const r of replacements) { + pre = pre.replace(r.regex, r.replace); + } + } + pre = pre.replace(maskRegex, (match) => { // Mask configured URLs + const r = /\\|\//.test(match[0]) + ? match[0] + mask(match.slice(1)) + : mask(match); + return r; + }); + return pre; + }, + proxyReqPathResolver: (req) => { + + // Unmask URL parts that were masked + let unmasked = unmask(req.url); + + // For Google measurement protocol hits, overwrite user's IP address in order for Google to determine location + if ( + config.proxy.ipOverrides[servername] + && config.proxy.ipOverrides[servername].urlMatch instanceof RegExp + && config.proxy.ipOverrides[servername].queryParameterName + && config.proxy.ipOverrides[servername].urlMatch.test(unmasked) + ) { + + const parsedUrl = url.parse(unmasked); + const clientIp = req.headers["x-forwarded-for"] + ? req.headers["x-forwarded-for"].split(/,\s?/g)[0] + : req.connection.remoteAddress.split(":").pop(); + const encodedIp = encodeURIComponent(clientIp); + + unmasked = parsedUrl.path + `${ + parsedUrl.search ? "&" : "?" + }${ + config.proxy.ipOverrides[servername].queryParameterName instanceof Array + ? config.proxy.ipOverrides[servername].queryParameterName.map(name => `${ name }=${ encodedIp }`).join("&") + : `${ config.proxy.ipOverrides[servername].queryParameterName }=${ encodedIp }` + }`; + + } + + // Apply overrides + const finalPath = proxyOptionsOverride["proxyReqPathResolver"] instanceof Function + ? proxyOptionsOverride["proxyReqPathResolver"](req, unmasked) + : unmasked; + + info(`Proxied: ${ servername }${ finalPath }`); + + return finalPath; + + } + }); +} \ No newline at end of file diff --git a/src/proxy/configured-domains.js b/src/proxy/configured-domains.js new file mode 100644 index 0000000..1297046 --- /dev/null +++ b/src/proxy/configured-domains.js @@ -0,0 +1,19 @@ +import { createDefaultProxy } from "../modules/proxy"; +import config from "../../config"; +import { unmask } from "../modules/mask"; + +const domains = new Set(config.proxy.domains); +const proxies = new Map(Array.from(domains).map((domain) => { + return [domain, createDefaultProxy(`https://${ domain }`, { + proxyReqPathResolver: (_, path) => path.replace(`/${ domain }`, "") // Strip domain from URL + })]; +})); + +export function enableDefaultProxy (expressApp) { + expressApp.use("/", (req, res, next) => { + const domain = unmask(req.url.split(/\//g)[1]); + return domains.has(domain) + ? proxies.get(domain)(req, res, next) // Use proxy for configured domains + : next(); + }); +} \ No newline at end of file diff --git a/test-static/index.html b/test-static/index.html new file mode 100644 index 0000000..2f702a5 --- /dev/null +++ b/test-static/index.html @@ -0,0 +1,40 @@ + + + + Test + + + + + + + + + + + + + + + + + + + + + This is Google Tag Manager test. You are being watched. Possibly.
+ (check the developer tools - network) (check the code of this page) + + + + \ No newline at end of file