diff --git a/.env.example b/.env.example index 55596a7..2e0675a 100644 --- a/.env.example +++ b/.env.example @@ -8,3 +8,5 @@ ALLOWED_INTERFACES=tun0 DISALLOWED_COUNTRIES=your_country_code_e_g_LV DISALLOWED_INTERNAL_IPS=your_internal_ip_e_g_192.168 DISALLOWED_EXT_IPS=your_external_ip +DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/your_webhook_url +UPDATE_INTERVAL_SECONDS=300 diff --git a/src/index.ts b/src/index.ts index 90ebe34..1fe200e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,90 +1,22 @@ -import { Transmission } from './transmission'; -import { DockerContainer } from './docker'; +import { setPortFlow } from './set-port-flow'; import { settings } from './settings'; -main().then(() => console.log('All done')).catch(console.error); +const isWorker = process.argv[2] === 'worker'; -async function main() { - console.log('Starting'); - const transmissionClient = new Transmission(settings.transmission); - const transmissionContainer = new DockerContainer( - settings.dockerTransmission.service, - settings.dockerTransmission.cwd, - settings.dockerTransmission.serviceName - ); - - const containerState = await transmissionContainer.getState(); - if (containerState?.state !== 'running') { - console.log('Transmission service is not running'); - return; - } - - console.log(`Gathering info`); - const port = await transmissionClient.getPort(); - console.log(`Port: ${port}`); - const portOpen = await transmissionClient.testPortIfOpen(); - console.log(`Port is open: ${portOpen}`); - const defaultInterface = await transmissionContainer.getDefaultInterface(); - console.log(`Default interface: ${defaultInterface?.interface} Internal ip: ${defaultInterface?.ip}`); - const ipInfo = await transmissionContainer.getIpInfo(); - console.log(`External ip: ${ipInfo?.ip} Country: ${ipInfo?.country}`); - - if (!defaultInterface || !ipInfo) { - console.log('Could not get default interface or ip info. Shutting down transmission service'); - await transmissionContainer.down(); - return; - } - - const countryOK = !settings.disallowedCountries.some((country) => ipInfo.country.includes(country)); - const internalIpOK = !settings.disallowedIntIps.some((ip) => defaultInterface.ip.includes(ip)); - const externalIpOK = !settings.disallowedExtIps.some((ip) => ipInfo.ip.includes(ip)); - const interfaceOK = settings.allowedInterfaces.some((inter) => defaultInterface.interface.includes(inter)); - - console.log(`Country OK: ${countryOK} Internal IP OK: ${internalIpOK} External IP OK: ${externalIpOK} Interface OK: ${interfaceOK}`); - - if (!countryOK || !internalIpOK || !externalIpOK || !interfaceOK) { - console.log('Shutting down transmission service'); - await transmissionContainer.down(); - return; - } - - if (portOpen) { - console.log(`Port ${port} is open`); - return; - } - - console.log(`Port ${port} is not open. Fetching new port.`); - - //fetch new port - const request = await fetch(`https://connect.pvdatanet.com/v3/Api/port?ip[]=${defaultInterface.ip}`) - const data = await request.json() as {status: string, supported: boolean} - - console.log(`Data received: ${JSON.stringify(data)}`); - - if (!data.status || !data.supported) { - console.log('Could not fetch new port. Shutting down transmission service'); - await transmissionContainer.down(); - return; - } - - const newPort= Number(data.status.split(" ")[1]); - - if (!newPort) { - console.log('Could not parse new port Shutting down transmission service'); - await transmissionContainer.down(); - return; - } - - console.log(`Setting port to ${newPort}`); - await transmissionClient.setPort(newPort); - - console.log('Port set'); - const newPortOpen = await transmissionClient.testPortIfOpen(); - console.log(`Port ${newPort} is open: ${newPortOpen}`); - - if (!newPortOpen) { - console.log('Port is not open, Shutting down transmission service'); - await transmissionContainer.down(); - return; - } +if (isWorker) { + //initial timeout is in case of a crash to not flood everything + console.log(`Starting worker, first check in ${settings.updateIntervalMs/1000}s`); + setTimeout(worker, settings.updateIntervalMs); +} else { + setPortFlow() + .then(() => console.log('All done')) + .catch(console.error); +} + +//todo - store some state in a file (in case of crash/restart)? +function worker() { + setPortFlow() + .then(() => console.log('All done')) + .catch(console.error) + .finally(() => setTimeout(worker, settings.updateIntervalMs)); } diff --git a/src/notification.ts b/src/notification.ts new file mode 100644 index 0000000..c56be28 --- /dev/null +++ b/src/notification.ts @@ -0,0 +1,24 @@ +import { settings } from './settings'; + +class NotificationService { + async sendNotification(message: string, important = false) { + const webhook = settings.discordWebhookUrl; + if (!webhook) { + return; + } + + console.log(`Posting message to Discord: ${message}`); + + const response = await fetch(webhook, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ content: `${important ? '@here ' :''}${message}` }), + }); + + console.log(`Discord response: ${response.status}, rate limit remaining: ${response.headers.get('x-ratelimit-remaining')}, rate limit reset in: ${response.headers.get('x-ratelimit-reset-after')}s.`); + } +} + +export const notificationService = new NotificationService(); diff --git a/src/set-port-flow.ts b/src/set-port-flow.ts new file mode 100644 index 0000000..6967e07 --- /dev/null +++ b/src/set-port-flow.ts @@ -0,0 +1,103 @@ +import { Transmission } from './transmission'; +import { DockerContainer } from './docker'; +import { settings } from './settings'; +import { notificationService } from './notification'; + +async function downTransmission(transmissionContainer: DockerContainer, reason: string) { + console.log(reason); + + await notificationService.sendNotification(reason, true); + return await transmissionContainer.down(); +} + +export async function setPortFlow() { + console.log('Starting set port flow'); + const transmissionClient = new Transmission(settings.transmission); + const transmissionContainer = new DockerContainer( + settings.dockerTransmission.service, + settings.dockerTransmission.cwd, + settings.dockerTransmission.serviceName + ); + + const containerState = await transmissionContainer.getState(); + if (containerState?.state !== 'running') { + console.log('Transmission service is not running'); + return; + } + + console.log(`Gathering info`); + const port = await transmissionClient.getPort(); + console.log(`Port: ${port}`); + const portOpen = await transmissionClient.testPortIfOpen(); + console.log(`Port is open: ${portOpen}`); + const defaultInterface = await transmissionContainer.getDefaultInterface(); + console.log(`Default interface: ${defaultInterface?.interface} Internal ip: ${defaultInterface?.ip}`); + const ipInfo = await transmissionContainer.getIpInfo(); + console.log(`External ip: ${ipInfo?.ip} Country: ${ipInfo?.country}`); + + if (!defaultInterface || !ipInfo) { + return await downTransmission( + transmissionContainer, + `Shutting down: defaultInterface: ${!!defaultInterface} ipInfo: ${!!ipInfo}`, + ); + } + + const countryOK = !settings.disallowedCountries.some((country) => ipInfo.country.includes(country)); + const internalIpOK = !settings.disallowedIntIps.some((ip) => defaultInterface.ip.includes(ip)); + const externalIpOK = !settings.disallowedExtIps.some((ip) => ipInfo.ip.includes(ip)); + const interfaceOK = settings.allowedInterfaces.some((inter) => defaultInterface.interface.includes(inter)); + + console.log(`Country OK: ${countryOK} Internal IP OK: ${internalIpOK} External IP OK: ${externalIpOK} Interface OK: ${interfaceOK}`); + + if (!countryOK || !internalIpOK || !externalIpOK || !interfaceOK) { + return await downTransmission( + transmissionContainer, + `Shutting down: Country OK: ${countryOK}, Internal IP OK: ${internalIpOK}, External IP OK: ${externalIpOK}, Interface OK: ${interfaceOK}.`, + ); + } + + if (portOpen) { + console.log(`Port ${port} is open`); + return; + } + + console.log(`Port ${port} is not open. Fetching new port.`); + + //fetch new port + const request = await fetch(`https://connect.pvdatanet.com/v3/Api/port?ip[]=${defaultInterface.ip}`) + const data = await request.json() as {status: string, supported: boolean} + + console.log(`Data received: ${JSON.stringify(data)}`); + + if (!data.status || !data.supported) { + return await downTransmission( + transmissionContainer, + `Shutting down: could not fetch new port, data: ${JSON.stringify(data)}`, + ); + } + + const newPort= Number(data.status.split(" ")[1]); + + if (!newPort) { + return await downTransmission( + transmissionContainer, + `Shutting down: could not parse new port, data: ${JSON.stringify(data)}`, + ); + } + + console.log(`Setting port to ${newPort}`); + await transmissionClient.setPort(newPort); + + console.log('Port set'); + const newPortOpen = await transmissionClient.testPortIfOpen(); + console.log(`Port ${newPort} is open: ${newPortOpen}`); + + if (!newPortOpen) { + return await downTransmission( + transmissionContainer, + `Shutting down: new port is still not open, data: ${JSON.stringify(data)}`, + ); + } + + await notificationService.sendNotification(`Port changed from ${port} to ${newPort} successfully`); +} diff --git a/src/settings.ts b/src/settings.ts index a809ddd..8e1669c 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -17,4 +17,6 @@ export const settings = { disallowedCountries: process.env.DISALLOWED_COUNTRIES?.split(',') ?? [], disallowedIntIps: process.env.DISALLOWED_INTERNAL_IPS?.split(',') ?? [], disallowedExtIps: process.env.DISALLOWED_EXT_IPS?.split(',') ?? [], + discordWebhookUrl: process.env.DISCORD_WEBHOOK_URL, + updateIntervalMs: (Number(process.env.UPDATE_INTERVAL_SECONDS) ?? 60 * 5) * 1000, } as const;