Skip to content

Commit

Permalink
[remoteopenhab] Connection to the remote server through openHAB Cloud
Browse files Browse the repository at this point in the history
Add new configuration settings username, password and authenticateAnyway

Fix the default REST path to avoid unnecessary request redirects

Provide the access token to requests only when necessary

Fix openhab#10055

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
  • Loading branch information
lolodomo committed Feb 13, 2021
1 parent 3386c47 commit 42f9cb5
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 50 deletions.
9 changes: 7 additions & 2 deletions bundles/org.openhab.binding.remoteopenhab/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,22 +42,27 @@ The `server` thing has the following configuration parameters:
| useHttps | no | Set to true if you want to use HTTPS to communicate with the remote openHAB server. Default is false. |
| port | yes | The HTTP port to use to communicate with the remote openHAB server. Default is 8080. |
| trustedCertificate | no | Set to true if you want to use HTTPS even without a valid SSL certificate provided by your remote server. |
| restPath | yes | The subpath of the REST API on the remote openHAB server. Default is /rest |
| restPath | yes | The subpath of the REST API on the remote openHAB server. Default is "/rest/" |
| token | no | The token to use when the remote openHAB server is setup to require authorization to run its REST API. |
| username | no | The username to use when the remote openHAB server is setup to require basic authorization to run its REST API. |
| password | no | The password to use when the remote openHAB server is setup to require basic authorization to run its REST API. |
| authenticateAnyway | no | Set it to true in case you want to pass authentication information even when the communicate with the remote openHAB server is not secured (only HTTP). This is of course not recommended especially if your connection is over the Internet. Default is false. |
| accessibilityInterval | no | Minutes between checking the remote server accessibility. 0 to disable the check. Default is 3. |
| aliveInterval | no | Number of last minutes to consider when monitoring the receipt of events from the remote server. If an event is received during this interval, the remote server is considered alive and its accessibility will not be verified. Use 0 to disable this feature. Default is 5. |
| restartIfNoActivity | no | Set it to true if you want to restart the connection (SSE) to the remote server when no events are received in the monitored interval. It is not necessary if the goal is to properly handle a short network outage (few seconds). This can be useful if you want to deal with a long network outage. Do not enable it if you remote server does not send events during the monitored interval under normal conditions, it will cause frequent restart of the connection and potential loss of events. Default is false. |

Please note that even though the default configuration is based on insecure communication over HTTP, it is recommended to adjust the configuration to be based on secure communication over HTTPS.
This is of course essential if your connection to the remote openHAB server is over the Internet.

The `thing` thing has the following configuration parameters:

| Parameter | Required | Description |
|----------------------|----------|---------------------------------------------|
| thingUID | yes | The thing UID in the remote openHAB server. |
| buildTriggerChannels | no | If set to true, a trigger channel will be automatically created and linked to each trigger channel from the remote thing. Default is true. |

Please note that if your remote server is an openHAB v3 server, you will need to define a valid token on your bridge thing to have your things correctly initialized.
Please note that if your remote server is an openHAB v3 server, in order for all of your things to be properly initialized, you will need to define on your bridge thing a valid API token in the parameter `token` and also define the parameter `authenticateAnyway` to true in case you are using an unsecured connection (HTTP).
This API token can be created on your remote server using Main UI.

