mirror of
https://github.com/dumbasPL/deluge-windscribe-ephemeral-port.git
synced 2024-11-10 01:02:17 +01:00
initial release
This commit is contained in:
parent
a5c30d00ce
commit
6d99a1d564
3 changed files with 273 additions and 1 deletions
39
src/deluge.ts
Normal file
39
src/deluge.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import {Deluge} from '@ctrl/deluge';
|
||||
|
||||
// env variables
|
||||
|
||||
const url = process.env.DELUGE_URL;
|
||||
const password = process.env.DELUGE_PASSWORD;
|
||||
|
||||
if (!url || url.length == 0) {
|
||||
console.log('Missing environment variable DELUGE_URL');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!password || password.length == 0) {
|
||||
console.log('Missing environment variable DELUGE_PASSWORD');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// functions
|
||||
|
||||
export async function updateDelugePort(port: number) {
|
||||
try {
|
||||
const client = new Deluge({
|
||||
baseUrl: url,
|
||||
password: password,
|
||||
});
|
||||
|
||||
const res = await client.setConfig({
|
||||
listen_ports: [port, port],
|
||||
random_port: false, // turn of random port as well
|
||||
});
|
||||
|
||||
if (res.error) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
console.log('Deluge port successfully updated');
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to update deluge port: ${error.message}`);
|
||||
}
|
||||
}
|
52
src/index.ts
52
src/index.ts
|
@ -1 +1,51 @@
|
|||
console.log('Hello World');
|
||||
import 'dotenv/config';
|
||||
import {updateDelugePort} from './deluge';
|
||||
import {getMyAccountCsrfToken, getPortForwardingInfo, login, removeEphemeralPort, requestMatchingEphemeralPort} from './windscribe';
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
// get a new session each time
|
||||
const sessionCookie = await login();
|
||||
|
||||
// get csrf token and time to pass on to future requests
|
||||
const csrfToken = await getMyAccountCsrfToken(sessionCookie);
|
||||
|
||||
// check for current status
|
||||
let portForwardingInfo = await getPortForwardingInfo(sessionCookie);
|
||||
|
||||
// check for mismatched ports if any present
|
||||
if (portForwardingInfo.ports.length == 2 && portForwardingInfo.ports[0] != portForwardingInfo.ports[1]) {
|
||||
console.log('detected mismatched ports, removing existing ports');
|
||||
await removeEphemeralPort(sessionCookie, csrfToken);
|
||||
|
||||
// update data to match current state
|
||||
portForwardingInfo.ports = [];
|
||||
portForwardingInfo.epfExpires = 0;
|
||||
}
|
||||
|
||||
// request new port of we don't have any
|
||||
if (portForwardingInfo.epfExpires == 0) {
|
||||
console.log('no port configured, Requesting new matching ephemeral port');
|
||||
portForwardingInfo = await requestMatchingEphemeralPort(sessionCookie, csrfToken);
|
||||
} else {
|
||||
console.log(`Using existing ephemeral port: ${portForwardingInfo.ports[0]}`);
|
||||
}
|
||||
|
||||
// update deluge with new port
|
||||
console.log('Updating deluge');
|
||||
await updateDelugePort(portForwardingInfo.ports[0]);
|
||||
|
||||
// schedule next run in 7 days since the time of creation (+ 1 minute just to be sure)
|
||||
// (this code is copied form the windscribe website btw)
|
||||
const expiresAt = new Date((portForwardingInfo.epfExpires + 86400 * 7) * 1000 + 60000);
|
||||
const diff = expiresAt.getTime() - new Date().getTime(); // time difference in milliseconds
|
||||
setTimeout(run, diff);
|
||||
console.log(`Port expires in ${Math.floor(diff/1000)} seconds. Next run scheduled at ${expiresAt.toLocaleString()}`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
// just kill the process on error
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
|
|
183
src/windscribe.ts
Normal file
183
src/windscribe.ts
Normal file
|
@ -0,0 +1,183 @@
|
|||
import axios, {AxiosResponse} from 'axios';
|
||||
import * as qs from 'qs';
|
||||
import {parse as parseCookie} from 'set-cookie-parser';
|
||||
|
||||
// env variables
|
||||
|
||||
const username = process.env.WINDSCRIBE_USERNAME;
|
||||
const password = process.env.WINDSCRIBE_PASSWORD;
|
||||
|
||||
if (!username || username.length == 0) {
|
||||
console.log('Missing environment variable WINDSCRIBE_USERNAME');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!password || password.length == 0) {
|
||||
console.log('Missing environment variable WINDSCRIBE_PASSWORD');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// interfaces
|
||||
|
||||
interface CsrfInfo {
|
||||
csrfTime: number;
|
||||
csrfToken: string;
|
||||
}
|
||||
|
||||
interface PortForwardingInfo {
|
||||
epfExpires: number;
|
||||
ports: number[];
|
||||
}
|
||||
|
||||
// constants
|
||||
|
||||
const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36';
|
||||
|
||||
// functions
|
||||
|
||||
export async function login(): Promise<string> {
|
||||
try {
|
||||
// get csrf token and time
|
||||
const {data: csrfData} = await axios.post<{csrf_token: string, csrf_time: number}>('https://res.windscribe.com/res/logintoken', null, {
|
||||
headers: {'User-Agent': userAgent},
|
||||
});
|
||||
|
||||
// log in
|
||||
const res = await axios.post('https://windscribe.com/login', qs.stringify({
|
||||
login: '1',
|
||||
upgrade: '0',
|
||||
csrf_time: csrfData.csrf_time,
|
||||
csrf_token: csrfData.csrf_token,
|
||||
username: username,
|
||||
password: password,
|
||||
code: ''
|
||||
}), {
|
||||
headers: {'content-type': 'application/x-www-form-urlencoded', 'User-Agent': userAgent},
|
||||
maxRedirects: 0,
|
||||
validateStatus: status => status == 302,
|
||||
});
|
||||
|
||||
// extract the cookie
|
||||
return parseCookie(res.headers['set-cookie'], {map: true, decodeValues: true})['ws_session_auth_hash'].value;
|
||||
} catch (error) {
|
||||
// try to extract windscribe message
|
||||
if (error.response) {
|
||||
const response = error.response as AxiosResponse<string>;
|
||||
const errorMessage = /<div class="content_message error">.*>(.*)<\/div/.exec(response.data);
|
||||
if (response.status == 200 && errorMessage && errorMessage[1]) {
|
||||
throw new Error(`Failed to log into windscribe: ${errorMessage[1]}`);
|
||||
}
|
||||
}
|
||||
|
||||
// or throw a generic error if windscribe message not found
|
||||
throw new Error(`Failed to log into windscribe: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getMyAccountCsrfToken(sessionCookie: string): Promise<CsrfInfo> {
|
||||
try {
|
||||
// get page
|
||||
const res = await axios.get<string>('https://windscribe.com/myaccount', {
|
||||
headers: {
|
||||
'Cookie': `ws_session_auth_hash=${sessionCookie};`,
|
||||
'User-Agent': userAgent,
|
||||
},
|
||||
});
|
||||
|
||||
// extract csrf tokena and time from page content
|
||||
const csrfTime = /csrf_time = (\d+);/.exec(res.data)[1];
|
||||
const csrfToken = /csrf_token = '(\w+)';/.exec(res.data)[1];
|
||||
|
||||
return {
|
||||
csrfTime: +csrfTime,
|
||||
csrfToken: csrfToken,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get csrf token from my account page: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPortForwardingInfo(sessionCookie: string): Promise<PortForwardingInfo> {
|
||||
try {
|
||||
// load sub page
|
||||
const res = await axios.get<string>('https://windscribe.com/staticips/load', {
|
||||
headers: {
|
||||
'Cookie': `ws_session_auth_hash=${sessionCookie};`,
|
||||
'User-Agent': userAgent,
|
||||
}
|
||||
});
|
||||
|
||||
// extract data from page
|
||||
const epfExpires = res.data.match(/epfExpires = (\d+);/)[1]; // this is always present. set to 0 if no port is active
|
||||
const ports = [...res.data.matchAll(/<span>(?<port>\d+)<\/span>/g)].map(x => +x[1]); // this will return an empty array when there are not pots forwarded
|
||||
|
||||
return {
|
||||
epfExpires: +epfExpires,
|
||||
ports,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get port forwarding info: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeEphemeralPort(sessionCookie: string, csrfInfo: CsrfInfo): Promise<void> {
|
||||
try {
|
||||
// remove port
|
||||
const res = await axios.post<{success: number, epf: boolean, message?: string}>('https://windscribe.com/staticips/deleteEphPort', qs.stringify({
|
||||
ctime: csrfInfo.csrfTime,
|
||||
ctoken: csrfInfo.csrfToken
|
||||
}), {
|
||||
headers: {
|
||||
'content-type': 'application/x-www-form-urlencoded',
|
||||
'Cookie': `ws_session_auth_hash=${sessionCookie};`,
|
||||
'User-Agent': userAgent,
|
||||
}
|
||||
});
|
||||
|
||||
// check for errors
|
||||
if (res.data.success == 0) {
|
||||
throw new Error(`success = 0; ${res.data.message ?? 'No message'}`);
|
||||
}
|
||||
|
||||
// make sure we actually removed it
|
||||
if (res.data.epf == false) {
|
||||
console.log('Tried to remove a non-existent ephemeral port, ignoring');
|
||||
} else {
|
||||
console.log('Deleted ephemeral port');
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to delete ephemeral port: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function requestMatchingEphemeralPort(sessionCookie: string, csrfInfo: CsrfInfo): Promise<PortForwardingInfo> {
|
||||
try {
|
||||
// request new port
|
||||
const res = await axios.post<{success: number, message?: string, epf?: {ext: number, int: number, start_ts: number}}>('https://windscribe.com/staticips/postEphPort', qs.stringify({
|
||||
ctime: csrfInfo.csrfTime,
|
||||
ctoken: csrfInfo.csrfToken,
|
||||
port: '', // empty string for a matching port
|
||||
}), {
|
||||
headers: {
|
||||
'content-type': 'application/x-www-form-urlencoded',
|
||||
'Cookie': `ws_session_auth_hash=${sessionCookie};`,
|
||||
'User-Agent': userAgent,
|
||||
}
|
||||
});
|
||||
|
||||
// check for errors
|
||||
if (res.data.success == 0) {
|
||||
throw new Error(`success = 0; ${res.data.message ?? 'No message'}`);
|
||||
}
|
||||
|
||||
// epf should be present by this point
|
||||
const epf = res.data.epf!;
|
||||
console.log(`Created new matching ephemeral port: ${epf.ext}`);
|
||||
return {
|
||||
epfExpires: epf.start_ts,
|
||||
ports: [epf.ext, epf.int],
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue