@mytmpvpn/mytmpvpn-client
Version:
MyTmpVpn Client Library
317 lines (316 loc) • 13.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createAuthenticatedClient = exports.MyTmpVpnClientMock = void 0;
const loglevel_1 = require("loglevel");
const vpn_1 = require("@mytmpvpn/mytmpvpn-common/models/vpn");
const uservpn_1 = require("@mytmpvpn/mytmpvpn-common/models/uservpn");
const errors_1 = require("@mytmpvpn/mytmpvpn-common/errors");
const client_1 = require("../client");
const axios_1 = require("axios");
const utils_1 = require("@mytmpvpn/mytmpvpn-common/utils");
const peanuts_1 = require("@mytmpvpn/mytmpvpn-common/models/peanuts");
const MAX_BYTES = 1024 * 1024; // Consumes a max of 1MB per call
const MAX_DURATION = 60 * 5; // Consumes a max of 5 minutes per call
// if MYTMPVPN_WAIT is set, then we wait the specified random amount of time +/- 50%
let maxWaitingTime = 1000;
if (process.env['MYTMPVPN_WAIT']) {
maxWaitingTime = new Number(process.env['MYTMPVPN_WAIT']).valueOf();
}
const MIN_BOUND = maxWaitingTime * 0.5;
const MAX_BOUND = maxWaitingTime * 1.5;
async function some_time_passed() {
// Use Math.random() to compute a sleep time between minBound and maxBound
const bound = MIN_BOUND + Math.random() * (MAX_BOUND - MIN_BOUND);
loglevel_1.default.debug(`Waiting for ${bound}ms`);
return await (0, utils_1.sleep)(bound);
}
function computePeanuts(bytes, duration) {
// Fake formulae. Let's make sure we don't consume more
// than 1 peanut per call
// i.e: MAX_BYTES * x + MAX_DURATION * y <= 1 peanut
// There are many solutions, but let's use a balance approach:
// x=5×10^−7, y=1.5×10^−3
return bytes * 5e-7 + duration * 1.5e-3;
}
// src/__mocks__/mytmpvpn-client.ts
// A mock that stores everything in memory
class MyTmpVpnClientMock extends client_1.MyTmpVpnClient {
constructor() {
super();
this.userVpnsDb = {};
// This should be big enough for unit-tests to pass
// Because we create multiple vpns from the same mocked user
// and the number of peanuts consumed is maximum 1
// this number should be high enough.
this.peanutsBalance = 37;
}
findDbEntry(vpnId) {
const dbEntry = this.userVpnsDb[vpnId];
if (!dbEntry) {
throw new errors_1.MyTmpVpnError("Vpn not found");
}
// if (dbEntry.userVpn.vpn.state !== VpnState.Running) {
// throw new MyTmpVpnError("Vpn not in Running state")
// }
return dbEntry;
}
increaseVpnMetrics(metrics) {
// We just add some random numbers here to simulate traffic
const bytes = metrics.bytes + Math.floor(Math.random() * 1024 * 1024);
const duration = metrics.duration + Math.floor(Math.random() * 60 * 5);
const peanuts = computePeanuts(bytes, duration);
const result = {
bytes,
duration,
peanuts
};
const peanutsDiff = peanuts - metrics.peanuts;
this.peanutsBalance -= peanutsDiff;
return result;
}
async getVpnConfigLimits() {
await some_time_passed();
return {
maxPeanutsFieldMinValue: 0.1,
maxPeanutsFieldMaxValue: 2,
deleteAfterFieldMinValue: 60,
deleteAfterFieldMaxValue: 60 * 5,
};
}
async createVpn(region, vpnConfig) {
loglevel_1.default.trace(`createVpn(${region}, ${JSON.stringify(vpnConfig)})`);
try {
await this.checkMaxVpns(vpnConfig);
const userId = this.getUser().getUsername();
const currentBalance = await this.getPeanutsBalance();
const vpnConfigLimits = await this.getVpnConfigLimits();
const config = (0, vpn_1.checkValidConfig)(userId, currentBalance, vpnConfigLimits, vpnConfig);
const userVpn = (0, uservpn_1.newUserVpn)({
userId,
region,
state: vpn_1.VpnState.Creating,
config,
});
this.userVpnsDb[userVpn.vpn.vpnId] = {
userVpn: userVpn,
metrics: {
bytes: 0,
duration: 0,
peanuts: 0,
}
};
// Let's add some time here for 2 reasons:
// 1. to simulate better the server side, as it takes some time to create an instance in the DB
// 2. to avoid clash in userId, as the creation time is taken for the id
await some_time_passed();
// Return a promise for that vpn
return new Promise((resolve, reject) => {
resolve(userVpn.vpn);
});
}
catch (err) {
loglevel_1.default.error(err);
if (err instanceof errors_1.MyTmpVpnError) {
throw new axios_1.AxiosError("An error happened in the backend", '400', undefined, '', {
data: err.message,
status: 400,
statusText: JSON.stringify(err),
headers: {},
config: {}
});
}
else {
throw new axios_1.AxiosError("An error happened in the backend", '500', undefined, '', {
data: err,
status: 500,
statusText: JSON.stringify(err),
headers: {},
config: {}
});
}
}
}
async checkMaxVpns(vpnConfig) {
const vpns = Object.values(this.userVpnsDb).map(vpnWithMetrics => {
return vpnWithMetrics.userVpn;
}).filter(vpn_1.vpnAgainstQuotaPredicate);
const vpnNb = vpns.length;
const maxVpns = (await this.listRegions()).length * 2;
(0, vpn_1.validateVpnNbAgainstQuota)(vpnNb, maxVpns);
}
async deleteVpn(vpnId) {
// Introduce a random sleep to simulate a long running operation
await some_time_passed();
try {
const dbEntry = this.findDbEntry(vpnId);
if (dbEntry.userVpn.vpn.state == vpn_1.VpnState.Running) {
dbEntry.userVpn.vpn.state = vpn_1.VpnState.Deprovisioning;
dbEntry.metrics = this.increaseVpnMetrics(dbEntry.metrics);
}
// Return a promise for that vpn
return new Promise((resolve, reject) => {
resolve({
vpn: dbEntry.userVpn.vpn,
metrics: dbEntry.metrics
});
});
}
catch (err) {
loglevel_1.default.error(err);
throw new axios_1.AxiosError("An error happened in the backend", '404', undefined, '', {
data: `Vpn not found: ${vpnId}`,
status: 404,
statusText: ``,
headers: {},
config: {}
});
}
}
async getVpn(vpnId) {
await some_time_passed();
const now = new Date();
const dbEntry = this.findDbEntry(vpnId);
switch (dbEntry.userVpn.vpn.state) {
case vpn_1.VpnState.Failed:
case vpn_1.VpnState.Deleted: {
// No progress in such a state, they are terminal states
break;
}
case vpn_1.VpnState.Creating:
case vpn_1.VpnState.Created:
case vpn_1.VpnState.Deprovisioning: {
// We make progress by increasing the state rank
dbEntry.userVpn.vpn.state = (0, vpn_1.fromRank)((0, vpn_1.toRank)(dbEntry.userVpn.vpn.state) + 1);
break;
}
case vpn_1.VpnState.Provisioning: {
// Next step of Provisioning state is Running
dbEntry.userVpn.vpn.state = vpn_1.VpnState.Running;
break;
}
case vpn_1.VpnState.Running: {
// When running, simulate consumption with metrics
dbEntry.metrics = this.increaseVpnMetrics(dbEntry.metrics);
if (this.peanutsBalance <= 0) {
loglevel_1.default.warn(`Not enough peanuts: ${this.peanutsBalance}, deprovisioning vpn ${JSON.stringify(dbEntry)}`);
dbEntry.userVpn.vpn.state = vpn_1.VpnState.Deprovisioning;
break;
}
if (dbEntry.metrics.peanuts >= dbEntry.userVpn.vpn.config.maxPeanuts) {
loglevel_1.default.warn(`Max number of peanuts reached for vpn ${JSON.stringify(dbEntry)}`);
dbEntry.userVpn.vpn.state = vpn_1.VpnState.Deprovisioning;
break;
}
const deleteAfter = dbEntry.userVpn.vpn.config.deleteAfter;
if (deleteAfter && Number.isSafeInteger(deleteAfter)) {
if (dbEntry.metrics.duration > deleteAfter) {
loglevel_1.default.warn(`Duration=${dbEntry.metrics.duration} > ${deleteAfter}=deleteAfter, triggering delete`);
dbEntry.userVpn.vpn.state = vpn_1.VpnState.Deprovisioning;
break;
}
}
// All good, remains in Running state
break;
}
default: {
loglevel_1.default.error("getVpn(): State is not supported (yet?):", dbEntry.userVpn.vpn.state);
// We should cover all states in this switch. Especially, Paused state is not supported
throw new errors_1.MyTmpVpnError(`State is not supported (yet?): ${JSON.stringify(dbEntry)}`);
}
}
loglevel_1.default.info("getVpn(): returning dbEntry", JSON.stringify(dbEntry));
// Return a promise for that vpn
return new Promise((resolve, reject) => {
resolve({
vpn: dbEntry.userVpn.vpn,
metrics: (0, vpn_1.metricsToClient)(dbEntry.metrics)
});
});
}
async getVpnConfig(vpnId) {
await some_time_passed();
return `Fake configuration data of vpn: ${vpnId}`;
}
async getVpnQrConfig(vpnId) {
await some_time_passed();
return `Fake QR code for configuration of vpn: ${vpnId}`;
}
async listVpnsPaginated(pagingParams) {
await some_time_passed();
const vpns = Object.entries(this.userVpnsDb).reverse();
const start = pagingParams.nextPageToken == null ? 0 : Number(pagingParams.nextPageToken);
let end = start + Number(pagingParams.pageSize);
let next = end;
if (end >= vpns.length) {
end = vpns.length;
next = undefined;
}
loglevel_1.default.debug(`listVpnsPaginated(${JSON.stringify(pagingParams)}), start: ${start}, end: ${end}, next: ${next}, returning`, JSON.stringify(vpns.slice(start, end)));
return {
// The subset in vpns
vpns: vpns.slice(start, end).map(userVpnWithMetrics => userVpnWithMetrics[1].userVpn.vpn),
nextPageToken: next?.toString(),
totalVpns: vpns.length,
totalPages: vpns.length / pagingParams.pageSize.valueOf()
};
}
async listRegions() {
await some_time_passed();
return ['us-west-2', 'eu-west-3', 'ca-central-1'];
}
async listRegionsDetailed() {
await some_time_passed();
return [
{ name: 'us-west-2', cctld: 'us', city: 'Portland', country: 'United States' },
{ name: 'us-west-3', cctld: 'fr', city: 'Paris', country: 'France' },
{ name: 'ca-central-1', cctld: 'ca', city: 'Montreal', country: 'Canada' },
];
}
async listPeanutsPacks() {
await some_time_passed();
return [
{
name: 'Starter Pack',
description: [
'~25 min with 4k traffic',
'~4 hours with HD traffic',
'~1 day web traffic'
],
peanuts: 17,
price: 2.71,
url: new URL('https://buy.com/starter')
},
{
name: 'Pro Pack',
description: [
'~1 hour with 4k traffic',
'~10 hours with HD traffic',
'~2.5 days web traffic'
],
peanuts: 34,
price: 4.89,
url: new URL('https://buy.com/pro')
},
{
name: 'Elite Pack',
description: [
'~2 hours with 4k traffic',
'~20 hours with HD traffic',
'~5 days web traffic'
],
peanuts: 73,
price: 9.87,
url: new URL('https://buy.com/elite')
},
];
}
async getPeanutsBalance() {
await some_time_passed();
return (0, peanuts_1.peanutsToClient)(this.peanutsBalance);
}
}
exports.MyTmpVpnClientMock = MyTmpVpnClientMock;
const createAuthenticatedClient = async () => {
return new MyTmpVpnClientMock();
};
exports.createAuthenticatedClient = createAuthenticatedClient;