Skip to content

Commit

Permalink
feat: add video ads blocking proxy
Browse files Browse the repository at this point in the history
  • Loading branch information
sapomaro committed Feb 17, 2024
1 parent 6fcb685 commit 88f1312
Show file tree
Hide file tree
Showing 14 changed files with 308 additions and 18 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/node_modules
package-lock.json
.env
.old
18 changes: 0 additions & 18 deletions index.js

This file was deleted.

3 changes: 3 additions & 0 deletions nodemon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"watch": ["*.js", "*.json", ".env"]
}
18 changes: 18 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "multipurpose-proxy-server",
"version": "1.0.0",
"description": "",
"main": "./src/index.js",
"scripts": {
"dev": "npx nodemon ./src/index.js",
"start": "npm run dev"
},
"license": "ISC",
"type": "module",
"dependencies": {
"dotenv": "16.3.1",
"express": "4.18.2",
"http-proxy-middleware": "2.0.6",
"nodemon": "3.0.1"
}
}
72 changes: 72 additions & 0 deletions src/apps/video-ads-blocking-proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import express from 'express';
import { config } from '../utils/config.js';
import { logger } from '../middlewares/logger.js';
import { proxy } from '../middlewares/proxy.js';

export function runVideoAdsBlockingProxy() {
const { IP, PORT1, HOST1, PATH1, PORT2, HOST2 } = config();

const app1 = express();
app1.use(express.json());

app1.use('/*', logger('<APP1-PROXY>'), proxy({
target: HOST1,
destination: IP,
pathRewrite: { '^/site': PATH1 },
amendIncomingHtml: `
<script>
function trimVideoIframe() {
const collection = document.getElementsByTagName('iframe') || [];
for (const item of collection) {
if (item.src?.includes('${HOST2}'.replace(/^.+\\.([A-Za-z0-9]+\\.[A-Za-z]+\\/?)$/, '$1'))) {
const newSrc = item.src.replace(/https:\\/\\/[^/]+\\//g, 'http://${IP}:${PORT2}/');
document.body.innerHTML = '<iframe id="video_iframe" width="560" height="400" src="' + newSrc + '" frameborder="0" allowfullscreen=""></iframe>';
break;
}
}
document.body.setAttribute('style', 'background: #000 !important; min-height: 100vh !important; display: flex !important; align-items: center !important; justify-content: center !important;');
}
document.addEventListener('DOMContentLoaded', () => {
setTimeout(trimVideoIframe, 1000);
});
</script>
`,
}));

app1.listen(PORT1, () => {
console.log(`APP1 RUNNING: http://${IP}:${PORT1}`);
});

const app2 = express();
app2.use(express.json());

app2.use('/*', logger('<APP2-PROXY>'), proxy({
target: HOST2,
destination: IP,
followRedirects: false,
amendIncomingHtml: `
<script>
function fastForwardVideoAds() {
const collection = document.getElementsByTagName('video') || [];
for (const video of collection) {
if (!video) { continue; }
const isVideoShort = video.duration < 60;
const isVideoPlaying = !!(video.currentTime > 0 && !video.paused && !video.ended && video.readyState > 2);
if (isVideoShort && isVideoPlaying) {
video.currentTime = video.currentTime + video.duration;
}
}
}
document.addEventListener('DOMContentLoaded', () => {
setInterval(fastForwardVideoAds, 200);
});
</script>
`,
}));

app2.listen(PORT2, () => {
console.log(`APP2 RUNNING: http://${IP}:${PORT2}`);
});
}
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { runVideoAdsBlockingProxy } from './apps/video-ads-blocking-proxy.js';

runVideoAdsBlockingProxy();
3 changes: 3 additions & 0 deletions src/middlewares/delay.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function delay(time) {
return (_req, _res, next) => setTimeout(next, time);
}
7 changes: 7 additions & 0 deletions src/middlewares/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function logger(preMessage = '', postMessage = '') {
return (req, res, next) => {
const fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl;
console.log(res.statusCode, preMessage, req.method, fullUrl, postMessage);
next();
};
}
86 changes: 86 additions & 0 deletions src/middlewares/proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { createProxyMiddleware, responseInterceptor } from 'http-proxy-middleware';
import { parseJson, stringifyJson } from '../utils/json.js';
import { mergeDeep } from '../utils/objects.js';
import { parseCookies } from '../utils/cookies.js';

