UNPKG

@mytmpvpn/mytmpvpn-client

Version:

MyTmpVpn Client Library

472 lines (471 loc) 18.9 kB
"use strict"; 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;