]> git.basschouten.com Git - openhab-addons.git/blob
5563919a0992ff7d2d2ca53978acb49e0fcd79b3
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.mybmw.internal.handler;
14
15 import static org.junit.jupiter.api.Assertions.*;
16 import static org.mockito.Mockito.*;
17 import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.*;
18
19 import java.nio.charset.StandardCharsets;
20 import java.security.KeyFactory;
21 import java.security.MessageDigest;
22 import java.security.PublicKey;
23 import java.security.spec.X509EncodedKeySpec;
24 import java.util.Base64;
25
26 import javax.crypto.Cipher;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jetty.client.HttpClient;
30 import org.eclipse.jetty.client.api.ContentResponse;
31 import org.eclipse.jetty.client.api.Request;
32 import org.eclipse.jetty.client.util.StringContentProvider;
33 import org.eclipse.jetty.http.HttpHeader;
34 import org.eclipse.jetty.util.MultiMap;
35 import org.eclipse.jetty.util.UrlEncoded;
36 import org.eclipse.jetty.util.ssl.SslContextFactory;
37 import org.junit.jupiter.api.Test;
38 import org.openhab.binding.mybmw.internal.MyBMWConfiguration;
39 import org.openhab.binding.mybmw.internal.dto.auth.AuthQueryResponse;
40 import org.openhab.binding.mybmw.internal.dto.auth.AuthResponse;
41 import org.openhab.binding.mybmw.internal.dto.auth.ChinaPublicKeyResponse;
42 import org.openhab.binding.mybmw.internal.dto.auth.ChinaTokenExpiration;
43 import org.openhab.binding.mybmw.internal.dto.auth.ChinaTokenResponse;
44 import org.openhab.binding.mybmw.internal.util.FileReader;
45 import org.openhab.binding.mybmw.internal.utils.BimmerConstants;
46 import org.openhab.binding.mybmw.internal.utils.Constants;
47 import org.openhab.binding.mybmw.internal.utils.Converter;
48 import org.openhab.core.io.net.http.HttpClientFactory;
49 import org.openhab.core.util.StringUtils;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
53 /**
54  * The {@link AuthTest} test authorization flow
55  *
56  * @author Bernd Weymann - Initial contribution
57  */
58 @NonNullByDefault
59 class AuthTest {
60     private final Logger logger = LoggerFactory.getLogger(AuthTest.class);
61
62     void testAuth() {
63         String user = "usr";
64         String pwd = "pwd";
65
66         SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
67         HttpClient authHttpClient = new HttpClient(sslContextFactory);
68         try {
69             authHttpClient.start();
70             Request firstRequest = authHttpClient
71                     .newRequest("https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_ROW)
72                             + "/eadrax-ucs/v1/presentation/oauth/config");
73             firstRequest.header("ocp-apim-subscription-key",
74                     BimmerConstants.OCP_APIM_KEYS.get(BimmerConstants.REGION_ROW));
75             firstRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)");
76
77             ContentResponse firstResponse = firstRequest.send();
78             logger.info(firstResponse.getContentAsString());
79             AuthQueryResponse aqr = Converter.getGson().fromJson(firstResponse.getContentAsString(),
80                     AuthQueryResponse.class);
81
82             String verifierBytes = StringUtils.getRandomAlphabetic(64).toLowerCase();
83             String codeVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(verifierBytes.getBytes());
84
85             MessageDigest digest = MessageDigest.getInstance("SHA-256");
86             byte[] hash = digest.digest(codeVerifier.getBytes(StandardCharsets.UTF_8));
87             String codeChallenge = Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
88
89             String stateBytes = StringUtils.getRandomAlphabetic(16).toLowerCase();
90             String state = Base64.getUrlEncoder().withoutPadding().encodeToString(stateBytes.getBytes());
91
92             String authUrl = aqr.gcdmBaseUrl + BimmerConstants.OAUTH_ENDPOINT;
93             logger.info(authUrl);
94             Request loginRequest = authHttpClient.POST(authUrl);
95             loginRequest.header("Content-Type", "application/x-www-form-urlencoded");
96
97             MultiMap<String> baseParams = new MultiMap<String>();
98             baseParams.put("client_id", aqr.clientId);
99             baseParams.put("response_type", "code");
100             baseParams.put("redirect_uri", aqr.returnUrl);
101             baseParams.put("state", state);
102             baseParams.put("nonce", "login_nonce");
103             baseParams.put("scope", String.join(" ", aqr.scopes));
104             baseParams.put("code_challenge", codeChallenge);
105             baseParams.put("code_challenge_method", "S256");
106
107             MultiMap<String> loginParams = new MultiMap<String>(baseParams);
108             loginParams.put("grant_type", "authorization_code");
109             loginParams.put("username", user);
110             loginParams.put("password", pwd);
111             loginRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
112                     UrlEncoded.encode(loginParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
113             ContentResponse secondResonse = loginRequest.send();
114             logger.info(secondResonse.getContentAsString());
115             String authCode = getAuthCode(secondResonse.getContentAsString());
116             logger.info(authCode);
117
118             MultiMap<String> authParams = new MultiMap<String>(baseParams);
119             authParams.put("authorization", authCode);
120             Request authRequest = authHttpClient.POST(authUrl).followRedirects(false);
121             authRequest.header("Content-Type", "application/x-www-form-urlencoded");
122             authRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
123                     UrlEncoded.encode(authParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
124             ContentResponse authResponse = authRequest.send();
125             logger.info("{}", authResponse.getHeaders());
126             logger.info("Response " + authResponse.getHeaders().get(HttpHeader.LOCATION));
127             String code = AuthTest.codeFromUrl(authResponse.getHeaders().get(HttpHeader.LOCATION));
128             logger.info("Code " + code);
129             logger.info("Auth");
130
131             logger.info(aqr.tokenEndpoint);
132             // AuthenticationStore authenticationStore = authHttpClient.getAuthenticationStore();
133             // BasicAuthentication ba = new BasicAuthentication(new URI(aqr.tokenEndpoint), Authentication.ANY_REALM,
134             // aqr.clientId, aqr.clientSecret);
135             // authenticationStore.addAuthentication(ba);
136             Request codeRequest = authHttpClient.POST(aqr.tokenEndpoint);
137             String basicAuth = "Basic "
138                     + Base64.getUrlEncoder().encodeToString((aqr.clientId + ":" + aqr.clientSecret).getBytes());
139             logger.info(basicAuth);
140             codeRequest.header("Content-Type", "application/x-www-form-urlencoded");
141             codeRequest.header(AUTHORIZATION, basicAuth);
142
143             MultiMap<String> codeParams = new MultiMap<String>();
144             codeParams.put("code", code);
145             codeParams.put("code_verifier", codeVerifier);
146             codeParams.put("redirect_uri", aqr.returnUrl);
147             codeParams.put("grant_type", "authorization_code");
148             codeRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
149                     UrlEncoded.encode(codeParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
150             ContentResponse codeResponse = codeRequest.send();
151             logger.info(codeResponse.getContentAsString());
152             AuthResponse ar = Converter.getGson().fromJson(codeResponse.getContentAsString(), AuthResponse.class);
153             Token t = new Token();
154             t.setType(ar.tokenType);
155             t.setToken(ar.accessToken);
156             t.setExpiration(ar.expiresIn);
157             logger.info(t.getBearerToken());
158
159             /**
160              * REQUEST CONTENT
161              */
162             HttpClient apiHttpClient = new HttpClient(sslContextFactory);
163             apiHttpClient.start();
164
165             MultiMap<String> vehicleParams = new MultiMap<String>();
166             vehicleParams.put("tireGuardMode", "ENABLED");
167             vehicleParams.put("appDateTime", Long.toString(System.currentTimeMillis()));
168             vehicleParams.put("apptimezone", "60");
169             // vehicleRequest.param("tireGuardMode", "ENABLED");
170             // vehicleRequest.param("appDateTime", Long.toString(System.currentTimeMillis()));
171             // vehicleRequest.param("apptimezone", "60.0");
172             // vehicleRequest.
173             // // logger.info(vehicleParams);
174             // vehicleRequest.content(new StringContentProvider(CONTENT_TYPE_JSON_ENCODED, vehicleParams.toString(),
175             // StandardCharsets.UTF_8));
176             // logger.info(vehicleRequest.getHeaders());
177             String params = UrlEncoded.encode(vehicleParams, StandardCharsets.UTF_8, false);
178
179             String vehicleUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_ROW)
180                     + "/eadrax-vcs/v1/vehicles";
181             logger.info(vehicleUrl);
182             Request vehicleRequest = apiHttpClient.newRequest(vehicleUrl + "?" + params);//
183             // .param("tireGuardMode", "ENABLED")
184             // .param("appDateTime", Long.toString(System.currentTimeMillis())).param("apptimezone", "60.0");
185             // vehicleRequest.header("Content-Type", "application/x-www-form-urlencoded");
186             vehicleRequest.header(HttpHeader.AUTHORIZATION, t.getBearerToken());
187             vehicleRequest.header("accept", "application/json");
188             vehicleRequest.header("accept-language", "de");
189             vehicleRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)");
190             // vehicleRequest.header(HttpHeader.CONTENT_TYPE, "application/x-www-form-urlencoded");
191             // vehicleRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
192             // UrlEncoded.encode(vehicleParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
193             ContentResponse vehicleResponse = vehicleRequest.send();
194             logger.info("Vehicle Status {} {}", vehicleResponse.getStatus(), vehicleResponse.getContentAsString());
195
196             /**
197              * CHARGE STATISTICS
198              */
199             MultiMap<String> chargeStatisticsParams = new MultiMap<String>();
200             chargeStatisticsParams.put("vin", "WBY1Z81040V905639");
201             chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime());
202             params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false);
203
204             String chargeStatisticsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_ROW)
205                     + "/eadrax-chs/v1/charging-statistics";
206             Request chargeStatisticsRequest = apiHttpClient.newRequest(chargeStatisticsUrl)
207                     .param("vin", "WBY1Z81040V905639").param("currentDate", Converter.getCurrentISOTime());
208             logger.info("{}", chargeStatisticsUrl);
209             // vehicleRequest.header("Content-Type", "application/x-www-form-urlencoded");
210             chargeStatisticsRequest.header(HttpHeader.AUTHORIZATION, t.getBearerToken());
211             chargeStatisticsRequest.header("accept", "application/json");
212             chargeStatisticsRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)");
213             chargeStatisticsRequest.header("accept-language", "de");
214
215             // MultiMap<String> chargeStatisticsParams = new MultiMap<String>();
216             // chargeStatisticsParams.put("vin", "WBY1Z81040V905639");
217             // chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime());
218             //
219             // params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false);
220             logger.info("{}", params);
221             chargeStatisticsRequest
222                     .content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, params, StandardCharsets.UTF_8));
223
224             ContentResponse chargeStatisticsResponse = chargeStatisticsRequest.send();
225             logger.info("{}", chargeStatisticsResponse.getStatus());
226             logger.info("{}", chargeStatisticsResponse.getReason());
227             logger.info("{}", chargeStatisticsResponse.getContentAsString());
228
229             /**
230              * CHARGE SESSIONS
231              */
232             MultiMap<String> chargeSessionsParams = new MultiMap<String>();
233             chargeSessionsParams.put("vin", "WBY1Z81040V905639");
234             chargeSessionsParams.put("maxResults", "40");
235             chargeSessionsParams.put("include_date_picker", "true");
236
237             params = UrlEncoded.encode(chargeSessionsParams, StandardCharsets.UTF_8, false);
238
239             String chargeSessionsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_ROW)
240                     + "/eadrax-chs/v1/charging-sessions";
241             Request chargeSessionsRequest = apiHttpClient.newRequest(chargeSessionsUrl + "?" + params);
242             logger.info("{}", chargeSessionsUrl);
243             // vehicleRequest.header("Content-Type", "application/x-www-form-urlencoded");
244             chargeSessionsRequest.header(HttpHeader.AUTHORIZATION, t.getBearerToken());
245             chargeSessionsRequest.header("accept", "application/json");
246             chargeSessionsRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)");
247             chargeSessionsRequest.header("accept-language", "de");
248
249             // MultiMap<String> chargeStatisticsParams = new MultiMap<String>();
250             // chargeStatisticsParams.put("vin", "WBY1Z81040V905639");
251             // chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime());
252             //
253             // params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false);
254             logger.info("{}", params);
255             // chargeStatisticsRequest
256             // .content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, params, StandardCharsets.UTF_8));
257
258             ContentResponse chargeSessionsResponse = chargeSessionsRequest.send();
259             logger.info("{}", chargeSessionsResponse.getStatus());
260             logger.info("{}", chargeSessionsResponse.getReason());
261             logger.info("{}", chargeSessionsResponse.getContentAsString());
262
263             String chargingControlUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_ROW)
264                     + "/eadrax-vrccs/v2/presentation/remote-commands/WBY1Z81040V905639/charging-control";
265             Request chargingControlRequest = apiHttpClient.POST(chargingControlUrl);
266             logger.info("{}", chargingControlUrl);
267             // vehicleRequest.header("Content-Type", "application/x-www-form-urlencoded");
268             chargingControlRequest.header(HttpHeader.AUTHORIZATION, t.getBearerToken());
269             chargingControlRequest.header("accept", "application/json");
270             chargingControlRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)");
271             chargingControlRequest.header("accept-language", "de");
272             chargingControlRequest.header("Content-Type", CONTENT_TYPE_JSON_ENCODED);
273
274             // String content = FileReader.readFileInString("src/test/resources/responses/charging-profile.json");
275             // logger.info("{}", content);
276             // ChargeProfile cpc = Converter.getGson().fromJson(content, ChargeProfile.class);
277             // String contentTranfsorm = Converter.getGson().toJson(cpc);
278             // String profile = "{chargingProfile:" + contentTranfsorm + "}";
279             // logger.info("{}", profile);
280             // chargingControlRequest
281             // .content(new StringContentProvider(CONTENT_TYPE_JSON_ENCODED, params, StandardCharsets.UTF_8));
282
283             // chargeStatisticsParams.put("vin", "WBY1Z81040V905639");
284             // chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime());
285             //
286             // params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false);
287
288             // ContentResponse chargingControlResponse = chargingControlRequest.send();
289             // logger.info("{}", chargingControlResponse.getStatus());
290             // logger.info("{}", chargingControlResponse.getReason());
291             // logger.info("{}", chargingControlResponse.getContentAsString());
292
293         } catch (Exception e) {
294             logger.error("{}", e.getMessage());
295         }
296     }
297
298     private String getAuthCode(String response) {
299         String[] keys = response.split("&");
300         for (int i = 0; i < keys.length; i++) {
301             if (keys[i].startsWith(AUTHORIZATION)) {
302                 String authCode = keys[i].split("=")[1];
303                 authCode = authCode.split("\"")[0];
304                 return authCode;
305             }
306         }
307         return Constants.EMPTY;
308     }
309
310     public static String codeFromUrl(String encodedUrl) {
311         final MultiMap<String> tokenMap = new MultiMap<String>();
312         UrlEncoded.decodeTo(encodedUrl, tokenMap, StandardCharsets.US_ASCII);
313         final StringBuilder codeFound = new StringBuilder();
314         tokenMap.forEach((key, value) -> {
315             if (!value.isEmpty()) {
316                 String val = value.get(0);
317                 if (key.endsWith(CODE)) {
318                     codeFound.append(val);
319                 }
320             }
321         });
322         return codeFound.toString();
323     }
324
325     @Test
326     public void testJWTDeserialze() {
327         String accessTokenResponseStr = FileReader
328                 .readFileInString("src/test/resources/responses/auth/auth_cn_login_pwd.json");
329         ChinaTokenResponse cat = Converter.getGson().fromJson(accessTokenResponseStr, ChinaTokenResponse.class);
330
331         // https://www.baeldung.com/java-jwt-token-decode
332         String token = cat.data.accessToken;
333         String[] chunks = token.split("\\.");
334         String tokenJwtDecodeStr = new String(Base64.getUrlDecoder().decode(chunks[1]));
335         ChinaTokenExpiration cte = Converter.getGson().fromJson(tokenJwtDecodeStr, ChinaTokenExpiration.class);
336         Token t = new Token();
337         t.setToken(token);
338         t.setType(cat.data.tokenType);
339         t.setExpirationTotal(cte.exp);
340         assertEquals(
341                 "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJEVU1NWSQxJEEkMTYzNzcwNzkxNjc4MiIsIm5iZiI6MTYzNzcwNzkxNiwiZXhwIjoxNjM3NzExMjE2LCJpYXQiOjE2Mzc3MDc5MTZ9.hpi-P97W68g7avGwu9dcBRapIsaG4F8MwOdPHe6PuTA",
342                 t.getBearerToken(), "Token");
343     }
344
345     public void testChina() {
346         SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
347         HttpClient authHttpClient = new HttpClient(sslContextFactory);
348         try {
349             authHttpClient.start();
350             HttpClientFactory mockHCF = mock(HttpClientFactory.class);
351             when(mockHCF.getCommonHttpClient()).thenReturn(authHttpClient);
352             MyBMWConfiguration config = new MyBMWConfiguration();
353             config.region = BimmerConstants.REGION_CHINA;
354             config.userName = "Hello User";
355             config.password = "Hello Password";
356             MyBMWProxy bmwProxy = new MyBMWProxy(mockHCF, config);
357             bmwProxy.updateTokenChina();
358         } catch (Exception e) {
359             logger.warn("Exception: " + e.getMessage());
360         }
361     }
362
363     @Test
364     public void testPublicKey() {
365         String publicKeyResponseStr = FileReader.readFileInString("src/test/resources/responses/auth/china-key.json");
366         ChinaPublicKeyResponse pkr = Converter.getGson().fromJson(publicKeyResponseStr, ChinaPublicKeyResponse.class);
367         String publicKeyStr = pkr.data.value;
368         String publicKeyPEM = publicKeyStr.replace("-----BEGIN PUBLIC KEY-----", "")
369                 .replaceAll(System.lineSeparator(), "").replace("-----END PUBLIC KEY-----", "").replace("\\r", "")
370                 .replace("\\n", "").trim();
371         byte[] encoded = Base64.getDecoder().decode(publicKeyPEM);
372         X509EncodedKeySpec spec = new X509EncodedKeySpec(encoded);
373         KeyFactory kf;
374         try {
375             kf = KeyFactory.getInstance("RSA");
376             PublicKey publicKey = kf.generatePublic(spec);
377             // https://www.thexcoders.net/java-ciphers-rsa/
378             Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
379             cipher.init(Cipher.ENCRYPT_MODE, publicKey);
380             byte[] encryptedBytes = cipher.doFinal("Hello World".getBytes());
381             String encodedPassword = Base64.getEncoder().encodeToString(encryptedBytes);
382             logger.info(encodedPassword);
383         } catch (Exception e) {
384             assertTrue(false, "Excpetion: " + e.getMessage());
385         }
386     }
387
388     public void testChinaToken() {
389         SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
390         HttpClient authHttpClient = new HttpClient(sslContextFactory);
391         try {
392             authHttpClient.start();
393             String url = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_CHINA)
394                     + BimmerConstants.CHINA_PUBLIC_KEY;
395             Request oauthQueryRequest = authHttpClient.newRequest(url);
396             oauthQueryRequest.header(X_USER_AGENT,
397                     String.format(BimmerConstants.BRAND_BMW, BimmerConstants.BRAND_BMW, BimmerConstants.REGION_ROW));
398
399             ContentResponse publicKeyResponse = oauthQueryRequest.send();
400             ChinaPublicKeyResponse pkr = Converter.getGson().fromJson(publicKeyResponse.getContentAsString(),
401                     ChinaPublicKeyResponse.class);
402             // https://stackoverflow.com/questions/11410770/load-rsa-public-key-from-file
403
404             String publicKeyStr = pkr.data.value;
405             // String cleanPublicKeyStr = pkr.data.value.replaceAll("(\r\n|\n)", Constants.EMPTY);
406             String publicKeyPEM = publicKeyStr.replace("-----BEGIN PUBLIC KEY-----", "")
407                     .replaceAll(System.lineSeparator(), "").replace("-----END PUBLIC KEY-----", "");
408             byte[] encoded = Base64.getDecoder().decode(publicKeyPEM);
409             X509EncodedKeySpec spec = new X509EncodedKeySpec(encoded);
410
411             KeyFactory kf = KeyFactory.getInstance("RSA");
412             PublicKey publicKey = kf.generatePublic(spec);
413             // https://www.thexcoders.net/java-ciphers-rsa/
414             Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
415             cipher.init(Cipher.ENCRYPT_MODE, publicKey);
416             byte[] encryptedBytes = cipher.doFinal("Hello World".getBytes());
417             String encodedPassword = Base64.getEncoder().encodeToString(encryptedBytes);
418             logger.info(encodedPassword);
419         } catch (Exception e) {
420             assertTrue(false, "Excpetion: " + e.getMessage());
421         }
422     }
423 }