Setting the `buildTriggerChannels` parameter to false is for the main following advanced usages:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ public class RemoteopenhabServerConfiguration {
public boolean useHttps = false;
public int port = 8080;
public boolean trustedCertificate = false;
public String restPath = "/rest";
public String restPath = "/rest/";
public String token = "";
public String username = "";
public String password = "";
public boolean authenticateAnyway = false;
public int accessibilityInterval = 3;
public int aliveInterval = 5;
public boolean restartIfNoActivity = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,14 +150,10 @@ public void initialize() {
}

String urlStr = url.toString();
if (urlStr.endsWith("/")) {
urlStr = urlStr.substring(0, urlStr.length() - 1);
}
logger.debug("REST URL = {}", urlStr);

restClient.setRestUrl(urlStr);
restClient.setAccessToken(config.token);
restClient.setCredential(config.username, config.password);
restClient.setAuthenticationData(config.authenticateAnyway, config.token, config.username, config.password);
if (config.useHttps && config.trustedCertificate) {
restClient.setHttpClient(httpClientTrustingCert);
restClient.setTrustedCertificate(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public class RemoteopenhabRestClient {
private @Nullable String restApiVersion;
private Map<String, @Nullable String> apiEndPointsUrls = new HashMap<>();
private @Nullable String topicNamespace;
private boolean authenticateAnyway;
private String accessToken;
private String credentialToken;
private boolean trustedCertificate;
Expand Down Expand Up @@ -125,11 +126,10 @@ public void setRestUrl(String restUrl) {
this.restUrl = restUrl;
}

public void setAccessToken(String accessToken) {
public void setAuthenticationData(boolean authenticateAnyway, String accessToken, String username,
String password) {
this.authenticateAnyway = authenticateAnyway;
this.accessToken = accessToken;
}

public void setCredential(String username, String password) {
if (username.isBlank() || password.isBlank()) {
this.credentialToken = "";
} else {
Expand All @@ -144,7 +144,7 @@ public void setTrustedCertificate(boolean trustedCertificate) {

public void tryApi() throws RemoteopenhabException {
try {
String jsonResponse = executeGetUrl(getRestUrl(), "application/json", false);
String jsonResponse = executeGetUrl(getRestUrl(), "application/json", false, false);
if (jsonResponse.isEmpty()) {
throw new RemoteopenhabException("JSON response is empty");
}
Expand Down Expand Up @@ -172,7 +172,7 @@ public List<RemoteopenhabItem> getRemoteItems(@Nullable String fields) throws Re
url += "&fields=" + fields;
}
boolean asyncReading = fields == null || Arrays.asList(fields.split(",")).contains("state");
String jsonResponse = executeGetUrl(url, "application/json", asyncReading);
String jsonResponse = executeGetUrl(url, "application/json", false, asyncReading);
if (jsonResponse.isEmpty()) {
throw new RemoteopenhabException("JSON response is empty");
}
Expand All @@ -186,7 +186,7 @@ public List<RemoteopenhabItem> getRemoteItems(@Nullable String fields) throws Re
public String getRemoteItemState(String itemName) throws RemoteopenhabException {
try {
String url = String.format("%s/%s/state", getRestApiUrl("items"), itemName);
return executeGetUrl(url, "text/plain", true);
return executeGetUrl(url, "text/plain", false, true);
} catch (RemoteopenhabException e) {
throw new RemoteopenhabException("Failed to get the state of remote item " + itemName
+ " using the items REST API: " + e.getMessage(), e);
Expand All @@ -196,7 +196,8 @@ public String getRemoteItemState(String itemName) throws RemoteopenhabException
public void sendCommandToRemoteItem(String itemName, Command command) throws RemoteopenhabException {
try {
String url = String.format("%s/%s", getRestApiUrl("items"), itemName);
executeUrl(HttpMethod.POST, url, "application/json", command.toFullString(), "text/plain", false, true);
executeUrl(HttpMethod.POST, url, "application/json", command.toFullString(), "text/plain", false, false,
true);
} catch (RemoteopenhabException e) {
throw new RemoteopenhabException("Failed to send command to the remote item " + itemName
+ " using the items REST API: " + e.getMessage(), e);
Expand All @@ -205,7 +206,7 @@ public void sendCommandToRemoteItem(String itemName, Command command) throws Rem

public List<RemoteopenhabThing> getRemoteThings() throws RemoteopenhabException {
try {
String jsonResponse = executeGetUrl(getRestApiUrl("things"), "application/json", false);
String jsonResponse = executeGetUrl(getRestApiUrl("things"), "application/json", true, false);
if (jsonResponse.isEmpty()) {
throw new RemoteopenhabException("JSON response is empty");
}
Expand All @@ -219,7 +220,7 @@ public List<RemoteopenhabThing> getRemoteThings() throws RemoteopenhabException
public RemoteopenhabThing getRemoteThing(String uid) throws RemoteopenhabException {
try {
String url = String.format("%s/%s", getRestApiUrl("things"), uid);
String jsonResponse = executeGetUrl(url, "application/json", false);
String jsonResponse = executeGetUrl(url, "application/json", true, false);
if (jsonResponse.isEmpty()) {
throw new RemoteopenhabException("JSON response is empty");
}
Expand All @@ -236,7 +237,14 @@ public RemoteopenhabThing getRemoteThing(String uid) throws RemoteopenhabExcepti

private String getRestApiUrl(String endPoint) throws RemoteopenhabException {
String url = apiEndPointsUrls.get(endPoint);
return url != null ? url : getRestUrl() + "/" + endPoint;
if (url == null) {
url = getRestUrl();
if (!url.endsWith("/")) {
url += "/";
}
url += endPoint;
}
return url;
}

public String getTopicNamespace() {
Expand All @@ -262,6 +270,7 @@ public void stop(boolean waitingForCompletion) {
}

private SseEventSource createEventSource(String restSseUrl) {
String credentialToken = restSseUrl.startsWith("https:") || authenticateAnyway ? this.credentialToken : "";
Client client;
// Avoid a timeout exception after 1 minute by setting the read timeout to 0 (infinite)
if (trustedCertificate) {
Expand All @@ -272,10 +281,10 @@ public boolean verify(@Nullable String hostname, @Nullable SSLSession session) {
return true;
}
}).readTimeout(0, TimeUnit.SECONDS)
.register(new RemoteopenhabStreamingRequestFilter(accessToken, credentialToken)).build();
.register(new RemoteopenhabStreamingRequestFilter(credentialToken)).build();
} else {
client = clientBuilder.readTimeout(0, TimeUnit.SECONDS)
.register(new RemoteopenhabStreamingRequestFilter(accessToken, credentialToken)).build();
.register(new RemoteopenhabStreamingRequestFilter(credentialToken)).build();
}
SseEventSource eventSource = eventSourceFactory.newSource(client.target(restSseUrl));
eventSource.register(this::onEvent, this::onError, this::onComplete);
Expand Down Expand Up @@ -483,28 +492,40 @@ private String extractThingUIDFromTopic(String topic, String eventType, String f
return parts[2];
}

public String executeGetUrl(String url, String acceptHeader, boolean asyncReading) throws RemoteopenhabException {
return executeUrl(HttpMethod.GET, url, acceptHeader, null, null, asyncReading, true);
public String executeGetUrl(String url, String acceptHeader, boolean provideAccessToken, boolean asyncReading)
throws RemoteopenhabException {
return executeUrl(HttpMethod.GET, url, acceptHeader, null, null, provideAccessToken, asyncReading, true);
}

public String executeUrl(HttpMethod httpMethod, String url, String acceptHeader, @Nullable String content,
@Nullable String contentType, boolean asyncReading, boolean retryIfEOF) throws RemoteopenhabException {
@Nullable String contentType, boolean provideAccessToken, boolean asyncReading, boolean retryIfEOF)
throws RemoteopenhabException {
final Request request = httpClient.newRequest(url).method(httpMethod)
.timeout(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS).followRedirects(false);

request.header(HttpHeaders.ACCEPT, acceptHeader);
if (!accessToken.isEmpty()) {
request.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
}
if (!credentialToken.isEmpty()) {
request.header(HttpHeaders.AUTHORIZATION, "Basic " + credentialToken);
.timeout(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS).followRedirects(false)
.header(HttpHeaders.ACCEPT, acceptHeader);

if (url.startsWith("https:") || authenticateAnyway) {
boolean useAlternativeHeader = false;
if (!credentialToken.isEmpty()) {
request.header(HttpHeaders.AUTHORIZATION, "Basic " + credentialToken);
useAlternativeHeader = true;
}
if (provideAccessToken && !accessToken.isEmpty()) {
if (useAlternativeHeader) {
request.header("X-OPENHAB-TOKEN", accessToken);
} else {
request.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
}
}
}

if (content != null && (HttpMethod.POST.equals(httpMethod) || HttpMethod.PUT.equals(httpMethod))
&& contentType != null) {
request.content(new StringContentProvider(content), contentType);
}

logger.debug("Request {} {}", request.getMethod(), request.getURI());

try {
if (asyncReading) {
InputStreamResponseListener listener = new InputStreamResponseListener();
Expand All @@ -524,9 +545,17 @@ public String executeUrl(HttpMethod httpMethod, String url, String acceptHeader,
} else {
ContentResponse response = request.send();
int statusCode = response.getStatus();
if (statusCode >= 300) {
response.getHeaders().forEach(
field -> logger.debug("response header {} = {}", field.getName(), field.getValue()));
if (statusCode == HttpStatus.MOVED_PERMANENTLY_301 || statusCode == HttpStatus.FOUND_302) {
String locationHeader = response.getHeaders().get(HttpHeaders.LOCATION);
if (locationHeader != null && !locationHeader.isBlank()) {
logger.debug("The remopte server redirected the request to this URL: {}", locationHeader);
return executeUrl(httpMethod, locationHeader, acceptHeader, content, contentType,
provideAccessToken, asyncReading, retryIfEOF);
} else {
String statusLine = statusCode + " " + response.getReason();
throw new RemoteopenhabException("HTTP call failed: " + statusLine);
}
} else if (statusCode >= HttpStatus.BAD_REQUEST_400) {
String statusLine = statusCode + " " + response.getReason();
throw new RemoteopenhabException("HTTP call failed: " + statusLine);
}
Expand All @@ -542,7 +571,8 @@ public String executeUrl(HttpMethod httpMethod, String url, String acceptHeader,
Throwable cause = e.getCause();
if (retryIfEOF && cause instanceof EOFException) {
logger.debug("EOFException - retry the request");
return executeUrl(httpMethod, url, acceptHeader, content, contentType, asyncReading, false);
return executeUrl(httpMethod, url, acceptHeader, content, contentType, provideAccessToken, asyncReading,
false);
} else {
throw new RemoteopenhabException(e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
package org.openhab.binding.remoteopenhab.internal.rest;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
Expand All @@ -32,27 +30,18 @@
@NonNullByDefault
public class RemoteopenhabStreamingRequestFilter implements ClientRequestFilter {

private final String accessToken;
private final String credentialToken;

public RemoteopenhabStreamingRequestFilter(String accessToken, String credentialToken) {
this.accessToken = accessToken;
public RemoteopenhabStreamingRequestFilter(String credentialToken) {
this.credentialToken = credentialToken;
}

@Override
public void filter(@Nullable ClientRequestContext requestContext) throws IOException {
if (requestContext != null) {
MultivaluedMap<String, Object> headers = requestContext.getHeaders();
List<Object> values = new ArrayList<>();
if (!accessToken.isEmpty()) {
values.add("Bearer " + accessToken);
}
if (!credentialToken.isEmpty()) {
values.add("Basic " + credentialToken);
}
if (!values.isEmpty()) {
headers.put(HttpHeaders.AUTHORIZATION, values);
headers.putSingle(HttpHeaders.AUTHORIZATION, "Basic " + credentialToken);
}
headers.putSingle(HttpHeaders.CACHE_CONTROL, "no-cache");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
<parameter name="restPath" type="text" required="true">
<label>REST API Path</label>
<description>The subpath of the REST API on the remote openHAB server.</description>
<default>/rest</default>
<default>/rest/</default>
<advanced>true</advanced>
</parameter>

Expand All @@ -69,6 +69,15 @@
<advanced>true</advanced>
</parameter>

<parameter name="authenticateAnyway" type="boolean">
<label>Authenticate Anyway</label>
<description>Set it to true in case you want to pass authentication information even when the communicate with the
remote openHAB server is not secured (only HTTP). This is of course not recommended especially if your connection
is over the Internet. Default is false.</description>
<default>false</default>
<advanced>true</advanced>
</parameter>

<parameter name="accessibilityInterval" type="integer" min="0" step="1" unit="min">
<label>Accessibility Interval</label>
<description>Minutes between checking the remote server accessibility. 0 to disable the check. Default is 3.</description>
Expand Down

0 comments on commit 42f9cb5

Please sign in to comment.