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