]> git.basschouten.com Git - openhab-addons.git/blob
b5185fef8edecd9d9d94d94cf6a1dcaa98697c1c
[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.model;
14
15 import java.net.ConnectException;
16 import java.net.UnknownHostException;
17 import java.nio.charset.StandardCharsets;
18 import java.util.HashMap;
19 import java.util.Map;
20 import java.util.Map.Entry;
21 import java.util.concurrent.ExecutionException;
22 import java.util.concurrent.TimeUnit;
23 import java.util.concurrent.TimeoutException;
24
25 import javax.net.ssl.SSLException;
26
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;
49
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;
57
58 /**
59  * The {@link UniFiControllerRequest} encapsulates a request sent by the {@link UniFiController}.
60  *
61  * @author Matthew Bowman - Initial contribution
62  *
63  * @param <T> The response type expected as a result of the request's execution
64  */
65 @NonNullByDefault
66 public class UniFiControllerRequest<T> {
67
68     private static final String CONTENT_TYPE_APPLICATION_JSON = MimeTypes.Type.APPLICATION_JSON.asString();
69
70     private static final long TIMEOUT_SECONDS = 5;
71
72     private static final String PROPERTY_DATA = "data";
73
74     private final Logger logger = LoggerFactory.getLogger(UniFiControllerRequest.class);
75
76     private final Gson gson;
77
78     private final HttpClient httpClient;
79
80     private final String host;
81
82     private final int port;
83
84     private String path = "/";
85
86     private final boolean unifios;
87
88     private String csrfToken;
89
90     private Map<String, String> queryParameters = new HashMap<>();
91
92     private Map<String, String> bodyParameters = new HashMap<>();
93
94     private final Class<T> resultType;
95
96     // Public API
97
98     public UniFiControllerRequest(Class<T> resultType, Gson gson, HttpClient httpClient, String host, int port,
99             String csrfToken, boolean unifios) {
100         this.resultType = resultType;
101         this.gson = gson;
102         this.httpClient = httpClient;
103         this.host = host;
104         this.port = port;
105         this.csrfToken = csrfToken;
106         this.unifios = unifios;
107     }
108
109     public void setAPIPath(String relativePath) {
110         if (unifios) {
111             this.path = "/proxy/network" + relativePath;
112         } else {
113             this.path = relativePath;
114         }
115     }
116
117     public void setPath(String path) {
118         this.path = path;
119     }
120
121     public void setBodyParameter(String key, Object value) {
122         this.bodyParameters.put(key, String.valueOf(value));
123     }
124
125     public void setQueryParameter(String key, Object value) {
126         this.queryParameters.put(key, String.valueOf(value));
127     }
128
129     public @Nullable T execute() throws UniFiException {
130         T result = null;
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);
137             }
138         }
139         return result;
140     }
141
142     // Private API
143
144     private String getContent() throws UniFiException {
145         String content;
146         ContentResponse response = getContentResponse();
147         int status = response.getStatus();
148         switch (status) {
149             case HttpStatus.OK_200:
150                 content = response.getContentAsString();
151                 if (logger.isTraceEnabled()) {
152                     logger.trace("<< {} {} \n{}", status, HttpStatus.getMessage(status), prettyPrintJson(content));
153                 }
154
155                 String csrfToken = response.getHeaders().get("X-CSRF-Token");
156                 if (csrfToken != null && !csrfToken.isEmpty()) {
157                     this.csrfToken = csrfToken;
158                 }
159                 break;
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");
166             default:
167                 throw new UniFiException("Unknown HTTP status code " + status + " returned by the controller");
168         }
169         return content;
170     }
171
172     private ContentResponse getContentResponse() throws UniFiException {
173         Request request = newRequest();
174         logger.trace(">> {} {}", request.getMethod(), request.getURI());
175         ContentResponse response;
176         try {
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) {
184                 // invalid hostname
185                 throw new UniFiInvalidHostException(cause);
186             } else if (cause instanceof ConnectException) {
187                 // cannot connect
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();
199             } else {
200                 // catch all
201                 throw new UniFiException(cause);
202             }
203         }
204         return response;
205     }
206
207     public String getCsrfToken() {
208         return csrfToken;
209     }
210
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)
215                 .method(method);
216         for (Entry<String, String> entry : queryParameters.entrySet()) {
217             request.param(entry.getKey(), entry.getValue());
218         }
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);
224         }
225
226         if (!csrfToken.isEmpty()) {
227             request.header("x-csrf-token", this.csrfToken);
228         }
229
230         return request;
231     }
232
233     private String getRequestBodyAsJson() {
234         JsonObject jsonObject = new JsonObject();
235         JsonElement jsonElement = null;
236         for (Entry<String, String> entry : bodyParameters.entrySet()) {
237             try {
238                 jsonElement = JsonParser.parseString(entry.getValue());
239             } catch (JsonSyntaxException e) {
240                 jsonElement = new JsonPrimitive(entry.getValue());
241             }
242             jsonObject.add(entry.getKey(), jsonElement);
243         }
244         return jsonObject.toString();
245     }
246
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);
251     }
252 }