UNPKG

@mytmpvpn/mytmpvpn-client

Version:

MyTmpVpn Client Library

342 lines (341 loc) 15.6 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 = 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); 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() { const regions = await this.listRegionsDetailed(); return regions.map(r => r['name']); } async listRegionsDetailed() { await some_time_passed(); return [ { "name": "af-south-1", "city": "Cape Town", "country": "South Africa", "cctld": "za" }, { "name": "ap-east-1", "city": "Hong Kong", "country": "China", "cctld": "hk" }, { "name": "ap-southeast-1", "city": "Singapore", "country": "Singapore", "cctld": "sg" }, { "name": "ap-southeast-2", "city": "Sydney", "country": "Autralia", "cctld": "au" }, { "name": "ap-southeast-3", "city": "Jakarta", "country": "Indonesia", "cctld": "id" }, { "name": "ap-northeast-1", "city": "Tokyo", "country": "Japan", "cctld": "jp" }, { "name": "ap-northeast-2", "city": "Seoul", "country": "South Korea", "cctld": "kr" }, { "name": "ap-northeast-3", "city": "Osaka", "country": "Japan", "cctld": "jp" }, { "name": "ap-south-1", "city": "Mumbai", "country": "India", "cctld": "in" }, { "name": "ap-south-2", "city": "Hyderabad", "country": "India", "cctld": "in" }, { "name": "ca-central-1", "city": "Montreal", "country": "Canada", "cctld": "ca" }, { "name": "eu-central-1", "city": "Frankfurt", "country": "Germany", "cctld": "de" }, { "name": "eu-central-2", "city": "Zurich", "country": "Switzerland", "cctld": "ch" }, { "name": "eu-north-1", "city": "Stockholm", "country": "Sweden", "cctld": "se" }, { "name": "eu-south-1", "city": "Milan", "country": "Italy", "cctld": "it" }, { "name": "eu-south-2", "city": "Zaragoza", "country": "Spain", "cctld": "es" }, { "name": "eu-west-1", "city": "Dublin", "country": "Ireland", "cctld": "ie" }, { "name": "eu-west-2", "city": "London", "country": "United Kingdom", "cctld": "gb" }, { "name": "eu-west-3", "city": "Paris", "country": "France", "cctld": "fr" }, { "name": "me-south-1", "city": "Manama", "country": "Bahrain", "cctld": "bh" }, { "name": "me-central-1", "city": "Dubai", "country": "UAE", "cctld": "ae" }, { "name": "us-east-1", "city": "Washington", "country": "USA", "cctld": "us" }, { "name": "us-east-2", "city": "Columbus", "country": "USA", "cctld": "us" }, { "name": "us-west-1", "city": "San Francisco", "country": "USA", "cctld": "us" }, { "name": "us-west-2", "city": "Portland", "country": "USA", "cctld": "us" }, { "name": "sa-east-1", "city": "Sao Paulo", "country": "Brazil", "cctld": "br" }, { "name": "cn-north-1", "city": "Beijing", "country": "China", "cctld": "cn" }, { "name": "cn-northwest-1", "city": "Ningxia", "country": "China", "cctld": "cn" } ]; } 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;