const http = require('http');
const request = require('request');
const xml2js = require('xml2js');
const ping = require('ping');
const express = require('express');
const fetch = require('node-fetch');
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const { execSync } = require('child_process');
const process = require('process');
const config = require('./config.js');
const app = express();
const lockfile = require('proper-lockfile');
const { parse } = require('csv-parse');
let connectedDevices = [];
let isChecking = true;
let mode = 3;
let reset = false;
const parseProxyConfig = async () => {
var proxyConfig = {}
const fileStream = fs.createReadStream(config.proxyServerDirectory + '3proxy.cfg');
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity,
});
for await (const line of rl) {
if (line.includes('proxy ')) {
const segments = line.split('-');
let port, ip;
for (let segment of segments) {
if (segment.startsWith('p')) {
port = segment.slice(1);
} else if (segment.startsWith('e')) {
ip = segment.slice(1);
}
}
if (ip && port) {
if (!proxyConfig[ip]) {
proxyConfig[ip] = {};
}
proxyConfig[ip].port = port;
}
}
}
return proxyConfig;
};
const getData = async (ip, path, method, headers, requestBody = '') => {
return new Promise((resolve, reject) => {
const options = { hostname: ip, path, method, headers };
const req = http.request(options, (res) => {
let data = '';
res.setEncoding('utf8');
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
resolve(data);
});
});
req.on('error', (e) => {
reject(e);
});
if (requestBody) {
req.write(requestBody);
}
req.end();
});
};
const parseXML = async (data) => {
return new Promise((resolve, reject) => {
xml2js.parseString(data, (error, result) => {
if (error) {
reject(error);
} else if (result && result.error) {
reject(result.error);
} else {
resolve(result);
}
});
});
};
const getTokens = async (device) => {
try {
const data = await getData(device.ip, '/api/webserver/SesTokInfo', 'GET');
const result = await parseXML(data);
return { session: result.response.SesInfo[0], postToken: result.response.TokInfo[0] };
} catch (error) {
throw error;
}
};
const updateConnectedDevice = (device, field, newValue) => {
const index = connectedDevices.findIndex((connectedDevice) => connectedDevice.interface === device.interface);
if (index > -1) {
connectedDevices[index][field] = newValue;
}
};
const connectDevice = async (device, postToken, session) => {
try {
const headers = {
__RequestVerificationToken: postToken,
Cookie: session,
'Content-Type': 'text/xml',
};
const chunk = await getData(device.ip, '/api/dialup/mobile-dataswitch', 'POST', headers, "<?xml version='1.0' encoding='UTF-8'?><request><dataswitch>1</dataswitch></request>");
} catch (error) {
throw error;
}
};
const changeIpDevice = async (device) => {
try {
const result = execSync('python3 ' + config.changeIpScript + ' --gateway ' + device.ip);
return result.toString();
} catch (error) {}
};
const isConnected = (ip) => connectedDevices.some(device => device.ip === ip);
const addDevice = (device) => {
if (!isConnected(device.ip)) {
device.publicIp = '';
device.changingIp = 0;
connectedDevices.push(device);
} else {
console.warn(`Device with IP ${device.ip} is already connected`);
}
};
const getNewIp = async (ipEnding = 9) => {
ipEnding = connectedDevices.length > 0 ? connectedDevices.length + 9 : ipEnding;
const ip = `192.168.${ipEnding}.1`;
return isConnected(ip) ? getNewIp(ipEnding + 1) : ip;
};
const getPublicIp = async (device) => {
try {
const ip = execSync('curl -k --silent --interface ' + device.interface + ' https://eth0.me');
return ip.toString();
} catch (error) {}
};
const updateIp = async (device, postToken, session, reset = false, includeDevice = true) => {
try {
let newIp;
if (reset) {
newIp = config.defaultIP;
updateConnectedDevice(device, 'active', 0);
updateConnectedDevice(device, 'publicIp', '');
} else {
newIp = await getNewIp();
if (includeDevice) {
addDevice({ ip: newIp, active: 0, interface: device.interface, inetIp: device.inetIp, port: device.port });
updateConnectedDevice(device, 'active', 0);
updateConnectedDevice(device, 'publicIp', '');
}
}
console.log(`Updating IP to: ${newIp}`);
const headers = {
__RequestVerificationToken: postToken,
Cookie: session,
'Content-Type': 'text/xml',
};
let ipAddressParts = newIp.split('.');
ipAddressParts.pop();
let newIpShort = ipAddressParts.join('.');
const chunk = await getData(device.ip, '/api/dhcp/settings', 'POST', headers, `<?xml version=\"1.0\" encoding=\"UTF-8\"?><request><DnsStatus>1</DnsStatus><DhcpStartIPAddress>${newIpShort}.100</DhcpStartIPAddress><DhcpIPAddress>${newIpShort}.1</DhcpIPAddress><accessipaddress></accessipaddress><homeurl>hi.link</homeurl><DhcpStatus>1</DhcpStatus><DhcpLanN>
console.log(`IP updated: ${newIp}`);
if (includeDevice) {
await changeIp({ ip: newIp, active: 0, interface: device.interface, inetIp: device.inetIp, publicIp: '' });
}
} catch (error) {
throw error;
}
};
const changeIp = async (device) => {
console.log(`Changing IP: ${device.ip}`);
try {
updateConnectedDevice(device, 'changingIp', 1);
await changeIpDevice(device);
updateConnectedDevice(device, 'changingIp', 0);
updateConnectedDevice(device, 'publicIp', '');
await logDeviceIp(device, 0);
} catch (error) {
console.error(`Error occurred while changing IP for device: ${error.message}`);
}
};
const logDeviceIp = async (device, attempts) => {
let ip = await getPublicIp(device);
if (ip) {
ip = ip.replace(/(\r\n|\n|\r)/gm, '');
lockfile.lock('loggedIps.csv', {
retries: { retries: 100, minTimeout: 10, maxTimeout: 100 }
})
.then((release) => {
let ipFound = 1;
let rows = [];
fs.createReadStream('loggedIps.csv')
.pipe(parse({delimiter: ';', from_line: 1}))
.on('data', function(row) {
if(row[0] === ip) {
row[1] = parseInt(row[1]) + 1;
ipFound = row[1];
}
rows.push(row);
})
.on('end', () => {
let writeStream = fs.createWriteStream('./loggedIps.csv');
if (ipFound <= 1) {
let newRow = [ip, 1];
rows.push(newRow);
}
rows.forEach((row) => {
writeStream.write(row.join(';') + '\n', () => {
});
});
writeStream.end();
console.log('Logged public IP ' + ip + ' for the ' + ipFound + '. time');
})
return release();
})
.catch((e) => {
console.error(e);
})
}
else if (attempts < 10) {
setTimeout(async () => {
await logDeviceIp(device, attempts + 1);
}, 2);
}
};
const continuousCheck = () =>
setInterval(async () => {
try {
if (isChecking) {
connectedDevices.forEach(async device => {
const response = await ping.promise.probe(device.ip, { timeout: config.pingTimeout });
if (response.alive) {
if ((device.active === 0) && (device.changingIp === 0)) {
console.log(`Device ${device.ip} is active`);
updateConnectedDevice(device, 'active', 1);
const publicIp = await getPublicIp(device);
updateConnectedDevice(device, 'publicIp', publicIp);
await logDeviceIp(device, 0);
} else if ((device.active === 1) && (device.changingIp === 1)) {
updateConnectedDevice(device, 'active', 0);
} else if ((device.active === 1) && (device.changingIp === 0) && (!device.publicIp)) {
const publicIp = await getPublicIp(device);
updateConnectedDevice(device, 'publicIp', publicIp);
}
} else {
if ((device.active === 1) || (device.changingIp === 1)) {
console.log(`Device ${device.ip} is offline`);
updateConnectedDevice(device, 'active', 0);
updateConnectedDevice(device, 'changingIp', 0);
updateConnectedDevice(device, 'publicIp', '');
}
}
});
}
} catch (error) {
console.error('Error occurred while pinging devices:', error.message);
}
}, config.checkInterval);
const processIps = async (ips, reset = false) => {
let proxyConfig = await parseProxyConfig();
for (const device of ips) {
try {
const response = await ping.promise.probe(device.ip, { timeout: config.pingTimeout });
if (response.alive) {
console.log(`Found a device with this IP: ${device.ip}`);
const { session, postToken } = await getTokens(device);
await connectDevice(device, postToken, session);
if (reset) {
isChecking = false;
console.log(`Resetting to default IP: ${config.defaultIP}`);
const newTokens = await getTokens(device);
await updateIp(device, newTokens.postToken, newTokens.session, reset);
await changeIp({ ip: config.defaultIP, active: 1, interface: device.interface, inetIp: '', publicIp: '' });
} else if (isConnected(device.ip)) {
console.log(`IP already being used. Updating IP: ${device.ip}`);
const newTokens = await getTokens(device);
await updateIp(device, newTokens.postToken, newTokens.session);
} else {
let port = proxyConfig[device.inetIp] ? proxyConfig[device.inetIp].port : '';
addDevice({ ip: device.ip, active: 0, interface: device.interface, inetIp: device.inetIp, port: port });
}
}
} catch (error) {
console.error(`Error occurred while processing IPs: ${error.message}`);
}
}
continuousCheck();
};
const parseIfconfig = async () => {
try {
const ifconfigOutput = execSync('ifconfig');
const lines = ifconfigOutput.toString().split('\n');
const devices = {};
let currentDevice;
for (const line of lines) {
if (line.includes('inet ')) {
const inetIp = line.split('inet ')[1].split(' ')[0];
if (inetIp.startsWith('192.168.') && inetIp.endsWith('.100')) {
let ipAddressParts = inetIp.split('.');
ipAddressParts.pop();
let newIpShort = ipAddressParts.join('.');
devices[currentDevice] = { ip: newIpShort + '.1', inetIp: inetIp, interface: currentDevice, publicIp: '' };
}
} else if (line.includes(': flags=')) {
currentDevice = line.split(':')[0];
}
}
return Object.values(devices);
} catch (error) {
console.error('Error while parsing ifconfig:', error.message);
}
};
const getIps = async () => {
try {
return await parseIfconfig();
} catch (error) {
console.error('Error while parsing ifconfig:', error.message);
}
};
const generateIps = () => Array.from({ length: config.maxDevices }, (_, i) => `192.168.${i + 9}.1`);
const defaultIpCheck = () => setInterval(() => cycleDefaultIpCheck(), config.checkInterval);
const cycleDefaultIpCheck = async () => {
try {
const response = await ping.promise.probe(config.defaultIP, { timeout: config.pingTimeout });
if (response.alive) {
console.log(`New device found with this IP: ${config.defaultIP}`);
const { session, postToken } = await getTokens({ ip: config.defaultIP });
await connectDevice({ ip: config.defaultIP }, postToken, session);
const newTokens = await getTokens({ ip: config.defaultIP });
await updateIp({ ip: config.defaultIP }, newTokens.postToken, newTokens.session, false, false);
}
} catch (error) {
console.error('Error occurred while pinging devices:', error.message);
}
};
function create3ProxyConfigFile(ips) {
let proxyConfigContent = `#! /usr/local/bin/3proxy
daemon
nserver 8.8.8.8
nscache 65536
timeouts 1 5 30 60 180 15 60
auth none
allow * 192.168.0.0/24\n`;
let port = 3200;
for (let device of ips) {
proxyConfigContent += `proxy -p${port} -e${device.inetIp}\n`;
port++;
}
proxyConfigContent += "flush\n";
fs.writeFileSync(path.join(config.proxyServerDirectory, '3proxy.cfg'), proxyConfigContent, 'utf8');
}
function createStartProxySHFile(ips) {
let instructions = '';
let proxySHContent = `sudo echo "Let's setup the IP"\n`;
for (let device of ips) {
proxySHContent += `sudo ifconfig ${device.interface} ${device.inetIp}\n`;
}
proxySHContent += "sleep 2\nsudo echo \"Let's setup the routes for the proxy\"\n";
let i = 1;
for (let device of ips) {
let ipAddressParts = device.ip.split('.');
ipAddressParts.pop();
let newIpShort = ipAddressParts.join('.');
proxySHContent += `sudo ip route replace ${newIpShort}.0/24 dev ${device.interface} src ${device.inetIp} table gateway${i}
sudo ip route replace default via ${device.ip} dev ${device.interface} table gateway${i}
sudo ip rule add from ${device.inetIp}/32 table gateway${i}
sudo ip rule add to ${device.inetIp}/32 table gateway${i}\n`;
instructions += `${i} gateway${i}\n`;
i++;
}
console.log(`Run "sudo nano /etc/iproute2/rt_tables" and add this to the end of the document:\n${instructions}`);
proxySHContent += `sleep 5
sudo echo "Let's start the proxy"
sudo 3proxy ${config.proxyServerDirectory}3proxy.cfg`;
fs.writeFileSync(path.join(config.proxyServerDirectory, 'startproxy.sh'), proxySHContent, { encoding: 'utf8', mode: 0o777 });
}
if (process.argv[2] == '1') {
mode = 1;
} else if (process.argv[2] == '2') {
mode = 2;
} else {
mode = 3;
}
if (mode == 1) {
console.log(`Important: Plug one USB modem in at a time.`);
console.log(`Wait until the IP address of USB modem has been changed before plugging in the next one!`);
console.log(`After the IP addresses of all devices have been changed run "node app.js 2" to create your 3proxy.cfg and startproxy.sh files.`);
generateIps().forEach(async (ip) => {
console.log('Inside')
console.log(ip)
const response = await ping.promise.probe(ip, { timeout: config.pingTimeout })
console.log('Response')
console.log(response)
if (response.alive) {
console.log(`Found a device with this IP: ${ip}`);
const { session, postToken } = await getTokens({ ip: ip });
await connectDevice({ ip: ip }, postToken, session);
addDevice({ ip: ip });
}
});
defaultIpCheck();
} else if (mode == 2) {
console.log(`Creating 3proxy.cfg and startproxy.sh files.`);
getIps()
.then(ips => {
const threeProxyPath = path.join(config.proxyServerDirectory, '3proxy.cfg');
if (!fs.existsSync(threeProxyPath)) {
create3ProxyConfigFile(ips);
}
const startProxyPath = path.join(config.proxyServerDirectory, 'startproxy.sh');
if (!fs.existsSync(startProxyPath)) {
createStartProxySHFile(ips);
}
console.log(`After you added everything to rt_tables:\n1) Run "sudo ${config.proxyServerDirectory}startproxy.sh" to start 3proxy.\n2) Run "node app.js" to start the proxy monitor.\n3) Visit http://localhost:3000 with your browser.`);
})
.catch(error => {});
} else {
const threeProxyPath = path.join(config.proxyServerDirectory, '3proxy.cfg');
const startProxyPath = path.join(config.proxyServerDirectory, 'startproxy.sh');
if ((!fs.existsSync(threeProxyPath)) || (!fs.existsSync(startProxyPath))) {
console.log(`Error: 3proxy.cfg and startproxy.sh files do not exists.`);
console.log(`Please run "node app.js 1" first to setup your USB modems!`);
} else {
app.get('/changeip', async (req, res) => {
const { ip } = req.query;
if (ip) {
const device = connectedDevices.find(device => device.ip === ip);
if (device) {
await changeIp(device);
res.send('');
} else {
console.log(`No device found with IP ${ip}`);
res.status(404).send('Device not found');
}
} else {
console.log(`IP is required`);
res.status(400).send('IP is required');
}
});
app.get('/devices', (req, res) => {
const devicesWithDetails = connectedDevices.map(device => {
return {
ip: device.ip,
interface: device.interface ? device.interface : 'Unknown',
inetIp: device.inetIp ? device.inetIp : 'Unknown',
port: device.port ? device.port : 'Unknown',
active: device.active,
publicIp: device.publicIp ? device.publicIp : 'Unknown'
};
});
res.send(devicesWithDetails);
});
app.get('/', (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Device Status</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" />
<style>
.status-circle {
display: inline-block; width:10px; height:10px; border-radius: 50%;
}
.status-circle.green { background-color:green; }
.status-circle.orange { background-color:orange; }
.status-circle.red { background-color:red; }
</style>
</head>
<body>
<div class="container py-4">
<div class="table-responsive">
<table class="table" id="devicesTable">
<thead>
<tr>
<th>Interface</th><th>IP</th><th>Proxy Port</th><th>Status</th><th>Public IP</th><th>Action</th>
</tr>
</thead>
<tbody><tr><td colspan="6" class="text-center">Loading...</td></tr></tbody>
</table>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script>
async function refreshTable() {
const res = await fetch('/devices');
const devices = await res.json();
if(devices.length === 0){
document.querySelector('#devicesTable tbody').innerHTML = '<tr><td colspan="6" class="text-center">No devices found.</td></tr>';
} else {
let html = '';
for (let device of devices) {
const statusCircle = '<span id="status-' + device.interface +'" class="status-circle ' + ((device.publicIp === \'Unknown\') ? "orange" : (device.active ? "green" : "red")) + '"></span>';
const changeIpButton = '<button id="changeIpButton-' + device.interface +'" onclick="changeIpOfDevice(\\\'\' + device.ip + \'\\\', \\\'\' + device.interface + \'\\\')" class="btn btn-primary"' + (device.active ? "" : " disabled") + '>New IP</button>';
html += '<tr><td>' + [device.interface, device.ip, device.port, statusCircle, device.publicIp,changeIpButton].join('</td><td>') + '</td></tr>';
}
document.querySelector('#devicesTable tbody').innerHTML = html;
}
}
async function changeIpOfDevice(ip, interface) {
const button = document.querySelector('#changeIpButton-' + interface);
button.disabled = true;
const status = document.querySelector('#status-' + interface);
if (status.classList.contains('green')) {
status.classList.remove('green');
status.classList.add('red');
}
await fetch('/changeip?ip=' + ip);
}
refreshTable();
setInterval(refreshTable, 3000);
</script>
</body>
</html>
`);
});
app.listen(config.serverPort, () => {
console.log(`Proxy monitor listening on port ${config.serverPort}`);
getIps()
.then(ips => processIps(ips, reset))
.catch(error => console.error('Error while getting IPs:', error.message));
});
}
}