@elshaer/homebridge-lg-thinq
Version:
A Homebridge plugin for controlling/monitoring LG ThinQ device via LG ThinQ platform.
303 lines • 14.6 kB
JavaScript
;
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