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
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
9 changes: 7 additions & 2 deletions src/constants/europe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,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 +114,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 +146,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;
}
121 changes: 39 additions & 82 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,104 +32,60 @@ 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 params = {
response_type: 'code',
client_id: serviceId,
redirect_uri: this.environment.endpoints.redirectUri, // `${this.environment.baseUrl}/api/v1/user/oauth2/redirect`,
state: 'ccsp',
lang: 'en'
};
await got(authUrl, { params });

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, // `${this.environment.baseUrl}/api/v1/user/oauth2/redirect`,
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}.`);
}
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
Loading