@mytmpvpn/mytmpvpn-client
Version:
MyTmpVpn Client Library
472 lines (471 loc) • 18.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createAuthenticatedClient = exports.MyTmpVpnClientMock = void 0;
const logging_1 = require("../logging");
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 axios_1 = require("axios");
const utils_1 = require("@mytmpvpn/mytmpvpn-common/utils");
const peanuts_1 = require("@mytmpvpn/mytmpvpn-common/models/peanuts");
const client_1 = require("../client");
const vpnConfig_1 = require("@mytmpvpn/mytmpvpn-common/models/vpnConfig");
const locations_1 = require("./locations");
// if MYTMPVPN_WAIT is set, then we wait the specified random amount of time +/- 50%
let maxWaitingTime = 100;
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);
logging_1.logger.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;
// Mock referral data
this.referralCode = 'ABC12345';
this.referralHistory = [
{
relationshipId: '1',
referralCode: 'ABC12345',
createdAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
referrerReward: 17,
refereeReward: 42
},
{
relationshipId: '2',
referralCode: 'ABC12345',
createdAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(),
referrerReward: 37,
refereeReward: 73
},
{
relationshipId: '3',
referralCode: 'ABC12345',
createdAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(),
referrerReward: 7,
refereeReward: 11
}
];
// Set up a default mock session for the mock client
this.setupMockSession();
}
setupMockSession() {
// Import here to avoid circular dependencies
const { CognitoUserSession, CognitoIdToken, CognitoAccessToken, CognitoRefreshToken, CognitoUser, CognitoUserPool } = require('amazon-cognito-identity-js');
const mockUserPool = new CognitoUserPool({
UserPoolId: 'us-east-1_mockUserPool',
ClientId: 'mock-client-id'
});
const mockUser = new CognitoUser({
Username: 'mockuser',
Pool: mockUserPool
});
const mockSession = new CognitoUserSession({
IdToken: new CognitoIdToken({ IdToken: 'mock-id-token' }),
AccessToken: new CognitoAccessToken({ AccessToken: 'mock-access-token' }),
RefreshToken: new CognitoRefreshToken({ RefreshToken: 'mock-refresh-token' })
});
this.setUserSession(mockUser, mockSession);
}
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);
const peanuts = computePeanuts(bytes, duration);
const result = {
bytes,
duration,
peanuts
};
const peanutsDiff = peanuts - metrics.peanuts;
this.peanutsBalance -= peanutsDiff;
return result;
}
async getUserConfig() {
await some_time_passed();
return {
giftedPeanuts: 10,
vpnConfigLimits: {
maxPeanutsFieldMinValue: 0.1,
maxPeanutsFieldMaxValue: 2,
deleteAfterFieldMinValue: 60,
deleteAfterFieldMaxValue: 60 * 5,
},
vpnsQuota: (0, locations_1.getLocations)().length * 2
};
}
async patchUserConfig(updates) {
await some_time_passed();
const current = await this.getUserConfig();
return { ...current, ...updates };
}
async createVpn(geonamesId, vpnConfig) {
logging_1.logger.debug(`createVpn(${geonamesId}, ${JSON.stringify(vpnConfig)})`);
try {
await this.checkMaxVpns(vpnConfig);
const userId = this.getUser().getUsername();
const currentBalance = await this.getPeanutsBalance();
const vpnConfigLimits = (await this.getUserConfig()).vpnConfigLimits;
const config = (0, vpnConfig_1.checkValidConfig)(userId, currentBalance, vpnConfigLimits, vpnConfig);
const userVpn = (0, uservpn_1.newUserVpn)({
userId,
geonamesId,
state: vpn_1.VpnState.Creating,
config,
});
this.userVpnsDb[userVpn.vpn.vpnId] = {
...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) {
logging_1.logger.error(err);
if (err instanceof errors_1.MyTmpVpnError) {
throw new axios_1.AxiosError("An error happened in the backend", '400', undefined, '', {
data: {
error: err.message
},
status: 400,
statusText: JSON.stringify(err),
headers: {},
config: {
headers: {}
}
});
}
else {
throw new axios_1.AxiosError("An error happened in the backend", '500', undefined, '', {
data: err,
status: 500,
statusText: JSON.stringify(err),
headers: {},
config: {
headers: {}
}
});
}
}
}
async checkMaxVpns(vpnConfig) {
const vpns = Object.values(this.userVpnsDb).map(vpnWithMetrics => {
return vpnWithMetrics;
}).filter(vpn_1.vpnAgainstQuotaPredicate);
const vpnNb = vpns.length;
const maxVpns = (await this.listLocations()).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.vpn.state == vpn_1.VpnState.Running) {
dbEntry.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.vpn,
metrics: dbEntry.metrics
});
});
}
catch (err) {
logging_1.logger.error(err);
throw new axios_1.AxiosError("An error happened in the backend", '404', undefined, '', {
data: {
error: `Vpn not found: ${vpnId}`,
},
status: 404,
statusText: ``,
headers: {},
config: {
headers: {}
}
});
}
}
async getVpn(vpnId) {
await some_time_passed();
const now = new Date();
const dbEntry = this.findDbEntry(vpnId);
switch (dbEntry.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 once in a while
if (Math.random() > 0.5) {
dbEntry.vpn.state = (0, vpn_1.fromRank)((0, vpn_1.toRank)(dbEntry.vpn.state) + 1);
}
break;
}
case vpn_1.VpnState.Provisioning: {
// Next step of Provisioning state is Running
dbEntry.vpn.state = vpn_1.VpnState.Running;
break;
}
case vpn_1.VpnState.Running: {
// When running, simulate consumption with metrics
const oldMetrics = dbEntry.metrics;
dbEntry.metrics = this.increaseVpnMetrics(oldMetrics);
logging_1.logger.debug(`Increasing metrics for: `, dbEntry, ', from: ', oldMetrics);
if (this.peanutsBalance <= 0) {
logging_1.logger.warn(`Not enough peanuts: ${this.peanutsBalance}, deprovisioning vpn ${JSON.stringify(dbEntry)}`);
dbEntry.vpn.state = vpn_1.VpnState.Deprovisioning;
break;
}
const maxAllowedPeanuts = dbEntry.vpn.config.maxPeanuts <= 0 ? this.peanutsBalance : dbEntry.vpn.config.maxPeanuts;
if (dbEntry.metrics.peanuts >= maxAllowedPeanuts) {
logging_1.logger.warn(`Max number of peanuts (${JSON.stringify(maxAllowedPeanuts)}) reached for vpn ${JSON.stringify(dbEntry)}`);
dbEntry.vpn.state = vpn_1.VpnState.Deprovisioning;
break;
}
const deleteAfter = dbEntry.vpn.config.deleteAfter;
if (deleteAfter && Number.isSafeInteger(deleteAfter)) {
if (dbEntry.metrics.duration > deleteAfter) {
logging_1.logger.warn(`Duration=${dbEntry.metrics.duration} > ${deleteAfter}=deleteAfter, triggering delete`);
dbEntry.vpn.state = vpn_1.VpnState.Deprovisioning;
break;
}
}
// All good, remains in Running state
break;
}
default: {
logging_1.logger.error("getVpn(): State is not supported (yet?):", dbEntry.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)}`);
}
}
logging_1.logger.info("getVpn(): returning dbEntry", JSON.stringify(dbEntry));
// Return a promise for that vpn
return new Promise((resolve, reject) => {
resolve({
vpn: dbEntry.vpn,
metrics: (0, vpn_1.metricsToClient)(dbEntry.metrics)
});
});
}
async getVpnConfig(vpnId, af) {
await some_time_passed();
const suffix = af === 'ipv6' ? ' (IPv6)' : ' (IPv4)';
return `Fake configuration data of vpn: ${vpnId}${suffix}`;
}
async getVpnQrConfig(vpnId, af) {
await some_time_passed();
const suffix = af === 'ipv6' ? ' (IPv6)' : ' (IPv4)';
return `Fake QR code for configuration of vpn: ${vpnId}${suffix}`;
}
async listVpnsPaginated(pagingParams) {
await some_time_passed();
const userVpnsWithMetrics = Object.values(this.userVpnsDb);
const getLocationByGeonamesId = (geonamesId) => {
return (0, locations_1.getLocations)().find((l) => geonamesId === l.geonamesId);
};
let vpns = userVpnsWithMetrics.map(uvpn => uvpn.vpn);
// Apply filtering first
vpns = (0, vpn_1.filterVpns)(vpns, getLocationByGeonamesId, pagingParams.filterCity, pagingParams.filterCountry);
// Apply sorting
vpns = (0, vpn_1.sortVpns)(vpns, getLocationByGeonamesId, pagingParams.sortBy, pagingParams.sortOrder);
// Parse page number from nextPageToken
let pageNumber = 1;
if (pagingParams.nextPageToken) {
try {
const tokenData = JSON.parse(pagingParams.nextPageToken);
pageNumber = tokenData.pageNumber || 1;
}
catch (error) {
// Invalid token format, default to page 1
pageNumber = 1;
}
}
// Calculate pagination
const pageSize = pagingParams.pageSize;
const start = (pageNumber - 1) * pageSize;
const end = start + pageSize;
const totalVpns = vpns.length;
const totalPages = Math.ceil(totalVpns / pageSize);
const hasNextPage = pageNumber < totalPages;
logging_1.logger.debug(`listVpnsPaginated(${JSON.stringify(pagingParams)}), page: ${pageNumber}, start: ${start}, end: ${end}, totalVpns: ${totalVpns}, totalPages: ${totalPages}`);
return {
vpns: vpns.slice(start, end),
nextPageToken: hasNextPage ? JSON.stringify({ pageNumber: pageNumber + 1 }) : undefined
};
}
async listLocations() {
await some_time_passed();
return (0, locations_1.getLocations)();
}
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);
}
async getReferralCode() {
await some_time_passed();
return {
referralCode: this.referralCode
};
}
async getReferrerCode() {
await some_time_passed();
return {
referralCode: undefined
};
}
async validateReferralCodeFromBackend(code) {
await some_time_passed();
if (code === this.referralCode) {
return {
isValid: false,
error: 'Cannot use your own referral code',
};
}
// Mock some expired codes
if (code === 'EXPIRED1') {
return {
isValid: false,
error: 'Referral code has expired',
};
}
// Mock some already used codes
if (code === 'USED1234') {
return {
isValid: false,
error: 'Referral code has already been used',
};
}
// Mock user not eligible scenarios
if (code === 'NOTELIG1') {
return {
isValid: false,
error: 'User not eligible for referral program',
};
}
// Mock suspicious activity scenarios
if (code === 'SUSPIC01') {
return {
isValid: false,
error: 'Suspicious activity detected',
};
}
return {
isValid: true
};
}
async getReferralStats() {
await some_time_passed();
return {
totalReferrals: this.referralHistory.length,
totalRewardsEarned: this.referralHistory.map(item => item.referrerReward).reduce((sum, p) => sum + p, 0),
};
}
async getReferralHistory(options) {
await some_time_passed();
const limit = options?.limit || 10;
const startIndex = options?.nextToken ? parseInt(atob(options.nextToken)) : 0;
const endIndex = startIndex + limit;
const items = this.referralHistory.slice(startIndex, endIndex);
const hasMore = endIndex < this.referralHistory.length;
const nextToken = hasMore ? btoa(endIndex.toString()) : undefined;
return {
history: items,
pagination: {
nextToken,
hasMore
}
};
}
}
exports.MyTmpVpnClientMock = MyTmpVpnClientMock;
const createAuthenticatedClient = async () => {
return new MyTmpVpnClientMock();
};
exports.createAuthenticatedClient = createAuthenticatedClient;