UNPKG

garmin-connect

Version:

Makes it simple to interface with Garmin Connect to get or set any data point

496 lines 24.7 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.HttpClient = void 0; var axios_1 = __importDefault(require("axios")); var form_data_1 = __importDefault(require("form-data")); var lodash_1 = __importDefault(require("lodash")); var luxon_1 = require("luxon"); var oauth_1_0a_1 = __importDefault(require("oauth-1.0a")); var qs_1 = __importDefault(require("qs")); var crypto = require('crypto'); var CSRF_RE = new RegExp('name="_csrf"\\s+value="(.+?)"'); var TICKET_RE = new RegExp('ticket=([^"]+)"'); var ACCOUNT_LOCKED_RE = new RegExp('var statuss*=s*"([^"]*)"'); var PAGE_TITLE_RE = new RegExp('<title>([^<]*)</title>'); var USER_AGENT_CONNECTMOBILE = 'com.garmin.android.apps.connectmobile'; var USER_AGENT_BROWSER = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'; var OAUTH_CONSUMER_URL = 'https://thegarth.s3.amazonaws.com/oauth_consumer.json'; // refresh token var isRefreshing = false; var refreshSubscribers = []; var HttpClient = /** @class */ (function () { function HttpClient(url) { var _this = this; this.url = url; this.client = axios_1.default.create(); this.client.interceptors.response.use(function (response) { return response; }, function (error) { return __awaiter(_this, void 0, void 0, function () { var originalRequest, token, err_1; var _this = this; var _a; return __generator(this, function (_b) { switch (_b.label) { case 0: originalRequest = error.config; if (!(((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) === 401 && !(originalRequest === null || originalRequest === void 0 ? void 0 : originalRequest._retry))) return [3 /*break*/, 6]; if (!this.oauth2Token) { return [2 /*return*/]; } if (!isRefreshing) return [3 /*break*/, 4]; _b.label = 1; case 1: _b.trys.push([1, 3, , 4]); return [4 /*yield*/, new Promise(function (resolve) { refreshSubscribers.push(function (token) { resolve(token); }); })]; case 2: token = _b.sent(); originalRequest.headers.Authorization = "Bearer ".concat(token); return [2 /*return*/, this.client(originalRequest)]; case 3: err_1 = _b.sent(); console.log('err:', err_1); return [2 /*return*/, Promise.reject(err_1)]; case 4: originalRequest._retry = true; isRefreshing = true; console.log('interceptors: refreshOauth2Token start'); return [4 /*yield*/, this.refreshOauth2Token()]; case 5: _b.sent(); console.log('interceptors: refreshOauth2Token end'); isRefreshing = false; refreshSubscribers.forEach(function (subscriber) { return subscriber(_this.oauth2Token.access_token); }); refreshSubscribers = []; originalRequest.headers.Authorization = "Bearer ".concat(this.oauth2Token.access_token); return [2 /*return*/, this.client(originalRequest)]; case 6: if (axios_1.default.isAxiosError(error)) { if (error === null || error === void 0 ? void 0 : error.response) this.handleError(error === null || error === void 0 ? void 0 : error.response); } throw error; } }); }); }); this.client.interceptors.request.use(function (config) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { if (this.oauth2Token) { config.headers.Authorization = 'Bearer ' + this.oauth2Token.access_token; } return [2 /*return*/, config]; }); }); }); } HttpClient.prototype.fetchOauthConsumer = function () { return __awaiter(this, void 0, void 0, function () { var response; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, axios_1.default.get(OAUTH_CONSUMER_URL)]; case 1: response = _a.sent(); this.OAUTH_CONSUMER = { key: response.data.consumer_key, secret: response.data.consumer_secret }; return [2 /*return*/]; } }); }); }; HttpClient.prototype.checkTokenVaild = function () { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: if (!this.oauth2Token) return [3 /*break*/, 2]; if (!(this.oauth2Token.expires_at < luxon_1.DateTime.now().toSeconds())) return [3 /*break*/, 2]; console.error('Token expired!'); return [4 /*yield*/, this.refreshOauth2Token()]; case 1: _a.sent(); _a.label = 2; case 2: return [2 /*return*/]; } }); }); }; HttpClient.prototype.get = function (url, config) { return __awaiter(this, void 0, void 0, function () { var response; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.client.get(url, config)]; case 1: response = _a.sent(); return [2 /*return*/, response === null || response === void 0 ? void 0 : response.data]; } }); }); }; HttpClient.prototype.post = function (url, data, config) { return __awaiter(this, void 0, void 0, function () { var response; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.client.post(url, data, config)]; case 1: response = _a.sent(); return [2 /*return*/, response === null || response === void 0 ? void 0 : response.data]; } }); }); }; HttpClient.prototype.put = function (url, data, config) { return __awaiter(this, void 0, void 0, function () { var response; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.client.put(url, data, config)]; case 1: response = _a.sent(); return [2 /*return*/, response === null || response === void 0 ? void 0 : response.data]; } }); }); }; HttpClient.prototype.delete = function (url, config) { return __awaiter(this, void 0, void 0, function () { var response; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.client.post(url, null, __assign(__assign({}, config), { headers: __assign(__assign({}, config === null || config === void 0 ? void 0 : config.headers), { 'X-Http-Method-Override': 'DELETE' }) }))]; case 1: response = _a.sent(); return [2 /*return*/, response === null || response === void 0 ? void 0 : response.data]; } }); }); }; HttpClient.prototype.setCommonHeader = function (headers) { var _this = this; lodash_1.default.each(headers, function (headerValue, key) { _this.client.defaults.headers.common[key] = headerValue; }); }; HttpClient.prototype.handleError = function (response) { this.handleHttpError(response); }; HttpClient.prototype.handleHttpError = function (response) { var status = response.status, statusText = response.statusText, data = response.data; var msg = "ERROR: (".concat(status, "), ").concat(statusText, ", ").concat(JSON.stringify(data)); console.error(msg); throw new Error(msg); }; /** * Login to Garmin Connect * @param username * @param password * @returns {Promise<HttpClient>} */ HttpClient.prototype.login = function (username, password) { return __awaiter(this, void 0, void 0, function () { var ticket, oauth1; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.fetchOauthConsumer()]; case 1: _a.sent(); return [4 /*yield*/, this.getLoginTicket(username, password)]; case 2: ticket = _a.sent(); return [4 /*yield*/, this.getOauth1Token(ticket)]; case 3: oauth1 = _a.sent(); // TODO: Handle MFA // Step 5: Oauth2 return [4 /*yield*/, this.exchange(oauth1)]; case 4: // TODO: Handle MFA // Step 5: Oauth2 _a.sent(); return [2 /*return*/, this]; } }); }); }; HttpClient.prototype.getLoginTicket = function (username, password) { return __awaiter(this, void 0, void 0, function () { var step1Params, step1Url, step2Params, step2Url, step2Result, csrfRegResult, csrf_token, signinParams, step3Url, step3Form, step3Result, ticketRegResult, ticket; return __generator(this, function (_a) { switch (_a.label) { case 0: step1Params = { clientId: 'GarminConnect', locale: 'en', service: this.url.GC_MODERN }; step1Url = "".concat(this.url.GARMIN_SSO_EMBED, "?").concat(qs_1.default.stringify(step1Params)); // console.log('login - step1Url:', step1Url); return [4 /*yield*/, this.client.get(step1Url)]; case 1: // console.log('login - step1Url:', step1Url); _a.sent(); step2Params = { id: 'gauth-widget', embedWidget: true, locale: 'en', gauthHost: this.url.GARMIN_SSO_EMBED }; step2Url = "".concat(this.url.SIGNIN_URL, "?").concat(qs_1.default.stringify(step2Params)); return [4 /*yield*/, this.get(step2Url)]; case 2: step2Result = _a.sent(); csrfRegResult = CSRF_RE.exec(step2Result); if (!csrfRegResult) { throw new Error('login - csrf not found'); } csrf_token = csrfRegResult[1]; signinParams = { id: 'gauth-widget', embedWidget: true, clientId: 'GarminConnect', locale: 'en', gauthHost: this.url.GARMIN_SSO_EMBED, service: this.url.GARMIN_SSO_EMBED, source: this.url.GARMIN_SSO_EMBED, redirectAfterAccountLoginUrl: this.url.GARMIN_SSO_EMBED, redirectAfterAccountCreationUrl: this.url.GARMIN_SSO_EMBED }; step3Url = "".concat(this.url.SIGNIN_URL, "?").concat(qs_1.default.stringify(signinParams)); step3Form = new form_data_1.default(); step3Form.append('username', username); step3Form.append('password', password); step3Form.append('embed', 'true'); step3Form.append('_csrf', csrf_token); return [4 /*yield*/, this.post(step3Url, step3Form, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', Dnt: 1, Origin: this.url.GARMIN_SSO_ORIGIN, Referer: this.url.SIGNIN_URL, 'User-Agent': USER_AGENT_BROWSER } })]; case 3: step3Result = _a.sent(); // console.log('step3Result:', step3Result) this.handleAccountLocked(step3Result); this.handlePageTitle(step3Result); this.handleMFA(step3Result); ticketRegResult = TICKET_RE.exec(step3Result); if (!ticketRegResult) { throw new Error('login failed (Ticket not found or MFA), please check username and password'); } ticket = ticketRegResult[1]; return [2 /*return*/, ticket]; } }); }); }; // TODO: Handle MFA HttpClient.prototype.handleMFA = function (htmlStr) { }; // TODO: Handle Phone number HttpClient.prototype.handlePageTitle = function (htmlStr) { var pageTitileRegResult = PAGE_TITLE_RE.exec(htmlStr); if (pageTitileRegResult) { var title = pageTitileRegResult[1]; console.log('login page title:', title); if (lodash_1.default.includes(title, 'Update Phone Number')) { // current I don't know where to update it // See: https://github.com/matin/garth/issues/19 throw new Error("login failed (Update Phone number), please update your phone number, currently I don't know where to update it"); } } }; HttpClient.prototype.handleAccountLocked = function (htmlStr) { var accountLockedRegResult = ACCOUNT_LOCKED_RE.exec(htmlStr); if (accountLockedRegResult) { var msg = accountLockedRegResult[1]; console.error(msg); throw new Error('login failed (AccountLocked), please open connect web page to unlock your account'); } }; HttpClient.prototype.refreshOauth2Token = function () { return __awaiter(this, void 0, void 0, function () { var oauth1; return __generator(this, function (_a) { switch (_a.label) { case 0: if (!!this.OAUTH_CONSUMER) return [3 /*break*/, 2]; return [4 /*yield*/, this.fetchOauthConsumer()]; case 1: _a.sent(); _a.label = 2; case 2: if (!this.oauth2Token || !this.oauth1Token) { throw new Error('No Oauth2Token or Oauth1Token'); } oauth1 = { oauth: this.getOauthClient(this.OAUTH_CONSUMER), token: this.oauth1Token }; return [4 /*yield*/, this.exchange(oauth1)]; case 3: _a.sent(); console.log('Oauth2 token refreshed!'); return [2 /*return*/]; } }); }); }; HttpClient.prototype.getOauth1Token = function (ticket) { return __awaiter(this, void 0, void 0, function () { var params, url, oauth, step4RequestData, headers, response, token; return __generator(this, function (_a) { switch (_a.label) { case 0: if (!this.OAUTH_CONSUMER) { throw new Error('No OAUTH_CONSUMER'); } params = { ticket: ticket, 'login-url': this.url.GARMIN_SSO_EMBED, 'accepts-mfa-tokens': true }; url = "".concat(this.url.OAUTH_URL, "/preauthorized?").concat(qs_1.default.stringify(params)); oauth = this.getOauthClient(this.OAUTH_CONSUMER); step4RequestData = { url: url, method: 'GET' }; headers = oauth.toHeader(oauth.authorize(step4RequestData)); return [4 /*yield*/, this.get(url, { headers: __assign(__assign({}, headers), { 'User-Agent': USER_AGENT_CONNECTMOBILE }) })]; case 1: response = _a.sent(); token = qs_1.default.parse(response); // console.log('getOauth1Token - token:', token); this.oauth1Token = token; return [2 /*return*/, { token: token, oauth: oauth }]; } }); }); }; HttpClient.prototype.getOauthClient = function (consumer) { var oauth = new oauth_1_0a_1.default({ consumer: consumer, signature_method: 'HMAC-SHA1', hash_function: function (base_string, key) { return crypto .createHmac('sha1', key) .update(base_string) .digest('base64'); } }); return oauth; }; // HttpClient.prototype.exchange = function (oauth1) { return __awaiter(this, void 0, void 0, function () { var token, baseUrl, requestData, step5AuthData, url, response; return __generator(this, function (_a) { switch (_a.label) { case 0: token = { key: oauth1.token.oauth_token, secret: oauth1.token.oauth_token_secret }; baseUrl = "".concat(this.url.OAUTH_URL, "/exchange/user/2.0"); requestData = { url: baseUrl, method: 'POST', data: null }; step5AuthData = oauth1.oauth.authorize(requestData, token); url = "".concat(baseUrl, "?").concat(qs_1.default.stringify(step5AuthData)); // console.log('exchange - url:', url); this.oauth2Token = undefined; return [4 /*yield*/, this.post(url, null, { headers: { 'User-Agent': USER_AGENT_CONNECTMOBILE, 'Content-Type': 'application/x-www-form-urlencoded' } })]; case 1: response = _a.sent(); // console.log('exchange - response:', response); this.oauth2Token = this.setOauth2TokenExpiresAt(response); return [2 /*return*/]; } }); }); }; HttpClient.prototype.setOauth2TokenExpiresAt = function (token) { // human readable date token['last_update_date'] = luxon_1.DateTime.now().toLocal().toString(); token['expires_date'] = luxon_1.DateTime.fromSeconds(luxon_1.DateTime.now().toSeconds() + token['expires_in']) .toLocal() .toString(); // timestamp for check expired token['expires_at'] = luxon_1.DateTime.now().toSeconds() + token['expires_in']; token['refresh_token_expires_at'] = luxon_1.DateTime.now().toSeconds() + token['refresh_token_expires_in']; return token; }; return HttpClient; }()); exports.HttpClient = HttpClient; //# sourceMappingURL=HttpClient.js.map