2 * Copyright (c) 2010-2021 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.model;
15 import java.net.ConnectException;
16 import java.net.UnknownHostException;
17 import java.nio.charset.StandardCharsets;
18 import java.util.HashMap;
20 import java.util.Map.Entry;
21 import java.util.concurrent.ExecutionException;
22 import java.util.concurrent.TimeUnit;
23 import java.util.concurrent.TimeoutException;
25 import javax.net.ssl.SSLException;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.eclipse.jetty.client.HttpClient;
30 import org.eclipse.jetty.client.HttpResponseException;
31 import org.eclipse.jetty.client.api.ContentProvider;
32 import org.eclipse.jetty.client.api.ContentResponse;
33 import org.eclipse.jetty.client.api.Request;
34 import org.eclipse.jetty.client.util.StringContentProvider;
35 import org.eclipse.jetty.http.HttpMethod;
36 import org.eclipse.jetty.http.HttpScheme;
37 import org.eclipse.jetty.http.HttpStatus;
38 import org.eclipse.jetty.http.HttpURI;
39 import org.eclipse.jetty.http.MimeTypes;
40 import org.openhab.binding.unifi.internal.api.UniFiCommunicationException;
41 import org.openhab.binding.unifi.internal.api.UniFiException;
42 import org.openhab.binding.unifi.internal.api.UniFiExpiredSessionException;
43 import org.openhab.binding.unifi.internal.api.UniFiInvalidCredentialsException;
44 import org.openhab.binding.unifi.internal.api.UniFiInvalidHostException;
45 import org.openhab.binding.unifi.internal.api.UniFiNotAuthorizedException;
46 import org.openhab.binding.unifi.internal.api.UniFiSSLException;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
50 import com.google.gson.Gson;
51 import com.google.gson.GsonBuilder;
52 import com.google.gson.JsonElement;
53 import com.google.gson.JsonObject;
54 import com.google.gson.JsonParser;
55 import com.google.gson.JsonPrimitive;
56 import com.google.gson.JsonSyntaxException;
59 * The {@link UniFiControllerRequest} encapsulates a request sent by the {@link UniFiController}.
61 * @author Matthew Bowman - Initial contribution
63 * @param <T> The response type expected as a result of the request's execution
66 public class UniFiControllerRequest<T> {
68 private static final String CONTENT_TYPE_APPLICATION_JSON = MimeTypes.Type.APPLICATION_JSON.asString();
70 private static final long TIMEOUT_SECONDS = 5;
72 private static final String PROPERTY_DATA = "data";
74 private final Logger logger = LoggerFactory.getLogger(UniFiControllerRequest.class);
76 private final Gson gson;
78 private final HttpClient httpClient;
80 private final String host;
82 private final int port;
84 private String path = "/";
86 private final boolean unifios;
88 private String csrfToken;
90 private Map<String, String> queryParameters = new HashMap<>();
92 private Map<String, String> bodyParameters = new HashMap<>();
94 private final Class<T> resultType;
98 public UniFiControllerRequest(Class<T> resultType, Gson gson, HttpClient httpClient, String host, int port,
99 String csrfToken, boolean unifios) {
100 this.resultType = resultType;
102 this.httpClient = httpClient;
105 this.csrfToken = csrfToken;
106 this.unifios = unifios;
109 public void setAPIPath(String relativePath) {
111 this.path = "/proxy/network" + relativePath;
113 this.path = relativePath;
117 public void setPath(String path) {
121 public void setBodyParameter(String key, Object value) {
122 this.bodyParameters.put(key, String.valueOf(value));
125 public void setQueryParameter(String key, Object value) {
126 this.queryParameters.put(key, String.valueOf(value));
129 public @Nullable T execute() throws UniFiException {
131 String json = getContent();
132 // mgb: only try and unmarshall non-void result types
133 if (!Void.class.equals(resultType)) {
134 JsonObject jsonObject = JsonParser.parseString(json).getAsJsonObject();
135 if (jsonObject.has(PROPERTY_DATA) && jsonObject.get(PROPERTY_DATA).isJsonArray()) {
136 result = gson.fromJson(jsonObject.getAsJsonArray(PROPERTY_DATA), resultType);
144 private String getContent() throws UniFiException {
146 ContentResponse response = getContentResponse();
147 int status = response.getStatus();
149 case HttpStatus.OK_200:
150 content = response.getContentAsString();
151 if (logger.isTraceEnabled()) {
152 logger.trace("<< {} {} \n{}", status, HttpStatus.getMessage(status), prettyPrintJson(content));
155 String csrfToken = response.getHeaders().get("X-CSRF-Token");
156 if (csrfToken != null && !csrfToken.isEmpty()) {
157 this.csrfToken = csrfToken;
160 case HttpStatus.BAD_REQUEST_400:
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 throw new UniFiException("Unknown HTTP status code " + status + " returned by the controller");
172 private ContentResponse getContentResponse() throws UniFiException {
173 Request request = newRequest();
174 logger.trace(">> {} {}", request.getMethod(), request.getURI());
175 ContentResponse response;
177 response = request.send();
178 } catch (TimeoutException | InterruptedException e) {
179 throw new UniFiCommunicationException(e);
180 } catch (ExecutionException e) {
181 // mgb: unwrap the cause and try to cleanly handle it
182 Throwable cause = e.getCause();
183 if (cause instanceof UnknownHostException) {
185 throw new UniFiInvalidHostException(cause);
186 } else if (cause instanceof ConnectException) {
188 throw new UniFiCommunicationException(cause);
189 } else if (cause instanceof SSLException) {
190 // cannot establish ssl connection
191 throw new UniFiSSLException(cause);
192 } else if (cause instanceof HttpResponseException
193 && ((HttpResponseException) cause).getResponse() instanceof ContentResponse) {
194 // the UniFi controller violates the HTTP protocol
195 // - it returns 401 UNAUTHORIZED without the WWW-Authenticate response header
196 // - this causes an ExceptionException to be thrown
197 // - we unwrap the response from the exception for proper handling of the 401 status code
198 response = (ContentResponse) ((HttpResponseException) cause).getResponse();
201 throw new UniFiException(cause);
207 public String getCsrfToken() {
211 private Request newRequest() {
212 HttpMethod method = bodyParameters.isEmpty() ? HttpMethod.GET : HttpMethod.POST;
213 HttpURI uri = new HttpURI(HttpScheme.HTTPS.asString(), host, port, path);
214 Request request = httpClient.newRequest(uri.toString()).timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
216 for (Entry<String, String> entry : queryParameters.entrySet()) {
217 request.param(entry.getKey(), entry.getValue());
219 if (!bodyParameters.isEmpty()) {
220 String jsonBody = getRequestBodyAsJson();
221 ContentProvider content = new StringContentProvider(CONTENT_TYPE_APPLICATION_JSON, jsonBody,
222 StandardCharsets.UTF_8);
223 request = request.content(content);
226 if (!csrfToken.isEmpty()) {
227 request.header("x-csrf-token", this.csrfToken);
233 private String getRequestBodyAsJson() {
234 JsonObject jsonObject = new JsonObject();
235 JsonElement jsonElement = null;
236 for (Entry<String, String> entry : bodyParameters.entrySet()) {
238 jsonElement = JsonParser.parseString(entry.getValue());
239 } catch (JsonSyntaxException e) {
240 jsonElement = new JsonPrimitive(entry.getValue());
242 jsonObject.add(entry.getKey(), jsonElement);
244 return jsonObject.toString();
247 private static String prettyPrintJson(String content) {
248 JsonObject json = JsonParser.parseString(content).getAsJsonObject();
249 Gson prettyGson = new GsonBuilder().setPrettyPrinting().create();
250 return prettyGson.toJson(json);