]> git.basschouten.com Git - openhab-addons.git/blob
0223226469b72e021747eb102e5d0ed03c2ebb30
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.salus.internal.aws.http;
14
15 import static java.nio.charset.StandardCharsets.UTF_8;
16 import static java.time.ZoneOffset.UTC;
17 import static java.util.Objects.requireNonNull;
18
19 import java.time.Clock;
20 import java.time.LocalDateTime;
21 import java.time.ZoneId;
22 import java.time.ZonedDateTime;
23 import java.util.List;
24 import java.util.SortedSet;
25 import java.util.TreeSet;
26 import java.util.concurrent.ExecutionException;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.salus.internal.rest.AbstractSalusApi;
31 import org.openhab.binding.salus.internal.rest.Device;
32 import org.openhab.binding.salus.internal.rest.DeviceProperty;
33 import org.openhab.binding.salus.internal.rest.GsonMapper;
34 import org.openhab.binding.salus.internal.rest.RestClient;
35 import org.openhab.binding.salus.internal.rest.exceptions.AuthSalusApiException;
36 import org.openhab.binding.salus.internal.rest.exceptions.SalusApiException;
37 import org.openhab.binding.salus.internal.rest.exceptions.UnsuportedSalusApiException;
38 import org.openhab.core.io.net.http.HttpClientFactory;
39
40 import software.amazon.awssdk.crt.auth.credentials.StaticCredentialsProvider;
41 import software.amazon.awssdk.crt.auth.signing.AwsSigner;
42 import software.amazon.awssdk.crt.auth.signing.AwsSigningConfig;
43 import software.amazon.awssdk.crt.auth.signing.AwsSigningResult;
44 import software.amazon.awssdk.crt.http.HttpHeader;
45 import software.amazon.awssdk.crt.http.HttpRequest;
46
47 /**
48  * The SalusApi class is responsible for interacting with a REST API to perform various operations related to the Salus
49  * system. It handles authentication, token management, and provides methods to retrieve and manipulate device
50  * information and properties.
51  *
52  * @author Martin GrzeĊ›lowski - Initial contribution
53  */
54 @NonNullByDefault
55 public class AwsSalusApi extends AbstractSalusApi<Authentication> {
56     private final AuthenticationHelper authenticationHelper;
57     private final String companyCode;
58     private final String awsService;
59     private final String region;
60     @Nullable
61     CogitoCredentials cogitoCredentials;
62
63     private AwsSalusApi(String username, byte[] password, String baseUrl, RestClient restClient, GsonMapper mapper,
64             Clock clock, AuthenticationHelper authenticationHelper, String companyCode, String awsService,
65             String region) {
66         super(username, password, baseUrl, restClient, mapper, clock);
67         this.authenticationHelper = authenticationHelper;
68         this.companyCode = companyCode;
69         this.awsService = awsService;
70         this.region = region;
71     }
72
73     public AwsSalusApi(HttpClientFactory httpClientFactory, String username, byte[] password, String baseUrl,
74             RestClient restClient, GsonMapper gsonMapper, String userPoolId, String identityPoolId, String clientId,
75             String region, String companyCode, String awsService) {
76         this(username, password, baseUrl, restClient, gsonMapper, Clock.systemDefaultZone(),
77                 new AuthenticationHelper(httpClientFactory, userPoolId, clientId, region, identityPoolId), companyCode,
78                 awsService, region);
79     }
80
81     @Override
82     protected void login() throws AuthSalusApiException {
83         logger.debug("Login with username '{}'", username);
84         var result = authenticationHelper.performSrpAuthentication(username, new String(password, UTF_8));
85         var localAuth = authentication = new Authentication(result.getAccessToken(), result.getExpiresIn(),
86                 result.getTokenType(), result.getRefreshToken(), result.getIdToken());
87         var local = LocalDateTime.now(clock).plusSeconds(localAuth.expiresIn())
88                 // this is to account that there is a delay between server setting `expires_in`
89                 // and client (OpenHAB) receiving it
90                 .minusSeconds(TOKEN_EXPIRE_TIME_ADJUSTMENT_SECONDS);
91         var localExpireTime = authTokenExpireTime = ZonedDateTime.of(local, UTC);
92
93         var id = authenticationHelper.getId(result);
94
95         var cogito = authenticationHelper.getCredentialsForIdentity(result, id.getIdentityId());
96         cogitoCredentials = new CogitoCredentials(//
97                 cogito.getCredentials().getAccessKeyId(), //
98                 cogito.getCredentials().getSecretKey(), //
99                 cogito.getCredentials().getSessionToken());
100
101         var cogitoExpirationTime = cogito.getCredentials().getExpiration();
102         if (cogitoExpirationTime.isBefore(localExpireTime.toInstant())) {
103             authTokenExpireTime = ZonedDateTime.ofInstant(cogitoExpirationTime, UTC);
104         }
105     }
106
107     @Override
108     protected void cleanAuth() {
109         super.cleanAuth();
110         cogitoCredentials = null;
111     }
112
113     @Override
114     public SortedSet<Device> findDevices() throws AuthSalusApiException, SalusApiException {
115         var result = new TreeSet<Device>();
116         var gateways = findGateways();
117         for (var gatewayId : gateways) {
118             var response = get(url("/api/v1/occupants/slider_details?id=%s&type=gateway".formatted(gatewayId)),
119                     authHeaders());
120             if (response == null) {
121                 continue;
122             }
123             result.addAll(mapper.parseAwsDevices(response));
124         }
125         return result;
126     }
127
128     private List<String> findGateways() throws SalusApiException, AuthSalusApiException {
129         var response = get(url("/api/v1/occupants/slider_list"), authHeaders());
130         if (response == null) {
131             return List.of();
132         }
133         return mapper.parseAwsGatewayIds(response);
134     }
135
136     private RestClient.Header[] authHeaders() throws AuthSalusApiException {
137         refreshAccessToken();
138         return new RestClient.Header[] {
139                 new RestClient.Header("x-access-token", requireNonNull(authentication).accessToken()),
140                 new RestClient.Header("x-auth-token", requireNonNull(authentication).idToken()),
141                 new RestClient.Header("x-company-code", companyCode) };
142     }
143
144     @Override
145     public SortedSet<DeviceProperty<?>> findDeviceProperties(String dsn)
146             throws SalusApiException, AuthSalusApiException {
147         var path = "https://%s.iot.%s.amazonaws.com/things/%s/shadow".formatted(awsService, region, dsn);
148         var time = ZonedDateTime.now(clock).withZoneSameInstant(ZoneId.of("UTC"));
149         var signingResult = buildSigningResult(dsn, time);
150         var headers = signingResult.getSignedRequest()//
151                 .getHeaders()//
152                 .stream()//
153                 .map(header -> new RestClient.Header(header.getName(), header.getValue()))//
154                 .toList()//
155                 .toArray(new RestClient.Header[0]);
156         var response = get(path, headers);
157         if (response == null) {
158             return new TreeSet<>();
159         }
160
161         return new TreeSet<>(mapper.parseAwsDeviceProperties(response));
162     }
163
164     private AwsSigningResult buildSigningResult(String dsn, ZonedDateTime time)
165             throws SalusApiException, AuthSalusApiException {
166         refreshAccessToken();
167         HttpRequest httpRequest = new HttpRequest("GET", "/things/%s/shadow".formatted(dsn),
168                 new HttpHeader[] { new HttpHeader("host", "") }, null);
169         var localCredentials = requireNonNull(cogitoCredentials);
170         try (var config = new AwsSigningConfig()) {
171             config.setRegion(region);
172             config.setService("iotdevicegateway");
173             config.setCredentialsProvider(new StaticCredentialsProvider.StaticCredentialsProviderBuilder()
174                     .withAccessKeyId(localCredentials.accessKeyId().getBytes(UTF_8))
175                     .withSecretAccessKey(localCredentials.secretKey().getBytes(UTF_8))
176                     .withSessionToken(localCredentials.sessionToken().getBytes(UTF_8)).build());
177             config.setTime(time.toInstant().toEpochMilli());
178             return AwsSigner.sign(httpRequest, config).get();
179         } catch (ExecutionException | InterruptedException e) {
180             throw new SalusApiException("Cannot build AWS signature!", e);
181         }
182     }
183
184     @Override
185     public Object setValueForProperty(String dsn, String propertyName, Object value) throws SalusApiException {
186         throw new UnsuportedSalusApiException("Setting value is not supported for AWS bridge");
187     }
188 }