From fca9c7b2ab637f3d7526b38af61b3881b3dc77b4 Mon Sep 17 00:00:00 2001 From: cruizba Date: Tue, 8 Feb 2022 20:04:51 +0100 Subject: [PATCH] Tests for IceServerProperties. Integrate new attribute to Connection and generation token logic --- openvidu-java-client/pom.xml | 5 + .../io/openvidu/java/client/Connection.java | 42 +-- .../java/client/ConnectionProperties.java | 14 +- .../java/client/IceServerProperties.java | 128 ++++++--- .../openvidu/server/core/SessionManager.java | 6 +- .../java/io/openvidu/server/core/Token.java | 24 +- .../openvidu/server/core/TokenGenerator.java | 14 +- .../server/rest/SessionRestController.java | 9 +- .../test/unit/IceServerPropertiesTests.java | 251 ++++++++++++++++++ 9 files changed, 417 insertions(+), 76 deletions(-) create mode 100644 openvidu-server/src/test/java/io/openvidu/server/test/unit/IceServerPropertiesTests.java diff --git a/openvidu-java-client/pom.xml b/openvidu-java-client/pom.xml index 501181a8..4be6cd83 100644 --- a/openvidu-java-client/pom.xml +++ b/openvidu-java-client/pom.xml @@ -86,6 +86,11 @@ ${version.junit} test + + commons-validator + commons-validator + ${version.commons-validator} + diff --git a/openvidu-java-client/src/main/java/io/openvidu/java/client/Connection.java b/openvidu-java-client/src/main/java/io/openvidu/java/client/Connection.java index fffa61b0..3e7ba5dd 100644 --- a/openvidu-java-client/src/main/java/io/openvidu/java/client/Connection.java +++ b/openvidu-java-client/src/main/java/io/openvidu/java/client/Connection.java @@ -324,6 +324,11 @@ public class Connection { if (this.connectionProperties.getNetworkCache() != null) { builder.networkCache(this.connectionProperties.getNetworkCache()); } + if (this.connectionProperties.getCustomIceServers() != null && !this.connectionProperties.getCustomIceServers().isEmpty()) { + for (IceServerProperties iceServerProperties: this.connectionProperties.getCustomIceServers()) { + builder.addCustomIceServer(iceServerProperties); + } + } this.connectionProperties = builder.build(); } @@ -417,6 +422,24 @@ public class Connection { ? OpenViduRole.valueOf(json.get("role").getAsString()) : null; + List customIceServers = new ArrayList<>(); + if (json.has("customIceServers") && json.get("customIceServers").isJsonArray()) { + JsonArray customIceServersJsonArray = json.get("customIceServers").getAsJsonArray(); + customIceServersJsonArray.forEach(iceJsonElem -> { + JsonObject iceJsonObj = iceJsonElem.getAsJsonObject(); + String url = (iceJsonObj.has("url") && !iceJsonObj.get("url").isJsonNull()) + ? json.get("url").getAsString() + : null; + String username = (iceJsonObj.has("username") && !iceJsonObj.get("username").isJsonNull()) + ? json.get("username").getAsString() + : null; + String credential = (iceJsonObj.has("credential") && !iceJsonObj.get("credential").isJsonNull()) + ? json.get("credential").getAsString() + : null; + customIceServers.add(new IceServerProperties.Builder().url(url).username(username).credential(credential).build()); + }); + } + // IPCAM String rtspUri = (json.has("rtspUri") && !json.get("rtspUri").isJsonNull()) ? json.get("rtspUri").getAsString() : null; @@ -431,25 +454,6 @@ public class Connection { ? json.get("networkCache").getAsInt() : null; - // External Ice Servers - List customIceServers = new ArrayList<>(); - if (json.has("customIceServers") && json.get("customIceServers").isJsonArray()) { - JsonArray customIceServersJsonArray = json.get("customIceServers").getAsJsonArray(); - customIceServersJsonArray.forEach(iceJsonElem -> { - JsonObject iceJsonObj = iceJsonElem.getAsJsonObject(); - String url = (iceJsonObj.has("urls") && !iceJsonObj.get("urls").isJsonNull()) - ? json.get("urls").getAsString() - : null; - String username = (iceJsonObj.has("username") && !iceJsonObj.get("username").isJsonNull()) - ? json.get("username").getAsString() - : null; - String credential = (iceJsonObj.has("credential") && !iceJsonObj.get("credential").isJsonNull()) - ? json.get("credential").getAsString() - : null; - customIceServers.add(new IceServerProperties.Builder().url(url).username(username).credential(credential).build()); - }); - } - this.connectionProperties = new ConnectionProperties(type, data, record, role, null, rtspUri, adaptativeBitrate, onlyPlayWithSubscribers, networkCache, customIceServers); diff --git a/openvidu-java-client/src/main/java/io/openvidu/java/client/ConnectionProperties.java b/openvidu-java-client/src/main/java/io/openvidu/java/client/ConnectionProperties.java index 1da3b382..38903f42 100644 --- a/openvidu-java-client/src/main/java/io/openvidu/java/client/ConnectionProperties.java +++ b/openvidu-java-client/src/main/java/io/openvidu/java/client/ConnectionProperties.java @@ -399,6 +399,12 @@ public class ConnectionProperties { } else { json.add("kurentoOptions", JsonNull.INSTANCE); } + JsonArray customIceServersJsonList = new JsonArray(); + customIceServers.forEach((customIceServer) -> { + customIceServersJsonList.add(customIceServer.toJson()); + }); + json.add("customIceServers", customIceServersJsonList); + // IPCAM if (getRtspUri() != null) { json.addProperty("rtspUri", getRtspUri()); @@ -420,14 +426,6 @@ public class ConnectionProperties { } else { json.add("networkCache", JsonNull.INSTANCE); } - - // Ice Servers - JsonArray customIceServersJsonList = new JsonArray(); - customIceServers.forEach((customIceServer) -> { - customIceServersJsonList.add(customIceServer.toJson()); - }); - json.add("customIceServers", customIceServersJsonList); - return json; } diff --git a/openvidu-java-client/src/main/java/io/openvidu/java/client/IceServerProperties.java b/openvidu-java-client/src/main/java/io/openvidu/java/client/IceServerProperties.java index b6a5de9e..565b6a6e 100644 --- a/openvidu-java-client/src/main/java/io/openvidu/java/client/IceServerProperties.java +++ b/openvidu-java-client/src/main/java/io/openvidu/java/client/IceServerProperties.java @@ -1,7 +1,11 @@ package io.openvidu.java.client; import com.google.gson.JsonObject; +import org.apache.commons.validator.routines.DomainValidator; +import org.apache.commons.validator.routines.InetAddressValidator; +import java.net.Inet6Address; +import java.net.UnknownHostException; import java.util.Arrays; import java.util.HashSet; import java.util.Set; @@ -37,7 +41,7 @@ public class IceServerProperties { */ public JsonObject toJson() { JsonObject json = new JsonObject(); - json.addProperty("urls", getUrl()); + json.addProperty("url", getUrl()); if (getUsername() != null && !getUsername().isEmpty()) { json.addProperty("username", getUsername()); } @@ -74,13 +78,15 @@ public class IceServerProperties { throw new IllegalArgumentException("External turn url cannot be null"); } this.checkValidStunTurn(this.url); - if (this.username == null ^ this.credential == null) { - // If one is null when the other is defined, fail... - throw new IllegalArgumentException("You need to define username and credentials if you define one of them"); - } - if (this.username != null && this.credential != null && this.url.startsWith("stun")) { - // Credentials can not be defined using stun - throw new IllegalArgumentException("Credentials can not be defined while using stun."); + if (this.url.startsWith("turn")) { + if ((this.username == null || this.credential == null)) { + throw new IllegalArgumentException("Credentials must be defined while using turn"); + } + } else if (this.url.startsWith("stun")) { + if (this.username != null || this.credential != null) { + // Credentials can not be defined using stun + throw new IllegalArgumentException("Credentials can not be defined while using stun."); + } } return new IceServerProperties(this.url, this.username, this.credential); } @@ -138,32 +144,88 @@ public class IceServerProperties { // Check if port is defined int portColon = hostAndPort.indexOf(':'); - if (portColon != -1) { - String[] splittedHostAndPort = hostAndPort.split(":"); - if (splittedHostAndPort.length != 2) { - throw new IllegalArgumentException("Host or port are not correctly " + - "defined in STUN/TURN uri: '" + uri + "'"); - } - String host = splittedHostAndPort[0]; - String port = splittedHostAndPort[1]; - - // Check if host is defined. Valid Host (Domain or IP) will be done at server side - if (host == null || host.isEmpty()) { - throw new IllegalArgumentException("Host defined in '" + uri + "' is empty or null"); - } - - if (port == null || port.isEmpty()) { - throw new IllegalArgumentException("Port defined in '" + uri + "' is empty or null"); - } - try { - int parsedPort = Integer.parseInt(port); - if (parsedPort <= 0 || parsedPort > 65535) { - throw new IllegalArgumentException("The port defined in '" + uri + "' is not a valid port number"); - } - } catch (NumberFormatException e) { - throw new IllegalArgumentException("The port defined in '" + uri + "' is not a number"); - } + // IPv6 are defined between brackets + int startIpv6Index = hostAndPort.indexOf('['); + int endIpv6Index = hostAndPort.indexOf(']'); + if (startIpv6Index == -1 ^ endIpv6Index == -1) { + throw new IllegalArgumentException("Not closed bracket '[' or ']' in uri: " + uri); } + + if (portColon != -1) { + if (startIpv6Index == -1 && endIpv6Index == -1) { + // If Ipv4 and port defined + String[] splittedHostAndPort = hostAndPort.split(":"); + if (splittedHostAndPort.length != 2) { + throw new IllegalArgumentException("Host or port are not correctly " + + "defined in STUN/TURN uri: '" + uri + "'"); + } + String host = splittedHostAndPort[0]; + String port = splittedHostAndPort[1]; + + // Check if host is defined. Valid Host (Domain or IP) will be done at server side + checkHostAndPort(uri, host, port); + } else { + // If portColon is found and Ipv6 + String ipv6 = hostAndPort.substring(startIpv6Index + 1, endIpv6Index); + String auxPort = hostAndPort.substring(endIpv6Index + 1); + if (auxPort.startsWith(":")) { + if (auxPort.length() == 1) { + throw new IllegalArgumentException("Host or port are not correctly defined in STUN/TURN uri: " + uri); + } + // If port is defined + // Get port without colon and check host and port + String host = ipv6; + String port = auxPort.substring(1); + checkHostAndPort(uri, host, port); + } else if (auxPort.length() > 0) { + // If auxPort = 0, no port is defined + throw new IllegalArgumentException("Port is not specified correctly after IPv6 in uri: '" + uri + "'"); + } + } + } else { + // If portColon not found, only host is defined + String host = hostAndPort; + checkHost(uri, host); + } + } + + private void checkHost(String uri, String host) { + if (host == null || host.isEmpty()) { + throw new IllegalArgumentException("Host defined in '" + uri + "' is empty or null"); + } + if (DomainValidator.getInstance().isValid(host)) { + return; + } + InetAddressValidator ipValidator = InetAddressValidator.getInstance(); + if (ipValidator.isValid(host)) { + return; + } + try { + Inet6Address.getByName(host).getHostAddress(); + return; + } catch (UnknownHostException e) { + throw new IllegalArgumentException("Is not a valid Internet Address (IP or Domain Name): '" + host + "'"); + } + } + + private void checkPort(String uri, String port) { + if (port == null || port.isEmpty()) { + throw new IllegalArgumentException("Port defined in '" + uri + "' is empty or null"); + } + + try { + int parsedPort = Integer.parseInt(port); + if (parsedPort <= 0 || parsedPort > 65535) { + throw new IllegalArgumentException("The port defined in '" + uri + "' is not a valid port number (0-65535)"); + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("The port defined in '" + uri + "' is not a number (0-65535)"); + } + } + + private void checkHostAndPort(String uri, String host, String port) { + this.checkHost(uri, host); + this.checkPort(uri, port); } } diff --git a/openvidu-server/src/main/java/io/openvidu/server/core/SessionManager.java b/openvidu-server/src/main/java/io/openvidu/server/core/SessionManager.java index 9821b2a2..3b249ea8 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/core/SessionManager.java +++ b/openvidu-server/src/main/java/io/openvidu/server/core/SessionManager.java @@ -19,6 +19,7 @@ package io.openvidu.server.core; import java.util.Collection; import java.util.HashSet; +import java.util.List; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; @@ -51,6 +52,7 @@ import io.openvidu.java.client.KurentoOptions; import io.openvidu.java.client.OpenViduRole; import io.openvidu.java.client.Recording; import io.openvidu.java.client.SessionProperties; +import io.openvidu.java.client.IceServerProperties; import io.openvidu.server.cdr.CDREventRecordingStatusChanged; import io.openvidu.server.config.OpenviduConfig; import io.openvidu.server.coturn.CoturnCredentialsService; @@ -329,13 +331,13 @@ public abstract class SessionManager { } public Token newToken(Session session, OpenViduRole role, String serverMetadata, boolean record, - KurentoOptions kurentoOptions) throws Exception { + KurentoOptions kurentoOptions, List customIceServers) throws Exception { if (!formatChecker.isServerMetadataFormatCorrect(serverMetadata)) { log.error("Data invalid format"); throw new OpenViduException(Code.GENERIC_ERROR_CODE, "Data invalid format"); } Token tokenObj = tokenGenerator.generateToken(session.getSessionId(), serverMetadata, record, role, - kurentoOptions); + kurentoOptions, customIceServers); // Internal dev feature: allows customizing connectionId if (serverMetadata.contains("openviduCustomConnectionId")) { diff --git a/openvidu-server/src/main/java/io/openvidu/server/core/Token.java b/openvidu-server/src/main/java/io/openvidu/server/core/Token.java index 09d68bac..6633a761 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/core/Token.java +++ b/openvidu-server/src/main/java/io/openvidu/server/core/Token.java @@ -17,18 +17,17 @@ package io.openvidu.server.core; +import io.openvidu.java.client.*; import org.apache.commons.lang3.RandomStringUtils; import com.google.gson.JsonNull; import com.google.gson.JsonObject; -import io.openvidu.java.client.ConnectionProperties; -import io.openvidu.java.client.ConnectionType; -import io.openvidu.java.client.KurentoOptions; -import io.openvidu.java.client.OpenViduRole; import io.openvidu.server.core.Participant.ParticipantStatus; import io.openvidu.server.coturn.TurnCredentials; +import java.util.List; + public class Token { private String token; @@ -77,7 +76,8 @@ public class Token { this.updateConnectionProperties(connectionProperties.getType(), connectionProperties.getData(), newRecord, connectionProperties.getRole(), connectionProperties.getKurentoOptions(), connectionProperties.getRtspUri(), connectionProperties.adaptativeBitrate(), - connectionProperties.onlyPlayWithSubscribers(), connectionProperties.getNetworkCache()); + connectionProperties.onlyPlayWithSubscribers(), connectionProperties.getNetworkCache(), + connectionProperties.getCustomIceServers()); } public OpenViduRole getRole() { @@ -88,7 +88,8 @@ public class Token { this.updateConnectionProperties(connectionProperties.getType(), connectionProperties.getData(), connectionProperties.record(), newRole, connectionProperties.getKurentoOptions(), connectionProperties.getRtspUri(), connectionProperties.adaptativeBitrate(), - connectionProperties.onlyPlayWithSubscribers(), connectionProperties.getNetworkCache()); + connectionProperties.onlyPlayWithSubscribers(), connectionProperties.getNetworkCache(), + connectionProperties.getCustomIceServers()); } public KurentoOptions getKurentoOptions() { @@ -118,6 +119,10 @@ public class Token { public String getConnectionId() { return connectionId; } + + public List getCustomIceServers() { + return this.connectionProperties.getCustomIceServers(); + } public void setConnectionId(String connectionId) { this.connectionId = connectionId; @@ -178,7 +183,7 @@ public class Token { private void updateConnectionProperties(ConnectionType type, String data, Boolean record, OpenViduRole role, KurentoOptions kurentoOptions, String rtspUri, Boolean adaptativeBitrate, Boolean onlyPlayWithSubscribers, - Integer networkCache) { + Integer networkCache, List iceServerProperties) { ConnectionProperties.Builder builder = new ConnectionProperties.Builder(); if (type != null) { builder.type(type); @@ -207,6 +212,11 @@ public class Token { if (networkCache != null) { builder.networkCache(networkCache); } + if (iceServerProperties != null) { + for (IceServerProperties customIceServer: iceServerProperties) { + builder.addCustomIceServer(customIceServer); + } + } this.connectionProperties = builder.build(); } diff --git a/openvidu-server/src/main/java/io/openvidu/server/core/TokenGenerator.java b/openvidu-server/src/main/java/io/openvidu/server/core/TokenGenerator.java index 35192b52..7245d19e 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/core/TokenGenerator.java +++ b/openvidu-server/src/main/java/io/openvidu/server/core/TokenGenerator.java @@ -24,12 +24,15 @@ import io.openvidu.java.client.ConnectionProperties; import io.openvidu.java.client.ConnectionType; import io.openvidu.java.client.KurentoOptions; import io.openvidu.java.client.OpenViduRole; +import io.openvidu.java.client.IceServerProperties; import io.openvidu.server.OpenViduServer; import io.openvidu.server.config.OpenviduBuildInfo; import io.openvidu.server.config.OpenviduConfig; import io.openvidu.server.coturn.CoturnCredentialsService; import io.openvidu.server.coturn.TurnCredentials; +import java.util.List; + public class TokenGenerator { @Autowired @@ -42,7 +45,7 @@ public class TokenGenerator { protected OpenviduBuildInfo openviduBuildConfig; public Token generateToken(String sessionId, String serverMetadata, boolean record, OpenViduRole role, - KurentoOptions kurentoOptions) throws Exception { + KurentoOptions kurentoOptions, List customIceServers) throws Exception { String token = OpenViduServer.wsUrl; token += "?sessionId=" + sessionId; token += "&token=" + IdentifierPrefixes.TOKEN_ID + RandomStringUtils.randomAlphabetic(1).toUpperCase() @@ -51,8 +54,13 @@ public class TokenGenerator { if (this.openviduConfig.isTurnadminAvailable()) { turnCredentials = coturnCredentialsService.createUser(); } - ConnectionProperties connectionProperties = new ConnectionProperties.Builder().type(ConnectionType.WEBRTC) - .data(serverMetadata).record(record).role(role).kurentoOptions(kurentoOptions).build(); + ConnectionProperties.Builder connectionPropertiesBuilder = new ConnectionProperties.Builder() + .type(ConnectionType.WEBRTC).data(serverMetadata).record(record).role(role) + .kurentoOptions(kurentoOptions); + for (IceServerProperties customIceServer: customIceServers) { + connectionPropertiesBuilder.addCustomIceServer(customIceServer); + } + ConnectionProperties connectionProperties = connectionPropertiesBuilder.build(); return new Token(token, sessionId, connectionProperties, turnCredentials); } diff --git a/openvidu-server/src/main/java/io/openvidu/server/rest/SessionRestController.java b/openvidu-server/src/main/java/io/openvidu/server/rest/SessionRestController.java index c7581d4c..1e3bc555 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/rest/SessionRestController.java +++ b/openvidu-server/src/main/java/io/openvidu/server/rest/SessionRestController.java @@ -21,6 +21,7 @@ import java.net.MalformedURLException; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.TimeUnit; @@ -653,7 +654,7 @@ public class SessionRestController { try { Token token = sessionManager.newToken(session, connectionProperties.getRole(), connectionProperties.getData(), connectionProperties.record(), - connectionProperties.getKurentoOptions()); + connectionProperties.getKurentoOptions(), connectionProperties.getCustomIceServers()); return new ResponseEntity<>(token.toJsonAsParticipant().toString(), RestUtils.getResponseHeaders(), HttpStatus.OK); } catch (Exception e) { @@ -909,7 +910,7 @@ public class SessionRestController { JsonArray customIceServersJsonArray = null; if (params.get("customIceServers") != null) { try { - customIceServersJsonArray = new Gson().toJsonTree(params.get("customIceServers"), Map.class) + customIceServersJsonArray = new Gson().toJsonTree(params.get("customIceServers"), List.class) .getAsJsonArray(); } catch (Exception e) { throw new Exception("Error in parameter 'customIceServersJson'. It is not a valid JSON object"); @@ -920,7 +921,7 @@ public class SessionRestController { for (int i = 0; i < customIceServersJsonArray.size(); i++) { JsonObject customIceServerJson = customIceServersJsonArray.get(i).getAsJsonObject(); IceServerProperties.Builder iceServerPropertiesBuilder = new IceServerProperties.Builder(); - iceServerPropertiesBuilder.url(customIceServerJson.get("urls").getAsString()); + iceServerPropertiesBuilder.url(customIceServerJson.get("url").getAsString()); if (customIceServerJson.has("username")) { iceServerPropertiesBuilder.username(customIceServerJson.get("username").getAsString()); } @@ -931,7 +932,7 @@ public class SessionRestController { builder.addCustomIceServer(iceServerProperties); } } catch (Exception e) { - throw new Exception("Type error in some parameter of 'kurentoOptions': " + e.getMessage()); + throw new Exception("Type error in some parameter of 'customIceServers': " + e.getMessage()); } } diff --git a/openvidu-server/src/test/java/io/openvidu/server/test/unit/IceServerPropertiesTests.java b/openvidu-server/src/test/java/io/openvidu/server/test/unit/IceServerPropertiesTests.java new file mode 100644 index 00000000..b3d8db1f --- /dev/null +++ b/openvidu-server/src/test/java/io/openvidu/server/test/unit/IceServerPropertiesTests.java @@ -0,0 +1,251 @@ +package io.openvidu.server.test.unit; + +import io.openvidu.java.client.IceServerProperties; +import org.junit.Test; +import org.junit.jupiter.api.DisplayName; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; + +public class IceServerPropertiesTests { + + @Test + @DisplayName("IceServerProperty exceptions tests") + public void iceServerPropertiesExceptionTest() { + // Wrong urls + notValidIceServerTest( + "wrongurl", null, null, + "Not a valid TURN/STUN uri provided. No colons found in: 'wrongurl'" + ); + notValidIceServerTest( + "wrongurl", "anyuser", null, + "Not a valid TURN/STUN uri provided. No colons found in: 'wrongurl'" + ); + notValidIceServerTest( + "wrongurl", null, "anypassword", + "Not a valid TURN/STUN uri provided. No colons found in: 'wrongurl'" + ); + notValidIceServerTest( + "wrongurl", "anyuser", "anypassword", + "Not a valid TURN/STUN uri provided. No colons found in: 'wrongurl'" + ); + + // Wrong prefixes + notValidIceServerTest( + "turnss:wrongurl", null, null, + "The protocol 'turnss' is invalid. Only valid values are: [turn, turns] [stuns, stun]" + ); + notValidIceServerTest( + "stunss:wrongurl", "anyuser", null, + "The protocol 'stunss' is invalid. Only valid values are: [turn, turns] [stuns, stun]" + ); + notValidIceServerTest( + "anything:wrongurl", null, "anypassword", + "The protocol 'anything' is invalid. Only valid values are: [turn, turns] [stuns, stun]" + ); + notValidIceServerTest( + ":", null, null, + "The protocol '' is invalid. Only valid values are: [turn, turns] [stuns, stun]" + ); + notValidIceServerTest( + "", null, null, + "Not a valid TURN/STUN uri provided. No colons found in: ''" + ); + + // Try invalid host and ports + notValidIceServerTest( + "stun:hostname.com:99a99", null, null, + "The port defined in 'stun:hostname.com:99a99' is not a number (0-65535)" + ); + notValidIceServerTest( + "stun:hostname.com:-1", null, null, + "The port defined in 'stun:hostname.com:-1' is not a valid port number (0-65535)" + ); + notValidIceServerTest( + "stun:hostname:port:more", null, null, + "Host or port are not correctly defined in STUN/TURN uri: 'stun:hostname:port:more'" + ); + notValidIceServerTest( + "stun:hostname.com:port more", null, null, + "The port defined in 'stun:hostname.com:port more' is not a number (0-65535)" + ); + notValidIceServerTest( + "stun:hostname.com:", null, null, + "Host or port are not correctly defined in STUN/TURN uri: 'stun:hostname.com:'" + ); + notValidIceServerTest( + "stun:[1:2:3:4:5:6:7:8]junk:1000", null, null, + "Port is not specified correctly after IPv6 in uri: 'stun:[1:2:3:4:5:6:7:8]junk:1000'" + ); + notValidIceServerTest( + "stun:[notvalid:]:1000", null, null, + "Is not a valid Internet Address (IP or Domain Name): 'notvalid:'" + ); + notValidIceServerTest( + "stun::5555", null, null, + "Host defined in 'stun::5555' is empty or null" + ); + notValidIceServerTest( + "stun:", null, null, + "Host defined in 'stun:' is empty or null" + ); + + // Illegal Uri tests according to RFC 3986 and RFC 7064 (URI schemes for STUN and TURN) + notValidIceServerTest( + "stun:/hostname.com", null, null, + "Is not a valid Internet Address (IP or Domain Name): '/hostname.com'" + ); + notValidIceServerTest( + "stun:?hostname.com", null, null, + "STUN uri can't have any '?' query param" + ); + notValidIceServerTest( + "stun:#hostname.com", null, null, + "Is not a valid Internet Address (IP or Domain Name): '#hostname.com'" + ); + + // illegal ?transport=xxx tests in turn uris + notValidIceServerTest( + "turn:hostname.com?transport=invalid", "anyuser", "anypassword", + "Wrong value specified in STUN/TURN uri: 'turn:hostname.com?transport=invalid'. Unique valid arguments after '?' are '?transport=tcp' or '?transport=udp" + ); + notValidIceServerTest( + "turn:hostname.com?transport=", "anyuser", "anypassword", + "Wrong value specified in STUN/TURN uri: 'turn:hostname.com?transport='. Unique valid arguments after '?' are '?transport=tcp' or '?transport=udp" + ); + notValidIceServerTest( + "turn:hostname.com?=", "anyuser", "anypassword", + "Wrong value specified in STUN/TURN uri: 'turn:hostname.com?='. Unique valid arguments after '?' are '?transport=tcp' or '?transport=udp" + ); + notValidIceServerTest( + "turn:hostname.com?", "anyuser", "anypassword", + "Wrong value specified in STUN/TURN uri: 'turn:hostname.com?'. Unique valid arguments after '?' are '?transport=tcp' or '?transport=udp" + ); + notValidIceServerTest( + "?", "anyuser", "anypassword", + "Not a valid TURN/STUN uri provided. No colons found in: '?'" + ); + + // Transport can not be defined in STUN + notValidIceServerTest( + "stun:hostname.com?transport=tcp", null, null, + "STUN uri can't have any '?' query param" + ); + notValidIceServerTest( + "stun:hostname.com?transport=udp", null, null, + "STUN uri can't have any '?' query param" + ); + + // Stun can not have credentials defined + notValidIceServerTest( + "stun:hostname.com", "username", "credential", + "Credentials can not be defined while using stun." + ); + notValidIceServerTest( + "stun:hostname.com", "username", "credential", + "Credentials can not be defined while using stun." + ); + notValidIceServerTest( + "stun:hostname.com", "username", null, + "Credentials can not be defined while using stun." + ); + notValidIceServerTest( + "stun:hostname.com", null, "credential", + "Credentials can not be defined while using stun." + ); + + // Turn must have credentials + notValidIceServerTest( + "turn:hostname.com", null, null, + "Credentials must be defined while using turn" + ); + notValidIceServerTest( + "turn:hostname.com", "username", null, + "Credentials must be defined while using turn" + ); + notValidIceServerTest( + "turn:hostname.com", null, "credential", + "Credentials must be defined while using turn" + ); + } + + @Test + @DisplayName("IceServerProperty exceptions tests") + public void iceServerPropertiesValidTest() { + // Stun and stuns + validIceServerTest("stun:hostname.com", null, null); + validIceServerTest("stuns:hostname.com", null, null); + + // Turn and turns + validIceServerTest("turn:hostname.com", "anyuser", "credential"); + validIceServerTest("turns:hostname.com", "anyuser", "credential"); + + // Test IPv4/IPv6/hostname and with/without port + validIceServerTest("stun:1.2.3.4:1234", null, null); + validIceServerTest("stun:[1:2:3:4:5:6:7:8]:4321", null, null); + validIceServerTest("stun:hostname.com:9999", null, null); + validIceServerTest("stun:1.2.3.4", null, null); + validIceServerTest("stun:[1:2:3:4:5:6:7:8]", null, null); + validIceServerTest("stuns:1.2.3.4:1234", null, null); + validIceServerTest("stuns:[1:2:3:4:5:6:7:8]:4321", null, null); + validIceServerTest("stuns:hostname.com:9999", null, null); + validIceServerTest("stuns:1.2.3.4", null, null); + validIceServerTest("stuns:[1:2:3:4:5:6:7:8]", null, null); + validIceServerTest("turn:1.2.3.4:1234", "anyuser", "credential"); + validIceServerTest("turn:[1:2:3:4:5:6:7:8]:4321", "anyuser", "credential"); + validIceServerTest("turn:hostname.com:9999", "anyuser", "credential"); + validIceServerTest("turn:1.2.3.4", "anyuser", "credential"); + validIceServerTest("turn:[1:2:3:4:5:6:7:8]", "anyuser", "credential"); + validIceServerTest("turns:1.2.3.4:1234", "anyuser", "credential"); + validIceServerTest("turns:[1:2:3:4:5:6:7:8]:4321", "anyuser", "credential"); + validIceServerTest("turns:hostname.com:9999", "anyuser", "credential"); + validIceServerTest("turns:1.2.3.4", "anyuser", "credential"); + validIceServerTest("turns:[1:2:3:4:5:6:7:8]", "anyuser", "credential"); + + // Test valid ?transport=tcp or ?transport=udp + validIceServerTest("turn:hostname.com:1234?transport=tcp", "anyuser", "credential"); + validIceServerTest("turn:hostname.com?transport=udp", "anyuser", "credential"); + validIceServerTest("turn:1.2.3.4:1234?transport=tcp", "anyuser", "credential"); + validIceServerTest("turn:1.2.3.4?transport=udp", "anyuser", "credential"); + validIceServerTest("turn:[1:2:3:4:5:6:7:8]:4321?transport=udp", "anyuser", "credential"); + validIceServerTest("turn:[1:2:3:4:5:6:7:8]?transport=udp", "anyuser", "credential"); + } + + private void validIceServerTest(String url, String username, String credential) { + assertDoesNotThrow(() -> { + IceServerProperties.Builder iceServerPropertiesBuilder = new IceServerProperties.Builder().url(url); + if (username != null) { + iceServerPropertiesBuilder.username(username); + } + if (credential != null) { + iceServerPropertiesBuilder.credential(credential); + } + IceServerProperties iceServerProperties = iceServerPropertiesBuilder.build(); + assertEquals(url, iceServerProperties.getUrl()); + if (username != null) { + assertEquals(username, iceServerProperties.getUsername()); + } + if (credential != null) { + assertEquals(credential, iceServerProperties.getCredential()); + } + }); + } + + private void notValidIceServerTest(String url, String username, String credential, String expectedMessage) { + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + IceServerProperties.Builder iceServerPropertiesBuilder = new IceServerProperties.Builder().url(url); + if (username != null) { + iceServerPropertiesBuilder.username(username); + } + if (credential != null) { + iceServerPropertiesBuilder.credential(credential); + } + iceServerPropertiesBuilder.build(); + }); + + String actualMessage = exception.getMessage(); + assertEquals(actualMessage, expectedMessage); + } + +}