UNPKG

@mytmpvpn/mytmpvpn-client

Version:

MyTmpVpn Client Library

317 lines (316 loc) 13.2 kB
"use strict"; 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;