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