diff --git a/src/main/java/cz/smarteon/loxone/LoxoneEndpoint.java b/src/main/java/cz/smarteon/loxone/LoxoneEndpoint.java index 35e11ef..5a5094e 100644 --- a/src/main/java/cz/smarteon/loxone/LoxoneEndpoint.java +++ b/src/main/java/cz/smarteon/loxone/LoxoneEndpoint.java @@ -41,11 +41,16 @@ public LoxoneEndpoint(@NotNull final String address) { /** * Create new instance of given host (only the host without protocol or port part is expected) and port. - * @param host loxone address host + * @param address loxone address (can include path like "dns.loxonecloud.com/783JDJ38DJ83JD") * @param port loxone address port */ - public LoxoneEndpoint(@NotNull final String host, final int port) { - this(host, port, false); + public LoxoneEndpoint(@NotNull final String address, final int port) { + this( + checkAndParseHost(requireNonNull(address, "address can't be null")), + address.contains(SLASH) ? null : port, + address.contains(SLASH), + address.contains(SLASH) ? address.substring(address.indexOf(SLASH)) : "" + ); } /** @@ -53,12 +58,17 @@ public LoxoneEndpoint(@NotNull final String host, final int port) { * and sets whether to use SSL. * BEWARE: Loxone miniserver (in version 10) doesn't support SSL natively. So the {@code useSsl} make sense only * when accessing miniserver through some reverse HTTPS proxy. - * @param host loxone address host + * @param address loxone address (can include path like "dns.loxonecloud.com/3D8UDJ83") * @param port loxone address port * @param useSsl whether to use SSL */ - public LoxoneEndpoint(@NotNull final String host, final int port, final boolean useSsl) { - this(host, port, useSsl, ""); + public LoxoneEndpoint(@NotNull final String address, final int port, final boolean useSsl) { + this( + checkAndParseHost(requireNonNull(address, "address can't be null")), + address.contains(SLASH) ? null : port, + address.contains(SLASH) || useSsl, + address.contains(SLASH) ? address.substring(address.indexOf(SLASH)) : "" + ); } /** diff --git a/src/main/java/cz/smarteon/loxone/LoxoneHttp.java b/src/main/java/cz/smarteon/loxone/LoxoneHttp.java index 3e9e30b..4d9d72d 100644 --- a/src/main/java/cz/smarteon/loxone/LoxoneHttp.java +++ b/src/main/java/cz/smarteon/loxone/LoxoneHttp.java @@ -70,10 +70,33 @@ T get(URL url, Command.Type type, Map properties, Class r try { connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(connectionTimeout); + connection.setInstanceFollowRedirects(false); + for (Map.Entry property : properties.entrySet()) { connection.setRequestProperty(property.getKey(), property.getValue()); } + final int responseCode = connection.getResponseCode(); + + if (responseCode == HttpURLConnection.HTTP_MOVED_PERM + || responseCode == HttpURLConnection.HTTP_MOVED_TEMP + || responseCode == HttpURLConnection.HTTP_SEE_OTHER + || responseCode == 307 + || responseCode == 308) { + + if (requestContext.get().redirects > MAX_REDIRECTS) { + throw new IllegalStateException("Too many redirects!"); + } + + final String locationHeader = connection.getHeaderField("Location"); + if (locationHeader != null) { + final URL location = new URL(locationHeader); + requestContext.get().redirect(location); + LOG.debug("Redirecting to: {}", location); + return get(location, type, properties, responseType); + } + } + if (responseCode == HttpURLConnection.HTTP_OK) { requestContext.get().lastUrl = connection.getURL(); try (InputStream is = connection.getInputStream()) { @@ -86,16 +109,6 @@ T get(URL url, Command.Type type, Map properties, Class r throw new IllegalStateException("Unknown command type " + type); } } - } else if (responseCode == HttpURLConnection.HTTP_MOVED_PERM - || responseCode == HttpURLConnection.HTTP_MOVED_TEMP - || responseCode == HttpURLConnection.HTTP_SEE_OTHER - || responseCode == 307) { - if (requestContext.get().redirects > MAX_REDIRECTS) { - throw new IllegalStateException("Too many redirects!"); - } - final URL location = new URL(connection.getHeaderField("Location")); - requestContext.get().redirect(location); - return get(location, type, properties, responseType); } else { throw new LoxoneException("Loxone command responded by status " + responseCode); } diff --git a/src/test/kotlin/LoxoneEndpointTest.kt b/src/test/kotlin/LoxoneEndpointTest.kt index 928016f..4f5e15a 100644 --- a/src/test/kotlin/LoxoneEndpointTest.kt +++ b/src/test/kotlin/LoxoneEndpointTest.kt @@ -8,6 +8,7 @@ import strikt.api.expectCatching import strikt.api.expectThat import strikt.assertions.isEqualTo import strikt.assertions.isFailure +import kotlin.toString class LoxoneEndpointTest { @@ -73,4 +74,31 @@ class LoxoneEndpointTest { fun `should verify equals`() { EqualsVerifier.forClass(LoxoneEndpoint::class.java).withNonnullFields("host", "path").verify() } + + enum class TestDnsEndpoint( + val endpoint: LoxoneEndpoint, + val expectedHttp: String, + val expectedWs: String + ) { + DnsSimple( + LoxoneEndpoint("dns.loxonecloud.com/5039IR9JFSF"), + "https://dns.loxonecloud.com/5039IR9JFSF/jdev", + "wss://dns.loxonecloud.com/ws/rfc6455" + ), + DnsWithPath( + LoxoneEndpoint("dns.loxonecloud.com/5039IR9JFSF/extra/path"), + "https://dns.loxonecloud.com/5039IR9JFSF/extra/path/jdev", + "wss://dns.loxonecloud.com/ws/rfc6455" + ) + } + + @ParameterizedTest + @EnumSource(TestDnsEndpoint::class) + fun `should create DNS endpoint with path`(testParameters: TestDnsEndpoint) { + expectThat(testParameters.endpoint) { + get { httpUrl("jdev").toString() }.isEqualTo(testParameters.expectedHttp) + get { webSocketUri().toString() }.isEqualTo(testParameters.expectedWs) + } + } + } diff --git a/src/test/kotlin/LoxoneHttpTest.kt b/src/test/kotlin/LoxoneHttpTest.kt index aa09071..379a295 100644 --- a/src/test/kotlin/LoxoneHttpTest.kt +++ b/src/test/kotlin/LoxoneHttpTest.kt @@ -13,6 +13,8 @@ import org.junit.jupiter.api.Test import strikt.api.expectThat import strikt.assertions.isEqualTo import java.net.URL +import kotlin.text.get +import kotlin.toString class LoxoneHttpTest { private lateinit var loxoneHttp: LoxoneHttp @@ -97,4 +99,38 @@ class LoxoneHttpTest { isEqualTo("testString") } } + + @Test + fun `should follow redirect without auth for DNS endpoint`() { + val finalLocation = "http://localhost:${Jadler.port()}/final" + + onRequest() + .havingMethodEqualTo("GET") + .havingPathEqualTo("/5039IR9JFSF/jdev/sps/io/test/value") + .respond() + .withStatus(302) + .withHeader("Location", finalLocation) + + onRequest() + .havingMethodEqualTo("GET") + .havingPathEqualTo("/final") + .respond() + .withStatus(200) + .withBody("\"redirectSuccess\"") + + expectThat( + loxoneHttp.get( + Command( + "/5039IR9JFSF/jdev/sps/io/test/value", + Command.Type.JSON, + String::class.java, + true, + false, + MiniserverType.KNOWN + ) + ) + ).isEqualTo("redirectSuccess") + + expectThat(loxoneHttp.lastUrl.toString()).isEqualTo(finalLocation) + } }