2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.icloud.internal;
15 import java.io.IOException;
16 import java.net.CookieManager;
17 import java.net.CookiePolicy;
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;
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;
44 * Class to handle iCloud API session information for accessing the API.
46 * The implementation of this class is inspired by https://github.com/picklepete/pyicloud.
48 * @author Simon Spielmann - Initial contribution
51 public class ICloudSession {
53 private final Logger logger = LoggerFactory.getLogger(ICloudSession.class);
55 private final HttpClient client;
57 private List<Pair<String, String>> headers = new ArrayList<>();
59 private ICloudSessionData data = new ICloudSessionData();
61 private Storage<String> stateStorage;
63 private static final String SESSION_DATA_KEY = "SESSION_DATA";
68 * @param stateStorage Storage to persist session state.
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;
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();
85 * Invoke an HTTP POST request to the given url and body.
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)
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);
102 * Invoke an HTTP GET request to the given url.
104 * @param url URL to call.
105 * @param overrideHeaders If not null the given headers are used to replace corresponding entries of the standard
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)
113 public String get(String url, List<Pair<String, String>> overrideHeaders)
114 throws IOException, InterruptedException, ICloudApiResponseException {
115 return request("GET", url, null, overrideHeaders);
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);
123 Builder builder = HttpRequest.newBuilder().uri(URI.create(url));
125 List<Pair<String, String>> requestHeaders = ListUtil.replaceEntries(this.headers, overrideHeaders);
127 for (Pair<String, String> header : requestHeaders) {
128 builder.header(header.getKey(), header.getValue());
132 builder.method(method, BodyPublishers.ofString(body));
135 HttpRequest request = builder.build();
137 logger.trace("Calling {}\nHeaders -----\n{}\nBody -----\n{}\n------\n", url, request.headers(), body);
139 HttpResponse<?> response = this.client.send(request, BodyHandlers.ofString());
141 Object responseBody = response.body();
142 String responseBodyAsString = responseBody != null ? responseBody.toString() : "";
144 logger.trace("Result {} {}\nHeaders -----\n{}\nBody -----\n{}\n------\n", url, response.statusCode(),
145 response.headers(), responseBodyAsString);
147 if (response.statusCode() >= 300) {
148 throw new ICloudApiResponseException(url, response.statusCode());
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());
159 this.stateStorage.put(SESSION_DATA_KEY, JsonUtils.toJson(this.data));
161 return responseBodyAsString;
165 * Sets default HTTP headers, for HTTP requests.
167 * @param headers HTTP headers to use for requests
170 public final void setDefaultHeaders(Pair<String, String>... headers) {
171 this.headers = Arrays.asList(headers);
177 public @Nullable String getScnt() {
184 public @Nullable String getSessionId() {
185 return data.sessionId;
189 * @return sessionToken
191 public @Nullable String getSessionToken() {
192 return data.sessionToken;
198 public @Nullable String getTrustToken() {
199 return data.trustToken;
203 * @return {@code true} if session token is not empty.
205 public boolean hasToken() {
206 String sessionToken = data.sessionToken;
207 return sessionToken != null && !sessionToken.isEmpty();
211 * @return accountCountry
213 public @Nullable String getAccountCountry() {
214 return data.accountCountry;
219 * Internal class to encapsulate data required for iCloud authentication.
221 * @author Simon Spielmann Initial Contribution
223 private class ICloudSessionData {
237 String accountCountry;