export function proxy(config = {}) {
const {
target,
destination,
followRedirects,
amendOutgoingCookies,
amendOutgoingJson,
amendIncomingJson,
amendIncomingHtml,
...restConfig
} = config;

return createProxyMiddleware({
target: target,
secure: true,
changeOrigin: true,
cookieDomainRewrite: destination,
followRedirects,
onProxyReq: (proxyReq, req) => {
const contentType = proxyReq.getHeader('Content-Type') || '';

if (amendOutgoingCookies) {
let newCookiesString = '';
if (typeof amendOutgoingCookies === 'function') {
newCookiesString = amendOutgoingCookies(req.headers.cookie);
} else if (amendOutgoingCookies) {
newCookiesString = stringifyJson(
(parseCookies(req.headers.cookie) || {}),
(parseCookies(amendOutgoingCookies) || {}),
);
}
proxyReq.setHeader('cookie', newCookiesString);
}

if (contentType.includes('application/json')) {
let dataObject = req.body || {};
if (amendOutgoingJson) {
if (typeof amendOutgoingJson === 'function') {
dataObject = amendOutgoingJson(dataObject);
} else if (amendOutgoingJson) {
dataObject = mergeDeep(dataObject, amendOutgoingJson);
}
}
const dataString = stringifyJson(dataObject);
proxyReq.setHeader('Content-Length', Buffer.byteLength(dataString));
proxyReq.write(dataString);
}
},
selfHandleResponse: true,
onProxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => {
const contentType = res.getHeader('Content-Type') || '';

if (followRedirects === false) {
res.removeHeader('location');
}

if (contentType.includes('application/json') && amendIncomingJson) {
let jsonObject = parseJson(responseBuffer.toString('utf8'));
if (typeof amendIncomingJson === 'function') {
jsonObject = amendIncomingJson(jsonObject);
} else if (typeof amendIncomingJson === 'object') {
jsonObject = mergeDeep(jsonObject, amendIncomingJson);
}
return stringifyJson(jsonObject);
}

if (contentType.includes('text/html') && amendIncomingHtml) {
let htmlString = responseBuffer.toString('utf8');
if (typeof amendIncomingHtml === 'function') {
htmlString = amendIncomingHtml(htmlString);
} else if (amendIncomingHtml) {
htmlString += amendIncomingHtml;
}
return htmlString;
}

return responseBuffer;
}),
...restConfig,
});
};
9 changes: 9 additions & 0 deletions src/utils/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import dotenv from 'dotenv';
import { getIp } from './ip.js';

dotenv.config();

export function config() {
process.env.IP = getIp();
return process.env;
}
25 changes: 25 additions & 0 deletions src/utils/cookies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export function parseCookies(cookiesInput) {
if (typeof cookiesInput === 'string') {
const cookiesObject = {};
const cookiesList = cookiesInput.split(';');
for (const cookiesPair of cookiesList) {
const [cookieKey, cookieValue] = cookiesPair.split('=');
if (cookieKey.trim()) {
cookiesObject[cookieKey.trim()] = (cookieValue || '').trim();
}
}
return cookiesObject;
} if (typeof cookiesInput === 'object') {
return cookiesInput;
}
return null;
}

export function stringifyCookies(cookiesInput) {
if (typeof cookiesInput === 'object') {
return Object.entries(cookiesInput).map(([key, value]) => `${key}=${value}`).join('; ');
} if (typeof cookiesInput === 'string') {
return cookiesInput;
}
return '';
}
6 changes: 6 additions & 0 deletions src/utils/ip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { networkInterfaces } from 'os';

export function getIp() {
return [].concat(...Object.values(networkInterfaces()))
.find((item) => !item.internal && item.family === 'IPv4')?.address;
}
20 changes: 20 additions & 0 deletions src/utils/json.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export function parseJson(jsonString) {
if (typeof jsonString !== 'string') {
return null;
}
try {
return JSON.parse(jsonString.replace(/([{,][ ]*)(['"])?([a-z0-9A-Z_]+)(['"])?:/g, '$1"$3": '));
} catch(error) {
console.error(error, jsonString);
return null;
}
}

export function stringifyJson(jsonObject) {
try {
return JSON.stringify(jsonObject);
} catch(error) {
console.error(error, value);
return '';
}
}
52 changes: 52 additions & 0 deletions src/utils/objects.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export function isPlainObject(item) {
if (typeof item !== 'object' || item === null) {
return false;
}
const prototype = Object.getPrototypeOf(item);
return (prototype === null || prototype === Object.prototype || Object.getPrototypeOf(prototype) === null);
}

export function isArray(item) {
return Array.isArray(item);
}

export function mergeDeep(target, ...sources) {
if (!sources.length) {
return target;
}
const source = sources.shift();
if (isPlainObject(target) && isPlainObject(source)) {
for (const key in source) {
if (isPlainObject(source[key])) {
if (!isPlainObject(target[key])) {
target[key] = {};
}
mergeDeep(target[key], source[key]);
} else if (isArray(source[key])) {
if (!isArray(target[key])) {
target[key] = [];
}
mergeDeep(target[key], source[key]);
} else {
target[key] = source[key];
}
}
} else if (isArray(target) && isArray(source)) {
for (let key = 0; key < source.length; ++key) {
if (isPlainObject(source[key])) {
if (!isPlainObject(target[key])) {
target[key] = {};
}
mergeDeep(target[key], source[key]);
} else if (isArray(source[key])) {
if (!isArray(target[key])) {
target[key] = [];
}
mergeDeep(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
return mergeDeep(target, ...sources);
}

0 comments on commit 88f1312

Please sign in to comment.