]> git.basschouten.com Git - openhab-addons.git/blob
1a340098b0ad6f9dcab034e86cb52969e8bf76ed
[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.icloud.internal;
14
15 import java.io.IOException;
16 import java.net.CookieManager;
17 import java.net.CookiePolicy;
18 import java.net.URI;
19 import java.net.http.HttpClient;
20 import java.net.http.HttpClient.Redirect;
21 import java.net.http.HttpClient.Version;
22 import java.net.http.HttpRequest;
23 import java.net.http.HttpRequest.BodyPublishers;
24 import java.net.http.HttpRequest.Builder;
25 import java.net.http.HttpResponse;
26 import java.net.http.HttpResponse.BodyHandlers;
27 import java.time.Duration;
28 import java.util.ArrayList;
29 import java.util.Arrays;
30 import java.util.List;
31
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.openhab.binding.icloud.internal.utilities.CustomCookieStore;
35 import org.openhab.binding.icloud.internal.utilities.JsonUtils;
36 import org.openhab.binding.icloud.internal.utilities.ListUtil;
37 import org.openhab.binding.icloud.internal.utilities.Pair;
38 import org.openhab.core.storage.Storage;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
41
42 /**
43  *
44  * Class to handle iCloud API session information for accessing the API.
45  *
46  * The implementation of this class is inspired by https://github.com/picklepete/pyicloud.
47  *
48  * @author Simon Spielmann - Initial contribution
49  */
50 @NonNullByDefault
51 public class ICloudSession {
52
53     private final Logger logger = LoggerFactory.getLogger(ICloudSession.class);
54
55     private final HttpClient client;
56
57     private List<Pair<String, String>> headers = new ArrayList<>();
58
59     private ICloudSessionData data = new ICloudSessionData();
60
61     private Storage<String> stateStorage;
62
63     private static final String SESSION_DATA_KEY = "SESSION_DATA";
64
65     /**
66      * The constructor.
67      *
68      * @param stateStorage Storage to persist session state.
69      */
70     public ICloudSession(Storage<String> stateStorage) {
71         String storedData = stateStorage.get(SESSION_DATA_KEY);
72         if (storedData != null) {
73             ICloudSessionData localSessionData = JsonUtils.fromJson(storedData, ICloudSessionData.class);
74             if (localSessionData != null) {
75                 data = localSessionData;
76             }
77         }
78         this.stateStorage = stateStorage;
79         client = HttpClient.newBuilder().version(Version.HTTP_1_1).followRedirects(Redirect.NORMAL)
80                 .connectTimeout(Duration.ofSeconds(20))
81                 .cookieHandler(new CookieManager(new CustomCookieStore(), CookiePolicy.ACCEPT_ALL)).build();
82     }
83
84     /**
85      * Invoke an HTTP POST request to the given url and body.
86      *
87      * @param url URL to call.
88      * @param body Body for the request
89      * @param overrideHeaders  If not null the given headers are used instead of the standard headers set via
90      *            {@link #setDefaultHeaders(Pair...)} (optional)
91      * @return Result body as {@link String}.
92      * @throws IOException if I/O error occurred
93      * @throws InterruptedException if this blocking request was interrupted
94      * @throws ICloudApiResponseException if the request failed (e.g. not OK HTTP return code)
95      */
96     public String post(String url, @Nullable String body, @Nullable List<Pair<String, String>> overrideHeaders)
97             throws IOException, InterruptedException, ICloudApiResponseException {
98         return request("POST", url, body, overrideHeaders);
99     }
100
101     /**
102      * Invoke an HTTP GET request to the given url.
103      *
104      * @param url URL to call.
105      * @param overrideHeaders  If not null the given headers are used to replace corresponding entries of the standard
106      *            headers set via
107      *            {@link #setDefaultHeaders(Pair...)}
108      * @return Result body as {@link String}.
109      * @throws IOException if I/O error occurred
110      * @throws InterruptedException if this blocking request was interrupted
111      * @throws ICloudApiResponseException if the request failed (e.g. not OK HTTP return code)
112      */
113     public String get(String url, List<Pair<String, String>> overrideHeaders)
114             throws IOException, InterruptedException, ICloudApiResponseException {
115         return request("GET", url, null, overrideHeaders);
116     }
117
118     private String request(String method, String url, @Nullable String body,
119             @Nullable List<Pair<String, String>> overrideHeaders)
120             throws IOException, InterruptedException, ICloudApiResponseException {
121         logger.debug("iCloud request {} {}.", method, url);
122
123         Builder builder = HttpRequest.newBuilder().uri(URI.create(url));
124
125         List<Pair<String, String>> requestHeaders = ListUtil.replaceEntries(this.headers, overrideHeaders);
126
127         for (Pair<String, String> header : requestHeaders) {
128             builder.header(header.getKey(), header.getValue());
129         }
130
131         if (body != null) {
132             builder.method(method, BodyPublishers.ofString(body));
133         }
134
135         HttpRequest request = builder.build();
136
137         logger.trace("Calling {}\nHeaders -----\n{}\nBody -----\n{}\n------\n", url, request.headers(), body);
138
139         HttpResponse<?> response = this.client.send(request, BodyHandlers.ofString());
140
141         Object responseBody = response.body();
142         String responseBodyAsString = responseBody != null ? responseBody.toString() : "";
143
144         logger.trace("Result {} {}\nHeaders -----\n{}\nBody -----\n{}\n------\n", url, response.statusCode(),
145                 response.headers(), responseBodyAsString);
146
147         if (response.statusCode() >= 300) {
148             throw new ICloudApiResponseException(url, response.statusCode());
149         }
150
151         // Store headers to reuse authentication
152         this.data.accountCountry = response.headers().firstValue("X-Apple-ID-Account-Country")
153                 .orElse(getAccountCountry());
154         this.data.sessionId = response.headers().firstValue("X-Apple-ID-Session-Id").orElse(getSessionId());
155         this.data.sessionToken = response.headers().firstValue("X-Apple-Session-Token").orElse(getSessionToken());
156         this.data.trustToken = response.headers().firstValue("X-Apple-TwoSV-Trust-Token").orElse(getTrustToken());
157         this.data.scnt = response.headers().firstValue("scnt").orElse(getScnt());
158
159         this.stateStorage.put(SESSION_DATA_KEY, JsonUtils.toJson(this.data));
160
161         return responseBodyAsString;
162     }
163
164     /**
165      * Sets default HTTP headers, for HTTP requests.
166      *
167      * @param headers HTTP headers to use for requests
168      */
169     @SafeVarargs
170     public final void setDefaultHeaders(Pair<String, String>... headers) {
171         this.headers = Arrays.asList(headers);
172     }
173
174     /**
175      * @return scnt
176      */
177     public @Nullable String getScnt() {
178         return data.scnt;
179     }
180
181     /**
182      * @return sessionId
183      */
184     public @Nullable String getSessionId() {
185         return data.sessionId;
186     }
187
188     /**
189      * @return sessionToken
190      */
191     public @Nullable String getSessionToken() {
192         return data.sessionToken;
193     }
194
195     /**
196      * @return trustToken
197      */
198     public @Nullable String getTrustToken() {
199         return data.trustToken;
200     }
201
202     /**
203      * @return {@code true} if session token is not empty.
204      */
205     public boolean hasToken() {
206         String sessionToken = data.sessionToken;
207         return sessionToken != null && !sessionToken.isEmpty();
208     }
209
210     /**
211      * @return accountCountry
212      */
213     public @Nullable String getAccountCountry() {
214         return data.accountCountry;
215     }
216
217     /**
218      *
219      * Internal class to encapsulate data required for iCloud authentication.
220      *
221      * @author Simon Spielmann Initial Contribution
222      */
223     private class ICloudSessionData {
224         @Nullable
225         String scnt;
226
227         @Nullable
228         String sessionId;
229
230         @Nullable
231         String sessionToken;
232
233         @Nullable
234         String trustToken;
235
236         @Nullable
237         String accountCountry;
238     }
239 }