2 * Copyright (c) 2010-2022 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.unifi.internal.api;
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;
23 import java.util.Map.Entry;
24 import java.util.concurrent.ExecutionException;
25 import java.util.concurrent.TimeUnit;
26 import java.util.concurrent.TimeoutException;
28 import javax.net.ssl.SSLException;
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;
47 import com.google.gson.Gson;
48 import com.google.gson.GsonBuilder;
49 import com.google.gson.JsonObject;
50 import com.google.gson.JsonParser;
53 * The {@link UniFiControllerRequest} encapsulates a request sent by the {@link UniFiController}.
55 * @author Matthew Bowman - Initial contribution
57 * @param <T> The response type expected as a result of the request's execution
60 class UniFiControllerRequest<T> {
62 private static final String CONTENT_TYPE_APPLICATION_JSON_UTF_8 = MimeTypes.Type.APPLICATION_JSON_UTF_8.asString();
64 private static final long TIMEOUT_SECONDS = 5;
66 private static final String PROPERTY_DATA = "data";
68 private final Logger logger = LoggerFactory.getLogger(UniFiControllerRequest.class);
70 private final Gson gson;
72 private final HttpClient httpClient;
74 private final String host;
76 private final int port;
78 private final boolean unifios;
80 private final HttpMethod method;
82 private String path = "/";
84 private String csrfToken;
86 private final Map<String, String> queryParameters = new HashMap<>();
88 private final Map<String, Object> bodyParameters = new HashMap<>();
90 private final Class<T> resultType;
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;
98 this.httpClient = httpClient;
102 this.csrfToken = csrfToken;
103 this.unifios = unifios;
106 public void setAPIPath(final String relativePath) {
108 this.path = "/proxy/network" + relativePath;
110 this.path = relativePath;
114 public void setPath(final String path) {
118 public void setBodyParameter(final String key, final Object value) {
119 this.bodyParameters.put(key, value);
122 public void setQueryParameter(final String key, final Object value) {
123 this.queryParameters.put(key, String.valueOf(value));
126 public @Nullable T execute() throws UniFiException {
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();
133 if (jsonObject.has(PROPERTY_DATA) && jsonObject.get(PROPERTY_DATA).isJsonArray()) {
134 result = gson.fromJson(jsonObject.getAsJsonArray(PROPERTY_DATA), resultType);
142 private String getContent() throws UniFiException {
144 final InputStreamResponseListener listener = new InputStreamResponseListener();
145 final Response response = getContentResponse(listener);
146 final int status = response.getStatus();
148 case HttpStatus.OK_200:
149 content = responseToString(listener);
150 if (logger.isTraceEnabled()) {
151 logger.trace("<< {} {} \n{}", status, HttpStatus.getMessage(status), prettyPrintJson(content));
154 final String csrfToken = response.getHeaders().get("X-CSRF-Token");
155 if (csrfToken != null && !csrfToken.isEmpty()) {
156 this.csrfToken = csrfToken;
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");
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");
173 private Response getContentResponse(final InputStreamResponseListener listener) throws UniFiException {
174 final Request request = newRequest();
175 logger.trace(">> {} {}", request.getMethod(), request.getURI());
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) {
187 throw new UniFiInvalidHostException(cause);
188 } else if (cause instanceof ConnectException) {
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();
203 throw new UniFiException(cause);
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);
216 return new String(responseContent.toByteArray(), StandardCharsets.UTF_8);
219 public String getCsrfToken() {
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)
227 for (final Entry<String, String> entry : queryParameters.entrySet()) {
228 request.param(entry.getKey(), entry.getValue());
230 if (!bodyParameters.isEmpty()) {
231 final String jsonBody = gson.toJson(bodyParameters);
233 logger.debug("Body parameters for request '{}': {}", request.getPath(), jsonBody);
235 new StringContentProvider(CONTENT_TYPE_APPLICATION_JSON_UTF_8, jsonBody, StandardCharsets.UTF_8));
238 if (!csrfToken.isEmpty()) {
239 request.header("x-csrf-token", this.csrfToken);
245 private static String prettyPrintJson(final String content) {
247 final JsonObject json = JsonParser.parseString(content).getAsJsonObject();
248 final Gson prettyGson = new GsonBuilder().setPrettyPrinting().create();
250 return prettyGson.toJson(json);
251 } catch (final RuntimeException e) {
252 // If could not parse the string as json, just return the string