]> git.basschouten.com Git - openhab-addons.git/blob
fc0439e3b5978af7caae41d033763ca97300ecf0
[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.unifi.internal.api;
14
15 import java.io.ByteArrayOutputStream;
16 import java.io.IOException;
17 import java.io.InputStream;
18 import java.net.ConnectException;
19 import java.net.UnknownHostException;
20 import java.nio.charset.StandardCharsets;
21 import java.util.HashMap;
22 import java.util.Map;
23 import java.util.Map.Entry;
24 import java.util.concurrent.ExecutionException;
25 import java.util.concurrent.TimeUnit;
26 import java.util.concurrent.TimeoutException;
27
28 import javax.net.ssl.SSLException;
29
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.eclipse.jetty.client.HttpClient;
33 import org.eclipse.jetty.client.HttpResponseException;
34 import org.eclipse.jetty.client.api.ContentResponse;
35 import org.eclipse.jetty.client.api.Request;
36 import org.eclipse.jetty.client.api.Response;
37 import org.eclipse.jetty.client.util.InputStreamResponseListener;
38 import org.eclipse.jetty.client.util.StringContentProvider;
39 import org.eclipse.jetty.http.HttpMethod;
40 import org.eclipse.jetty.http.HttpScheme;
41 import org.eclipse.jetty.http.HttpStatus;
42 import org.eclipse.jetty.http.HttpURI;
43 import org.eclipse.jetty.http.MimeTypes;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
46
47 import com.google.gson.Gson;
48 import com.google.gson.GsonBuilder;
49 import com.google.gson.JsonObject;
50 import com.google.gson.JsonParseException;
51 import com.google.gson.JsonParser;
52
53 /**
54  * The {@link UniFiControllerRequest} encapsulates a request sent by the {@link UniFiController}.
55  *
56  * @author Matthew Bowman - Initial contribution
57  *
58  * @param <T> The response type expected as a result of the request's execution
59  */
60 @NonNullByDefault
61 class UniFiControllerRequest<T> {
62
63     private static final String CONTROLLER_PARSE_ERROR = "@text/error.controller.parse_error";
64
65     private static final String CONTENT_TYPE_APPLICATION_JSON_UTF_8 = MimeTypes.Type.APPLICATION_JSON_UTF_8.asString();
66
67     private static final long TIMEOUT_SECONDS = 5;
68
69     private static final String PROPERTY_DATA = "data";
70
71     private final Logger logger = LoggerFactory.getLogger(UniFiControllerRequest.class);
72
73     private final Gson gson;
74
75     private final HttpClient httpClient;
76
77     private final String host;
78
79     private final int port;
80
81     private final boolean unifios;
82
83     private final HttpMethod method;
84
85     private String path = "/";
86
87     private String csrfToken;
88
89     private final Map<String, String> queryParameters = new HashMap<>();
90
91     private final Map<String, Object> bodyParameters = new HashMap<>();
92
93     private final Class<T> resultType;
94
95     // Public API
96
97     public UniFiControllerRequest(final Class<T> resultType, final Gson gson, final HttpClient httpClient,
98             final HttpMethod method, final String host, final int port, final String csrfToken, final boolean unifios) {
99         this.resultType = resultType;
100         this.gson = gson;
101         this.httpClient = httpClient;
102         this.method = method;
103         this.host = host;
104         this.port = port;
105         this.csrfToken = csrfToken;
106         this.unifios = unifios;
107     }
108
109     public void setAPIPath(final String relativePath) {
110         if (unifios) {
111             this.path = "/proxy/network" + relativePath;
112         } else {
113             this.path = relativePath;
114         }
115     }
116
117     public void setPath(final String path) {
118         this.path = path;
119     }
120
121     public void setBodyParameter(final String key, final Object value) {
122         this.bodyParameters.put(key, value);
123     }
124
125     public void setQueryParameter(final String key, final Object value) {
126         this.queryParameters.put(key, String.valueOf(value));
127     }
128
129     public @Nullable T execute() throws UniFiException {
130         T result = (T) null;
131         final String json = getContent();
132         // mgb: only try and unmarshall non-void result types
133         if (!Void.class.equals(resultType)) {
134             try {
135                 final JsonObject jsonObject = JsonParser.parseString(json).getAsJsonObject();
136
137                 if (jsonObject.has(PROPERTY_DATA) && jsonObject.get(PROPERTY_DATA).isJsonArray()) {
138                     result = (T) gson.fromJson(jsonObject.getAsJsonArray(PROPERTY_DATA), resultType);
139                 }
140             } catch (final JsonParseException e) {
141                 logger.debug(
142                         "Could not parse content retrieved from the server. Is the configuration pointing to the right server/port?, {}",
143                         e.getMessage());
144                 if (logger.isTraceEnabled()) {
145                     prettyPrintJson(json);
146                 }
147                 throw new UniFiCommunicationException(CONTROLLER_PARSE_ERROR);
148             }
149         }
150         return result;
151     }
152
153     // Private API
154
155     private String getContent() throws UniFiException {
156         String content;
157         final InputStreamResponseListener listener = new InputStreamResponseListener();
158         final Response response = getContentResponse(listener);
159         final int status = response.getStatus();
160         switch (status) {
161             case HttpStatus.OK_200:
162                 content = responseToString(listener);
163                 if (logger.isTraceEnabled()) {
164                     logger.trace("<< {} {} \n{}", status, HttpStatus.getMessage(status), prettyPrintJson(content));
165                 }
166
167                 final String csrfToken = response.getHeaders().get("X-CSRF-Token");
168                 if (csrfToken != null && !csrfToken.isEmpty()) {
169                     this.csrfToken = csrfToken;
170                 }
171                 break;
172             case HttpStatus.BAD_REQUEST_400:
173                 logger.info("UniFi returned a status 400: {}", prettyPrintJson(responseToString(listener)));
174                 throw new UniFiInvalidCredentialsException("Invalid Credentials");
175             case HttpStatus.UNAUTHORIZED_401:
176                 throw new UniFiExpiredSessionException("Expired Credentials");
177             case HttpStatus.FORBIDDEN_403:
178                 throw new UniFiNotAuthorizedException("Unauthorized Access");
179             default:
180                 logger.info("UniFi returned a status code {}: {}", status, prettyPrintJson(responseToString(listener)));
181                 throw new UniFiException("Unknown HTTP status code " + status + " returned by the controller");
182         }
183         return content;
184     }
185
186     private Response getContentResponse(final InputStreamResponseListener listener) throws UniFiException {
187         final Request request = newRequest();
188         logger.trace(">> {} {}", request.getMethod(), request.getURI());
189         Response response;
190         try {
191             request.send(listener);
192             response = listener.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
193         } catch (TimeoutException | InterruptedException e) {
194             throw new UniFiCommunicationException(e);
195         } catch (final ExecutionException e) {
196             // mgb: unwrap the cause and try to cleanly handle it
197             final Throwable cause = e.getCause();
198             if (cause instanceof TimeoutException) {
199                 throw new UniFiCommunicationException(e);
200             } else if (cause instanceof UnknownHostException) {
201                 // invalid hostname
202                 throw new UniFiInvalidHostException(cause);
203             } else if (cause instanceof ConnectException) {
204                 // cannot connect
205                 throw new UniFiCommunicationException(cause);
206             } else if (cause instanceof SSLException) {
207                 // cannot establish ssl connection
208                 throw new UniFiSSLException(cause);
209             } else if (cause instanceof HttpResponseException httpResponseException
210                     && httpResponseException.getResponse() instanceof ContentResponse) {
211                 // the UniFi controller violates the HTTP protocol
212                 // - it returns 401 UNAUTHORIZED without the WWW-Authenticate response header
213                 // - this causes an ExecutionException to be thrown
214                 // - we unwrap the response from the exception for proper handling of the 401 status code
215                 response = httpResponseException.getResponse();
216             } else {
217                 // catch all
218                 throw new UniFiException(cause);
219             }
220         }
221         return response;
222     }
223
224     private static String responseToString(final InputStreamResponseListener listener) throws UniFiException {
225         final ByteArrayOutputStream responseContent = new ByteArrayOutputStream();
226         try (InputStream input = listener.getInputStream()) {
227             input.transferTo(responseContent);
228         } catch (final IOException e) {
229             throw new UniFiException(e);
230         }
231         return new String(responseContent.toByteArray(), StandardCharsets.UTF_8);
232     }
233
234     public String getCsrfToken() {
235         return csrfToken;
236     }
237
238     private Request newRequest() {
239         final HttpURI uri = new HttpURI(HttpScheme.HTTPS.asString(), host, port, path);
240         final Request request = httpClient.newRequest(uri.toString()).timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
241                 .method(method);
242         for (final Entry<String, String> entry : queryParameters.entrySet()) {
243             request.param(entry.getKey(), entry.getValue());
244         }
245         if (!bodyParameters.isEmpty()) {
246             final String jsonBody = gson.toJson(bodyParameters);
247
248             logger.debug("Body parameters for request '{}': {}", request.getPath(), jsonBody);
249             request.content(
250                     new StringContentProvider(CONTENT_TYPE_APPLICATION_JSON_UTF_8, jsonBody, StandardCharsets.UTF_8));
251         }
252
253         if (!csrfToken.isEmpty()) {
254             request.header("x-csrf-token", this.csrfToken);
255         }
256
257         return request;
258     }
259
260     private String prettyPrintJson(final String content) {
261         try {
262             final JsonObject json = JsonParser.parseString(content).getAsJsonObject();
263             final Gson prettyGson = new GsonBuilder().setPrettyPrinting().create();
264
265             return prettyGson.toJson(json);
266         } catch (final RuntimeException e) {
267             logger.debug("RuntimeException pretty printing JSON. Returning the raw content.", e);
268             // If could not parse the string as JSON, just return the string
269             return content;
270         }
271     }
272 }