UNPKG

@shuangbing/bmw-connected-drive

Version:

This package can be used to access the BMW ConnectedDrive services.

187 lines 8.98 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Account = void 0; const Constants_1 = require("./Constants"); const LocalTokenStore_1 = require("./LocalTokenStore"); const crypto_1 = __importDefault(require("crypto")); const url_1 = require("url"); const crossFetch = require('cross-fetch'); const fetch = require('fetch-cookie')(crossFetch); class Account { constructor(username, password, region, tokenStore, logger) { this.username = username; this.password = password; this.region = region; this.tokenStore = tokenStore !== null && tokenStore !== void 0 ? tokenStore : new LocalTokenStore_1.LocalTokenStore(); this.logger = logger; } async getToken() { var _a, _b, _c, _d, _e; if (!this.token && this.tokenStore) { (_a = this.logger) === null || _a === void 0 ? void 0 : _a.LogDebug("Attempting retrieving token from token store."); this.token = this.tokenStore.retrieveToken(); } if (this.token && new Date() > this.token.validUntil) { (_b = this.logger) === null || _b === void 0 ? void 0 : _b.LogDebug("Token expired."); if (this.token.refreshToken) { (_c = this.logger) === null || _c === void 0 ? void 0 : _c.LogDebug("Attempting refreshing."); try { const refreshToken = this.token.refreshToken; this.token = undefined; this.token = await this.retrieveToken({ "grant_type": "refresh_token", "refresh_token": refreshToken }); } catch { // Intentional empty catch, as if the refresh failed, we can attempt a normal token retrieval. } } else { this.token = undefined; } } if (!this.token || !((_d = this.token) === null || _d === void 0 ? void 0 : _d.accessToken)) { (_e = this.logger) === null || _e === void 0 ? void 0 : _e.LogDebug("Getting token from token endpoint."); this.token = await this.retrieveToken({ "grant_type": "authorization_code", "username": this.username, "password": this.password }); } if (!this.token) { throw new Error("Error occurred while retrieving token."); } return this.token; } async retrieveToken(parameters) { var _a, _b, _c, _d, _e, _f; const authSettingsUrl = `https://${Constants_1.Constants.ServerEndpoints[this.region]}/eadrax-ucs/v1/presentation/oauth/config`; let serverResponse = await this.executeFetchWithRetry(authSettingsUrl, { method: "GET", headers: { "ocp-apim-subscription-key": Constants_1.Constants.OAuthAuthorizationKey[this.region], "x-user-agent": "android(v1.07_20200330);bmw;1.7.0(11152)" }, credentials: "same-origin" }, response => response.ok); let data = await serverResponse.json(); const clientId = data.clientId; const clientSecret = data.clientSecret; const code_verifier = Account.base64UrlEncode(crypto_1.default.randomBytes(64)); const hash = crypto_1.default.createHash('sha256'); const code_challenge = Account.base64UrlEncode(hash.update(code_verifier).digest()); const state = Account.base64UrlEncode(crypto_1.default.randomBytes(16)); const authenticateUrl = `${data.gcdmBaseUrl}/gcdm/oauth/authenticate`; const tokenUrl = data.tokenEndpoint; const returnUrl = data.returnUrl; const baseOAuthParams = { client_id: clientId, response_type: "code", redirect_uri: returnUrl, state: state, nonce: "login_nonce", scope: data.scopes.join(" "), code_challenge: code_challenge, code_challenge_method: "S256" }; let body = { ...parameters, ...baseOAuthParams }; (_a = this.logger) === null || _a === void 0 ? void 0 : _a.LogTrace(JSON.stringify(body)); serverResponse = await this.executeFetchWithRetry(authenticateUrl, { method: "POST", body: new url_1.URLSearchParams(body), headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" }, credentials: "same-origin" }, response => response.ok); data = await serverResponse.json(); const authorization = Account.getQueryStringValue(data.redirect_to, "authorization"); (_b = this.logger) === null || _b === void 0 ? void 0 : _b.LogTrace(authorization); body = { ...baseOAuthParams, ...{ authorization: authorization } }; (_c = this.logger) === null || _c === void 0 ? void 0 : _c.LogTrace(JSON.stringify(body)); serverResponse = await this.executeFetchWithRetry(authenticateUrl, { method: "POST", body: new url_1.URLSearchParams(body), headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" }, redirect: "manual", credentials: "same-origin" }, response => response.status === 302); const nextUrl = serverResponse.headers.get("location"); (_d = this.logger) === null || _d === void 0 ? void 0 : _d.LogTrace(nextUrl); const code = Account.getQueryStringValue(nextUrl, "code"); (_e = this.logger) === null || _e === void 0 ? void 0 : _e.LogTrace(JSON.stringify(code)); const authHeaderValue = Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); serverResponse = await this.executeFetchWithRetry(tokenUrl, { method: "POST", body: new url_1.URLSearchParams({ code: code, code_verifier: code_verifier, redirect_uri: returnUrl, grant_type: "authorization_code" }), headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", authorization: `Basic ${authHeaderValue}` }, credentials: "same-origin" }, response => response.ok); data = await serverResponse.json(); this.token = { response: JSON.stringify(data), accessToken: data.access_token, refreshToken: data.refresh_token, validUntil: new Date(new Date().getTime() + ((data.expires_in - 5) * 1000)) }; if (this.tokenStore) { (_f = this.logger) === null || _f === void 0 ? void 0 : _f.LogDebug("Storing token in token store."); this.tokenStore.storeToken(this.token); } return this.token; } async executeFetchWithRetry(url, init, responseValidator) { var _a; let response; let retryCount = 0; do { response = await fetch(url, init); retryCount++; } while (retryCount < 10 && !responseValidator(response) && (await this.delay(1000))); if (!responseValidator(response)) { (_a = this.logger) === null || _a === void 0 ? void 0 : _a.LogError(`${response.status}: Error occurred while attempting to retrieve token. Server response: ${(await response.text())}`); throw new Error(`${response.status}: Error occurred while attempting to retrieve token.`); } return response; } async delay(ms) { var _a; (_a = this.logger) === null || _a === void 0 ? void 0 : _a.LogTrace("Sleeping for retry."); await new Promise(resolve => setTimeout(resolve, ms)); return true; } static getQueryStringValue(url, queryParamName) { const splitUrl = url === null || url === void 0 ? void 0 : url.split("?"); const queryString = splitUrl.length > 1 ? splitUrl[1] : splitUrl[0]; const parsedQueryString = queryString === null || queryString === void 0 ? void 0 : queryString.split("&"); if (!parsedQueryString) { throw new Error(`Url: '${url}' does not contain query string.`); } for (const param of parsedQueryString) { const paramKeyValue = param.split("="); if (paramKeyValue[0].toLowerCase() === queryParamName) { return paramKeyValue[1]; } } throw new Error(`Url: '${url}' does not contain parameter '${queryParamName}'.`); } static base64UrlEncode(buffer) { return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); } } exports.Account = Account; //# sourceMappingURL=Account.js.map