Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bluelinky",
"version": "9.1.0",
"version": "9.2.0",
"description": "An unofficial nodejs API wrapper for Hyundai bluelink",
"main": "dist/index.cjs",
"module": "dist/index.esm.js",
Expand Down
10 changes: 8 additions & 2 deletions src/constants/europe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const DEFAULT_LANGUAGE: EULanguages = 'en';
export interface EuropeanBrandEnvironment {
brand: Brand;
host: string;
idpUrl: string;
baseUrl: string;
clientId: string;
appId: string;
Expand Down Expand Up @@ -79,12 +80,14 @@ const getHyundaiEnvironment = ({
stampsFile,
}: EnvironmentConfig): EuropeanBrandEnvironment => {
const host = 'prd.eu-ccapi.hyundai.com:8080';
const idpUrl = 'https://idpconnect-eu.hyundai.com';
const baseUrl = `https://${host}`;
const clientId = '6d477c38-3ca4-4cf3-9557-2a1929a94654';
const appId = '1eba27d2-9a5b-4eba-8ec7-97eb6c62fb51';
return {
brand: 'hyundai',
host,
idpUrl,
baseUrl,
clientId,
appId,
Expand Down Expand Up @@ -112,12 +115,15 @@ const getKiaEnvironment = ({
stampsFile,
}: EnvironmentConfig): EuropeanBrandEnvironment => {
const host = 'prd.eu-ccapi.kia.com:8080';
const idpUrl = 'https://idpconnect-eu.kia.com';
const baseUrl = `https://${host}`;
const clientId = 'fdc85c00-0a2f-4c64-bcb4-2cfb1500730a';
const appId = 'a2b8469b-30a3-4361-8e13-6fceea8fbe74';
// const appId = 'a2b8469b-30a3-4361-8e13-6fceea8fbe74';
const appId = '1518dd6b-2759-4995-9ae5-c9ad4a9ddad1';
return {
brand: 'kia',
host,
idpUrl,
baseUrl,
clientId,
appId,
Expand All @@ -141,7 +147,7 @@ const getKiaEnvironment = ({

export const getBrandEnvironment = ({
brand,
stampMode = StampMode.DISTANT,
stampMode = StampMode.LOCAL,
stampsFile,
}: BrandEnvironmentConfig): EuropeanBrandEnvironment => {
switch (brand) {
Expand Down
8 changes: 1 addition & 7 deletions src/controllers/authStrategies/authStrategy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import got from 'got';
import { CookieJar } from 'tough-cookie';
import { DEFAULT_LANGUAGE, EULanguages, EuropeanBrandEnvironment } from '../../constants/europe';
import { EuropeanBrandEnvironment } from '../../constants/europe';

export type Code = string;

Expand All @@ -14,15 +14,9 @@ export interface AuthStrategy {

export async function initSession(
environment: EuropeanBrandEnvironment,
language: EULanguages = DEFAULT_LANGUAGE,
cookies?: CookieJar
): Promise<CookieJar> {
const cookieJar = cookies ?? new CookieJar();
await got(environment.endpoints.session, { cookieJar });
await got(environment.endpoints.language, {
method: 'POST',
body: `{"lang":"${language}"}`,
cookieJar,
});
return cookieJar;
}
123 changes: 40 additions & 83 deletions src/controllers/authStrategies/european.brandAuth.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import got from 'got';
import { CookieJar } from 'tough-cookie';
import { EULanguages, EuropeanBrandEnvironment } from '../../constants/europe';
import { AuthStrategy, Code, initSession } from './authStrategy';
import Url, { URLSearchParams } from 'url';
//import Url,
import { URLSearchParams } from 'url';

const stdHeaders = {
'User-Agent':
Expand Down Expand Up @@ -31,106 +32,62 @@ export class EuropeanBrandAuthStrategy implements AuthStrategy {
}

public async login(user: { username: string; password: string; }, options?: { cookieJar?: CookieJar }): Promise<{ code: Code, cookies: CookieJar }> {
const cookieJar = await initSession(this.environment, this.language, options?.cookieJar);
const cookieJar = await initSession(this.environment, options?.cookieJar);
const { body: { userId, serviceId } } = await got(this.environment.endpoints.integration, {
cookieJar,
json: true,
headers: stdHeaders
});
const brandAuthUrl = this.environment.brandAuthUrl({ language: this.language, userId, serviceId });
const parsedBrandUrl = Url.parse(brandAuthUrl, true);
const { body: authForm } = await got(
//const parsedBrandUrl = Url.parse(brandAuthUrl, true);
//const { body: authForm } =
await got(
brandAuthUrl, {
cookieJar,
headers: stdHeaders
});
const actionUrl = /action="([a-z0-9:/\-.?_=&;]*)"/gi.exec(authForm);
const preparedUrl = actionUrl?.[1].replace(/&amp;/g, '&');
if (!preparedUrl) {
throw new Error('@EuropeanBrandAuthStrategy.login: cannot found the auth url from the form.');
}
const formData = new URLSearchParams();
formData.append('username', user.username);
formData.append('password', user.password);
formData.append('credentialId', '');
formData.append('rememberMe', 'on');
const { headers: { location: redirectTo }, body: afterAuthForm } = await manageGot302(got.post(preparedUrl, {
cookieJar,
body: formData.toString(),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
...stdHeaders
},
followRedirect: false,
}));

const authUrl = `${this.environment.idpUrl}/auth/api/v2/user/oauth2/authorize`;
const codeData = new URLSearchParams({
response_type: 'code',
client_id: serviceId,
redirect_uri: this.environment.endpoints.redirectUri,
state: 'ccsp',
lang: 'en'
});
await got(authUrl, { body:codeData.toString() });

const loginUrl = `${this.environment.idpUrl}/auth/account/signin`;
const loginData = new URLSearchParams({
username: user.username,
password: user.password,
encryptedPassword: 'false',
remember_me: 'false',
redirect_uri: this.environment.endpoints.redirectUri,
state: 'ccsp',
client_id: this.environment.clientId
});
const { headers: { location: redirectTo }, body: afterAuthForm } = await manageGot302(got.post(loginUrl, {
body: loginData.toString(),
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
followRedirect: false, // equivalent to maxRedirects: 0
throwHttpErrors: false // needed so it doesn't throw on 302
}));

if(!redirectTo) {
const errorMessage = /<span class="kc-feedback-text">(.+)<\/span>/gm.exec(afterAuthForm);
if (errorMessage) {
throw new Error(`@EuropeanBrandAuthStrategy.login: Authentication failed with message : ${errorMessage[1]}`);
}
throw new Error('@EuropeanBrandAuthStrategy.login: Authentication failed, cannot retrieve error message');
}
const authResult = await got(redirectTo, {
cookieJar,
headers: stdHeaders
});
let url = authResult.url;
let htmlPage = authResult.body;
if(!url) {
throw new Error(`@EuropeanBrandAuthStrategy.login: after login redirection got stuck : ${htmlPage}`);
}
if(url.includes('login-actions/required-action')) {
const loginActionUrl = /action="([a-z0-9:/\-.?_=&;]*)"/gi.exec(htmlPage);
const loginActionCode = /name="code" value="(.*)"/gi.exec(htmlPage);
if (!loginActionUrl) {
throw new Error('@EuropeanBrandAuthStrategy.login: Cannot find login-actions url.');
}
if (!loginActionCode) {
throw new Error('@EuropeanBrandAuthStrategy.login: Cannot find login-actions code.');
}
const actionUrl = (loginActionUrl[1].startsWith('/')) ? `${parsedBrandUrl.protocol}//${parsedBrandUrl.host}${loginActionUrl[1]}` : loginActionUrl[1];
const loginActionForm = new URLSearchParams();
loginActionForm.append('code', loginActionCode[1]);
loginActionForm.append('accept', '');
const { headers: { location: loginActionRedirect }, body: AfterLoginActionAuthForm } = await manageGot302(got.post(actionUrl, {
cookieJar,
body: loginActionForm.toString(),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
...stdHeaders
},
}));
if(!loginActionRedirect) {
const errorMessage = /<span class="kc-feedback-text">(.+)<\/span>/gm.exec(AfterLoginActionAuthForm);
if (errorMessage) {
throw new Error(`@EuropeanBrandAuthStrategy.login: Authentication action failed with message : ${errorMessage[1]}`);
}
throw new Error('@EuropeanBrandAuthStrategy.login: Authentication action failed, cannot retrieve error message');
}
const authResult = await got(loginActionRedirect, {
cookieJar,
headers: stdHeaders
});
url = authResult.url;
htmlPage = authResult.body;
}
const { body, statusCode } = await got.post(this.environment.endpoints.silentSignIn, {
cookieJar,
body: {
intUserId: ''
},
json: true,
headers: {
...stdHeaders,
'ccsp-service-id': this.environment.clientId,
}
});
if(!body.redirectUrl) {
throw new Error(`@EuropeanBrandAuthStrategy.login: silent sign In didn't work, could not retrieve auth code. status: ${statusCode}, body: ${JSON.stringify(body)}`);
}
const { code } = Url.parse(body.redirectUrl, true).query;
const parsedUrl = new URL(redirectTo);
const code = parsedUrl.searchParams.get('code');

if (!code) {
throw new Error(`@EuropeanBrandAuthStrategy.login: Cannot find the argument code in ${body.redirectUrl}.`);
throw new Error(`@EuropeanBrandAuthStrategy.login: Cannot find the argument code.`);
}
return {
code: code as Code,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class EuropeanLegacyAuthStrategy implements AuthStrategy {
user: { username: string; password: string },
options?: { cookieJar: CookieJar }
): Promise<{ code: Code; cookies: CookieJar }> {
const cookieJar = await initSession(this.environment, this.language, options?.cookieJar);
const cookieJar = await initSession(this.environment, options?.cookieJar);
const { body, statusCode } = await got(this.environment.endpoints.login, {
method: 'POST',
json: true,
Expand Down
70 changes: 56 additions & 14 deletions src/controllers/european.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { URLSearchParams } from 'url';

import { CookieJar } from 'tough-cookie';
import { VehicleRegisterOptions } from '../interfaces/common.interfaces';
import { asyncMap, manageBluelinkyError, Stringifiable, uuidV4 } from '../tools/common.tools';
import { asyncMap, manageBluelinkyError, Stringifiable } from '../tools/common.tools';
import { AuthStrategy, Code } from './authStrategies/authStrategy';
import { EuropeanBrandAuthStrategy } from './authStrategies/european.brandAuth.strategy';
import { EuropeanLegacyAuthStrategy } from './authStrategies/european.legacyAuth.strategy';
Expand Down Expand Up @@ -53,7 +53,8 @@ export class EuropeanController extends SessionController<EuropeBlueLinkyConfig>
)} are.`
);
}
this.session.deviceId = uuidV4();
// this.session.deviceId = uuidV4();
this.session.deviceId = 'c629f223-3c53-4128-8a83-36bea6b74207';
this._environment = getBrandEnvironment(userConfig);
this.authStrategies = {
main: new EuropeanBrandAuthStrategy(this._environment, this.userConfig.language),
Expand All @@ -70,7 +71,8 @@ export class EuropeanController extends SessionController<EuropeBlueLinkyConfig>
accessToken: undefined,
refreshToken: undefined,
controlToken: undefined,
deviceId: uuidV4(),
// deviceId: uuidV4(),
deviceId: 'c629f223-3c53-4128-8a83-36bea6b74207',
tokenExpiresAt: 0,
controlTokenExpiresAt: 0,
};
Expand Down Expand Up @@ -155,6 +157,41 @@ export class EuropeanController extends SessionController<EuropeBlueLinkyConfig>
}
}

public async register(): Promise<string> {
/*
const genRanHex = size =>
[...Array(size)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
const notificationReponse = await got(
`${this.environment.baseUrl}/api/v1/spa/notifications/register`,
{
method: 'POST',
headers: {
'ccsp-service-id': this.environment.clientId,
'Content-Type': 'application/json;charset=UTF-8',
'Host': this.environment.host,
'Connection': 'Keep-Alive',
'Accept-Encoding': 'gzip',
'User-Agent': 'okhttp/3.10.0',
'ccsp-application-id': this.environment.appId,
'Stamp': await this.environment.stamp(),
},
body: {
pushRegId: genRanHex(64),
pushType: 'APNS',
uuid: this.session.deviceId,
},
json: true,
}
);

if (notificationReponse) {
this.session.deviceId = notificationReponse.body.resMsg.deviceId;
}
logger.debug('@EuropeController.login: Device registered');
*/
return 'Registered again';
}

public async login(): Promise<string> {
try {
if (!this.userConfig.password || !this.userConfig.username) {
Expand Down Expand Up @@ -202,7 +239,7 @@ export class EuropeanController extends SessionController<EuropeBlueLinkyConfig>
},
body: {
pushRegId: genRanHex(64),
pushType: 'APNS',
pushType: 'GCM',
uuid: this.session.deviceId,
},
json: true,
Expand All @@ -213,24 +250,25 @@ export class EuropeanController extends SessionController<EuropeBlueLinkyConfig>
this.session.deviceId = notificationReponse.body.resMsg.deviceId;
}
logger.debug('@EuropeController.login: Device registered');

const formData = new URLSearchParams();
formData.append('grant_type', 'authorization_code');
formData.append('redirect_uri', this.environment.endpoints.redirectUri);
formData.append('redirect_uri', this.environment.endpoints.redirectUri ); //`${this.environment.baseUrl}/api/v1/user/oauth2/redirect`);
formData.append('code', authResult.code);
formData.append('client_id', this.environment.clientId);
formData.append('client_secret','secret');

const response = await got(this.environment.endpoints.token, {
const response = await got(`${this.environment.idpUrl}/auth/api/v2/user/oauth2/token`, {
method: 'POST',
headers: {
'Authorization': this.environment.basicToken,
//'Authorization': this.environment.basicToken,
'Content-Type': 'application/x-www-form-urlencoded',
'Host': this.environment.host,
'Connection': 'Keep-Alive',
//'Host': this.environment.host,
//'Connection': 'Keep-Alive',
'Accept-Encoding': 'gzip',
'User-Agent': 'okhttp/3.10.0',
'grant_type': 'authorization_code',
'ccsp-application-id': this.environment.appId,
'Stamp': await this.environment.stamp(),
//'grant_type': 'authorization_code',
//'ccsp-application-id': this.environment.appId,
'Stamp': 'false', // await this.environment.stamp(),
},
body: formData.toString(),
cookieJar: authResult.cookies,
Expand Down Expand Up @@ -321,14 +359,18 @@ export class EuropeanController extends SessionController<EuropeBlueLinkyConfig>
}
}

public async getVehicleHttpService(): Promise<GotInstance<GotJSONFn>> {
public async getVehicleHttpService(vehicle?: EuropeanVehicle): Promise<GotInstance<GotJSONFn>> {
await this.checkControlToken();
const ccuccs2 = Number (vehicle ? vehicle.vehicleConfig.ccuCCS2ProtocolSupport : false);

return got.extend({
baseUrl: this.environment.baseUrl,
headers: {
...this.defaultHeaders,
'Authorization': this.session.controlToken,
'AuthorizationCCSP': this.session.controlToken,
'Stamp': await this.environment.stamp(),
'Ccuccs2protocolsupport': ccuccs2,
},
json: true,
});
Expand Down
Loading