UNPKG

@elshaer/homebridge-lg-thinq

Version:

A Homebridge plugin for controlling/monitoring LG ThinQ device via LG ThinQ platform.

303 lines 14.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Auth = void 0; const crypto_1 = __importDefault(require("crypto")); const luxon_1 = require("luxon"); const qs = __importStar(require("qs")); const url_1 = require("url"); const errors_1 = require("../errors"); const constants = __importStar(require("./constants")); const request_1 = require("./request"); const Session_1 = require("./Session"); class Auth { constructor(gateway) { this.gateway = gateway; this.lgeapi_url = `https://${this.gateway.country_code.toLowerCase()}.lgeapi.com/`; this.logger = console; } async login(username, password) { // get signature and timestamp in login form const hash = crypto_1.default.createHash('sha512'); return this.loginStep2(username, hash.update(password).digest('hex')); } async loginStep2(username, encrypted_password, extra_headers) { const headers = this.defaultEmpHeaders; const preLoginData = { 'user_auth2': encrypted_password, 'log_param': 'login request / user_id : ' + username + ' / third_party : null / svc_list : SVC202,SVC710 / 3rd_service : ', }; const preLogin = await request_1.requestClient.post(this.gateway.login_base_url + 'preLogin', qs.stringify(preLoginData), { headers }) .then(res => res.data); headers['X-Signature'] = preLogin.signature; headers['X-Timestamp'] = preLogin.tStamp; const data = { 'user_auth2': preLogin.encrypted_pw, 'password_hash_prameter_flag': 'Y', 'svc_list': 'SVC202,SVC710', ...extra_headers, }; // try login with username and hashed password const loginUrl = this.gateway.emp_base_url + 'emp/v2.0/account/session/' + encodeURIComponent(username); const account = await request_1.requestClient.post(loginUrl, qs.stringify(data), { headers }).then(res => res.data.account).catch(err => { if (!err.response) { throw err; } const { code, message } = err.response.data.error; if (code === 'MS.001.03') { throw new errors_1.AuthenticationError('Your account was already used to registered in ' + message + '.'); } throw new errors_1.AuthenticationError(message); }); // dynamic get secret key for emp signature const empSearchKeyUrl = this.gateway.login_base_url + 'searchKey?key_name=OAUTH_SECRETKEY&sever_type=OP'; const secretKey = await request_1.requestClient.get(empSearchKeyUrl).then(res => res.data).then(data => data.returnData); const timestamp = luxon_1.DateTime.utc().toRFC2822(); const empData = { account_type: account.userIDType, client_id: constants.CLIENT_ID, country_code: account.country, redirect_uri: 'lgaccount.lgsmartthinq:/', response_type: 'code', state: '12345', username: account.userID, }; const empUrl = new url_1.URL('https://emp-oauth.lgecloud.com/emp/oauth2/authorize/empsession?' + qs.stringify(empData)); const signature = this.signature(`${empUrl.pathname}${empUrl.search}\n${timestamp}`, secretKey); const empHeaders = { 'lgemp-x-app-key': constants.OAUTH_CLIENT_KEY, 'lgemp-x-date': timestamp, 'lgemp-x-session-key': account.loginSessionID, 'lgemp-x-signature': signature, 'Accept': 'application/json', 'X-Device-Type': 'M01', 'X-Device-Platform': 'ADR', 'Content-Type': 'application/x-www-form-urlencoded', 'Access-Control-Allow-Origin': '*', 'Accept-Encoding': 'gzip, deflate, br', 'Accept-Language': 'en-US,en;q=0.9', // eslint-disable-next-line max-len 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.44', }; // create emp session and get access token const authorize = await request_1.requestClient.get(empUrl.href, { headers: empHeaders, }).then(res => res.data).catch(err => { throw new errors_1.AuthenticationError(err.response.data.error.message); }); if (authorize.status !== 1) { throw new errors_1.TokenError(authorize.message || authorize); } const redirect_uri = new url_1.URL(authorize.redirect_uri); const tokenData = { code: redirect_uri.searchParams.get('code'), grant_type: 'authorization_code', redirect_uri: empData.redirect_uri, }; const requestUrl = '/oauth/1.0/oauth2/token?' + qs.stringify(tokenData); const token = await request_1.requestClient.post(redirect_uri.searchParams.get('oauth2_backend_url') + 'oauth/1.0/oauth2/token', qs.stringify(tokenData), { headers: { 'x-lge-app-os': 'ADR', 'x-lge-appkey': constants.CLIENT_ID, 'x-lge-oauth-signature': this.signature(`${requestUrl}\n${timestamp}`, constants.OAUTH_SECRET_KEY), 'x-lge-oauth-date': timestamp, 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', }, }).then(res => res.data); this.lgeapi_url = token.oauth2_backend_url || this.lgeapi_url; return new Session_1.Session(token.access_token, token.refresh_token, token.expires_in); } get defaultEmpHeaders() { return { 'Accept': 'application/json', 'X-Application-Key': constants.APPLICATION_KEY, 'X-Client-App-Key': constants.CLIENT_ID, 'X-Lge-Svccode': 'SVC709', 'X-Device-Type': 'M01', 'X-Device-Platform': 'ADR', 'X-Device-Language-Type': 'IETF', 'X-Device-Publish-Flag': 'Y', 'X-Device-Country': this.gateway.country_code, 'X-Device-Language': this.gateway.language_code, 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 'Access-Control-Allow-Origin': '*', 'Accept-Encoding': 'gzip, deflate, br', 'Accept-Language': 'en-US,en;q=0.9', }; } async handleNewTerm(accessToken) { const showTermUrl = 'common/showTerms?callback_url=lgaccount.lgsmartthinq:/updateTerms' + '&country=VN&language=en-VN&division=ha:T20&terms_display_type=3&svc_list=SVC202'; const showTermHtml = await request_1.requestClient.get(this.gateway.login_base_url + showTermUrl, { headers: { 'X-Login-Session': accessToken, }, }).then(res => res.data); const headers = { ...this.defaultEmpHeaders, 'X-Login-Session': accessToken, 'X-Signature': showTermHtml.match(/signature[\s]+:[\s]+"([^"]+)"/)[1], 'X-Timestamp': showTermHtml.match(/tStamp[\s]+:[\s]+"([^"]+)"/)[1], }; const accountTermUrl = 'emp/v2.0/account/user/terms?opt_term_cond=001&term_data=SVC202&itg_terms_use_flag=Y&dummy_terms_use_flag=Y'; const accountTerms = (await request_1.requestClient.get(this.gateway.emp_base_url + accountTermUrl, { headers }) .then(res => { var _a; return (_a = res.data.account) === null || _a === void 0 ? void 0 : _a.terms; })) .map(term => { return term.termsID; }); const termInfoUrl = 'emp/v2.0/info/terms?opt_term_cond=001&only_service_terms_flag=&itg_terms_use_flag=Y&term_data=SVC202'; const infoTerms = await request_1.requestClient.get(this.gateway.emp_base_url + termInfoUrl, { headers }).then(res => { return res.data.info.terms; }); const newTermAgreeNeeded = infoTerms.filter((term) => { return accountTerms.indexOf(term.termsID) === -1; }).map(term => { return [term.termsType, term.termsID, term.defaultLang].join(':'); }).join(','); if (newTermAgreeNeeded) { const updateAccountTermUrl = 'emp/v2.0/account/user/terms'; await request_1.requestClient.post(this.gateway.emp_base_url + updateAccountTermUrl, qs.stringify({ terms: newTermAgreeNeeded }), { headers, }); } } async getJSessionId(accessToken) { // login to old gateway also - thinq v1 const memberLoginUrl = this.gateway.thinq1_url + 'member/login'; const memberLoginHeaders = { 'x-thinq-application-key': 'wideq', 'x-thinq-security-key': 'nuts_securitykey', 'Accept': 'application/json', 'x-thinq-token': accessToken, }; const memberLoginData = { countryCode: this.gateway.country_code, langCode: this.gateway.language_code, loginType: 'EMP', token: accessToken, }; return await request_1.requestClient.post(memberLoginUrl, { lgedmRoot: memberLoginData }, { headers: memberLoginHeaders, }) .then(res => res.data) .then(data => data.lgedmRoot.jsessionId) .catch(err => { this.logger.debug(err.message.startsWith(errors_1.ManualProcessNeededErrorCode) ? 'Please open the native LG App and sign in to your account to see what happened,' + ' maybe new agreement need your accept. Then try restarting Homebridge.' : err.message); this.logger.debug(err); this.logger.info('Failed to login to old thinq v1 gateway. See debug logs for more details. Continuing anyways.'); }); } async refreshNewToken(session) { try { const gateway = await request_1.requestClient.post('https://kic.lgthinq.com:46030/api/common/gatewayUriList', { lgedmRoot: { countryCode: this.gateway.country_code, langCode: this.gateway.language_code, }, }, { headers: { 'Accept': 'application/json', 'x-thinq-application-key': 'wideq', 'x-thinq-security-key': 'nuts_securitykey', }, }).then(res => res.data.lgedmRoot); this.lgeapi_url = gateway.oauthUri + '/'; } catch (err) { // ignore this error } const tokenUrl = this.lgeapi_url + 'oauth/1.0/oauth2/token'; const data = { grant_type: 'refresh_token', refresh_token: session.refreshToken, }; const timestamp = luxon_1.DateTime.utc().toRFC2822(); const requestUrl = '/oauth/1.0/oauth2/token' + qs.stringify(data, { addQueryPrefix: true }); const signature = this.signature(`${requestUrl}\n${timestamp}`, constants.OAUTH_SECRET_KEY); const headers = { 'x-lge-app-os': 'ADR', 'x-lge-appkey': constants.CLIENT_ID, 'x-lge-oauth-signature': signature, 'x-lge-oauth-date': timestamp, 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', }; const resp = await request_1.requestClient.post(tokenUrl, qs.stringify(data), { headers }).then(resp => resp.data); session.newToken(resp.access_token, parseInt(resp.expires_in)); return session; } async getUserNumber(accessToken) { const profileUrl = this.lgeapi_url + 'users/profile'; const timestamp = luxon_1.DateTime.utc().toRFC2822(); const signature = this.signature(`/users/profile\n${timestamp}`, constants.OAUTH_SECRET_KEY); const headers = { 'Accept': 'application/json', 'Authorization': 'Bearer ' + accessToken, 'X-Lge-Svccode': 'SVC202', 'X-Application-Key': constants.APPLICATION_KEY, 'lgemp-x-app-key': constants.CLIENT_ID, 'X-Device-Type': 'M01', 'X-Device-Platform': 'ADR', 'x-lge-oauth-date': timestamp, 'x-lge-oauth-signature': signature, }; const resp = await request_1.requestClient.get(profileUrl, { headers }).then(resp => resp.data); if (resp.status === 2) { throw new errors_1.AuthenticationError(resp.message); } return resp.account.userNo; } async getLoginUrl() { const params = { country: this.gateway.country_code, language: this.gateway.language_code, client_id: constants.CLIENT_ID, svc_list: constants.SVC_CODE, svc_integrated: 'Y', redirect_uri: 'lgaccount.lgsmartthinq:/', show_thirdparty_login: 'LGE,MYLG,GGL,AMZ,FBK,APPL', division: 'ha:T20', callback_url: 'lgaccount.lgsmartthinq:/', oauth2State: '12345', show_select_country: 'N', }; return this.gateway.login_base_url + 'login/signIn' + qs.stringify(params, { addQueryPrefix: true }); } signature(message, secret) { return crypto_1.default.createHmac('sha1', Buffer.from(secret)).update(message).digest('base64'); } } exports.Auth = Auth; //# sourceMappingURL=Auth.js.map