diff --git a/openvidu-browser/package.json b/openvidu-browser/package.json index adc65aa9..e281dce1 100644 --- a/openvidu-browser/package.json +++ b/openvidu-browser/package.json @@ -5,6 +5,7 @@ "hark": "1.2.3", "jsnlog": "2.30.0", "platform": "1.3.6", + "semver": "7.3.5", "uuid": "8.3.2", "wolfy87-eventemitter": "5.2.9" }, diff --git a/openvidu-browser/src/OpenVidu/Session.ts b/openvidu-browser/src/OpenVidu/Session.ts index bcd55d1a..ad243837 100644 --- a/openvidu-browser/src/OpenVidu/Session.ts +++ b/openvidu-browser/src/OpenVidu/Session.ts @@ -44,6 +44,8 @@ import { OpenViduError, OpenViduErrorName } from '../OpenViduInternal/Enums/Open import { VideoInsertMode } from '../OpenViduInternal/Enums/VideoInsertMode'; import { OpenViduLogger } from '../OpenViduInternal/Logger/OpenViduLogger'; import { PlatformUtils } from '../OpenViduInternal/Utils/Platform'; +import semverMajor = require('semver/functions/major'); +import semverMinor = require('semver/functions/minor'); /** * @hidden @@ -1250,6 +1252,7 @@ export class Session extends EventDispatcher { token: (!!token) ? token : '', session: this.sessionId, platform: !!platform.getDescription() ? platform.getDescription() : 'unknown', + sdkVersion: this.openvidu.libraryVersion, metadata: !!this.options.metadata ? this.options.metadata : '', secret: this.openvidu.getSecret(), recorder: this.openvidu.getRecorder() @@ -1547,10 +1550,14 @@ export class Session extends EventDispatcher { if (opts.life != null) { this.openvidu.life = opts.life; } - if (opts.version !== this.openvidu.libraryVersion) { - logger.warn('OpenVidu Server (' + opts.version + - ') and OpenVidu Browser (' + this.openvidu.libraryVersion + - ') versions do NOT match. There may be incompatibilities') + const minorDifference: number = semverMinor(opts.version) - semverMinor(this.openvidu.libraryVersion); + if ((semverMajor(opts.version) !== semverMajor(this.openvidu.libraryVersion)) || !(minorDifference == 0 || minorDifference == 1)) { + logger.error(`openvidu-browser (${this.openvidu.libraryVersion}) and openvidu-server (${opts.version}) versions are incompatible. ` + + 'Errors are likely to occur. openvidu-browser SDK is only compatible with the same version or the immediately following minor version of an OpenVidu deployment'); + } else if (minorDifference == 1) { + logger.warn(`openvidu-browser version ${this.openvidu.libraryVersion} does not match openvidu-server version ${opts.version}. ` + + `These versions are still compatible with each other, but openvidu-browser version must be updated as soon as possible to ${semverMajor(opts.version)}.${semverMinor(opts.version)}.x. ` + + `This client using openvidu-browser ${this.openvidu.libraryVersion} will become incompatible with the next release of openvidu-server`); } } diff --git a/openvidu-client/src/main/java/io/openvidu/client/internal/ProtocolElements.java b/openvidu-client/src/main/java/io/openvidu/client/internal/ProtocolElements.java index a7648fc4..4e917016 100644 --- a/openvidu-client/src/main/java/io/openvidu/client/internal/ProtocolElements.java +++ b/openvidu-client/src/main/java/io/openvidu/client/internal/ProtocolElements.java @@ -38,6 +38,7 @@ public class ProtocolElements { public static final String JOINROOM_METADATA_PARAM = "metadata"; public static final String JOINROOM_SECRET_PARAM = "secret"; public static final String JOINROOM_PLATFORM_PARAM = "platform"; + public static final String JOINROOM_SDKVERSION_PARAM = "sdkVersion"; public static final String JOINROOM_RECORDER_PARAM = "recorder"; public static final String JOINROOM_PEERID_PARAM = "id"; diff --git a/openvidu-server/pom.xml b/openvidu-server/pom.xml index 80eb6a09..ae27db39 100644 --- a/openvidu-server/pom.xml +++ b/openvidu-server/pom.xml @@ -332,6 +332,11 @@ openvidu-java-client ${version.openvidu.java.client} + + org.apache.maven + maven-artifact + ${version.maven.artifact} + diff --git a/openvidu-server/src/main/java/io/openvidu/server/rpc/RpcHandler.java b/openvidu-server/src/main/java/io/openvidu/server/rpc/RpcHandler.java index 11feaaf4..7c6e3b1a 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/rpc/RpcHandler.java +++ b/openvidu-server/src/main/java/io/openvidu/server/rpc/RpcHandler.java @@ -27,6 +27,7 @@ import java.util.concurrent.ConcurrentMap; import javax.servlet.http.HttpSession; import org.apache.commons.lang3.RandomStringUtils; +import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.kurento.jsonrpc.DefaultJsonRpcHandler; import org.kurento.jsonrpc.Session; import org.kurento.jsonrpc.Transaction; @@ -46,6 +47,7 @@ import io.openvidu.client.OpenViduException; import io.openvidu.client.OpenViduException.Code; import io.openvidu.client.internal.ProtocolElements; import io.openvidu.java.client.ConnectionProperties; +import io.openvidu.server.config.OpenviduBuildInfo; import io.openvidu.server.config.OpenviduConfig; import io.openvidu.server.core.EndReason; import io.openvidu.server.core.IdentifierPrefixes; @@ -55,6 +57,8 @@ import io.openvidu.server.core.SessionManager; import io.openvidu.server.core.Token; import io.openvidu.server.utils.GeoLocation; import io.openvidu.server.utils.GeoLocationByIp; +import io.openvidu.server.utils.VersionComparator; +import io.openvidu.server.utils.VersionComparator.VersionMismatchException; public class RpcHandler extends DefaultJsonRpcHandler { @@ -63,6 +67,9 @@ public class RpcHandler extends DefaultJsonRpcHandler { @Autowired OpenviduConfig openviduConfig; + @Autowired + OpenviduBuildInfo openviduBuildConfig; + @Autowired GeoLocationByIp geoLocationByIp; @@ -292,6 +299,8 @@ public class RpcHandler extends DefaultJsonRpcHandler { participant.getPlatform()); } + checkSdkVersionCompliancy(request, participant); + rpcConnection.setSessionId(sessionId); sessionManager.joinRoom(participant, sessionId, request.getId()); @@ -795,36 +804,36 @@ public class RpcHandler extends DefaultJsonRpcHandler { return Arrays.asList("*"); } - public static String getStringParam(Request request, String key) { + public static String getStringParam(Request request, String key) throws RuntimeException { if (request.getParams() == null || request.getParams().get(key) == null) { throw new RuntimeException("Request element '" + key + "' is missing in method '" + request != null ? request.getMethod() : "[NO REQUEST OBJECT]" - + "'. CHECK THAT 'openvidu-server' AND 'openvidu-browser' SHARE THE SAME VERSION NUMBER"); + + "'. Check that 'openvidu-server' AND 'openvidu-browser' versions are compatible with each other"); } return request.getParams().get(key).getAsString(); } - public static int getIntParam(Request request, String key) { + public static int getIntParam(Request request, String key) throws RuntimeException { if (request.getParams() == null || request.getParams().get(key) == null) { throw new RuntimeException("Request element '" + key + "' is missing in method '" + request.getMethod() - + "'. CHECK THAT 'openvidu-server' AND 'openvidu-browser' SHARE THE SAME VERSION NUMBER"); + + "'. Check that 'openvidu-server' AND 'openvidu-browser' versions are compatible with each other"); } return request.getParams().get(key).getAsInt(); } - public static boolean getBooleanParam(Request request, String key) { + public static boolean getBooleanParam(Request request, String key) throws RuntimeException { if (request.getParams() == null || request.getParams().get(key) == null) { throw new RuntimeException("Request element '" + key + "' is missing in method '" + request.getMethod() - + "'. CHECK THAT 'openvidu-server' AND 'openvidu-browser' SHARE THE SAME VERSION NUMBER"); + + "'. Check that 'openvidu-server' AND 'openvidu-browser' versions are compatible with each other"); } return request.getParams().get(key).getAsBoolean(); } - public static JsonElement getParam(Request request, String key) { + public static JsonElement getParam(Request request, String key) throws RuntimeException { if (request.getParams() == null || request.getParams().get(key) == null) { throw new RuntimeException("Request element '" + key + "' is missing in method '" + request.getMethod() - + "'. CHECK THAT 'openvidu-server' AND 'openvidu-browser' SHARE THE SAME VERSION NUMBER"); + + "'. Check that 'openvidu-server' AND 'openvidu-browser' versions are compatible with each other"); } return request.getParams().get(key); } @@ -879,4 +888,39 @@ public class RpcHandler extends DefaultJsonRpcHandler { return senderPublicId; } + private void checkSdkVersionCompliancy(Request request, Participant participant) { + // TODO: remove try-catch after release 2.21.0 + String clientVersion = null; + try { + clientVersion = getStringParam(request, ProtocolElements.JOINROOM_SDKVERSION_PARAM); + } catch (RuntimeException e) { + // This empty catch is here to support client SDK 2.20.0 with server 2.21.0 + // TODO: remove try-catch after release 2.21.0 + return; + } + final String serverVersion = openviduBuildConfig.getOpenViduServerVersion(); + try { + new VersionComparator().checkVersionCompatibility(clientVersion, serverVersion); + } catch (VersionMismatchException e) { + if (e.isIncompatible()) { + log.error( + "Participant {} with IP {} and platform {} has an incompatible version of openvidu-browser SDK ({}) for this OpenVidu deployment ({}). This may cause the system to malfunction", + participant.getParticipantPublicId(), participant.getLocation().getIp(), + participant.getPlatform(), clientVersion, serverVersion); + log.error(e.getMessage()); + log.error( + "openvidu-browser SDK is only compatible with the same version or the immediately following minor version of an OpenVidu deployment"); + } else { + DefaultArtifactVersion v = new DefaultArtifactVersion(serverVersion); + log.warn( + "Participant {} with IP {} and platform {} has an older version of openvidu-browser SDK ({}) for this OpenVidu deployment ({}). " + + "These versions are still compatible with each other, but client SDK must be updated as soon as possible to {}.x. This client using " + + "openvidu-browser {} will become incompatible with the next release of openvidu-server", + participant.getParticipantPublicId(), participant.getLocation().getIp(), + participant.getPlatform(), clientVersion, serverVersion, + (v.getMajorVersion() + "." + v.getMinorVersion()), clientVersion); + } + } + } + } diff --git a/openvidu-server/src/main/java/io/openvidu/server/utils/VersionComparator.java b/openvidu-server/src/main/java/io/openvidu/server/utils/VersionComparator.java new file mode 100644 index 00000000..e5fabb52 --- /dev/null +++ b/openvidu-server/src/main/java/io/openvidu/server/utils/VersionComparator.java @@ -0,0 +1,70 @@ +package io.openvidu.server.utils; + +import java.util.regex.Pattern; + +import org.apache.maven.artifact.versioning.DefaultArtifactVersion; + +public class VersionComparator { + + // https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string + private final String OFFICIAL_SEMVER_REGEX = "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"; + + /** + * Checks if client version and server version are compatible with each other + * + * @param clientVersion + * @param serverVersion + * @throws VersionMismatchException if versions do not comply with semver + * format, or if client version and server + * version do not match + */ + public void checkVersionCompatibility(String clientVersion, String serverVersion) throws VersionMismatchException { + checkSemver(clientVersion, "openvidu-browser"); + checkSemver(serverVersion, "openvidu-server"); + DefaultArtifactVersion comparableClientVersion = new DefaultArtifactVersion(clientVersion); + DefaultArtifactVersion comparableServerVersion = new DefaultArtifactVersion(serverVersion); + if (comparableClientVersion.getMajorVersion() != comparableServerVersion.getMajorVersion()) { + throw new VersionMismatchException(true, "Incompatible major versions of openvidu-browser (\"" + + clientVersion + "\") and openvidu-server (\"" + serverVersion + "\")"); + } + checkMinorVersions(comparableClientVersion, comparableServerVersion); + } + + private void checkSemver(String version, String artifactName) { + if (!Pattern.compile(OFFICIAL_SEMVER_REGEX).matcher(version).matches()) { + throw new RuntimeException( + "Version \"" + version + "\" of " + artifactName + " does not comply with semver format"); + } + } + + private void checkMinorVersions(DefaultArtifactVersion clientVersion, DefaultArtifactVersion serverVersion) + throws VersionMismatchException { + int difference = serverVersion.getMinorVersion() - clientVersion.getMinorVersion(); + if (difference == 1) { + throw new VersionMismatchException(false, "openvidu-browser \"" + clientVersion + "\" requires update to \"" + + serverVersion.getMajorVersion() + "." + serverVersion.getMinorVersion() + ".x\""); + } + if (difference != 0) { + throw new VersionMismatchException(true, "Incompatible minor versions of openvidu-browser (\"" + + clientVersion + "\") and openvidu-server (\"" + serverVersion + "\")"); + } + } + + public class VersionMismatchException extends Exception { + + private static final long serialVersionUID = 1L; + + private boolean incompatible; + + public VersionMismatchException(boolean incompatible, String errorMessage) { + super(errorMessage); + this.incompatible = incompatible; + } + + public boolean isIncompatible() { + return this.incompatible; + } + + } + +} diff --git a/openvidu-server/src/test/java/io/openvidu/server/test/unit/VersionComparatorTest.java b/openvidu-server/src/test/java/io/openvidu/server/test/unit/VersionComparatorTest.java new file mode 100644 index 00000000..66a7f8e3 --- /dev/null +++ b/openvidu-server/src/test/java/io/openvidu/server/test/unit/VersionComparatorTest.java @@ -0,0 +1,94 @@ +package io.openvidu.server.test.unit; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +import io.openvidu.server.utils.VersionComparator; +import io.openvidu.server.utils.VersionComparator.VersionMismatchException; + +public class VersionComparatorTest { + + @Test + void versionSyntaxWrongTest() throws VersionMismatchException { + VersionComparator comparator = new VersionComparator(); + assertThrows(RuntimeException.class, () -> { + comparator.checkVersionCompatibility("not_a_valid_semver", "2.21.0"); + }); + assertThrows(RuntimeException.class, () -> { + comparator.checkVersionCompatibility("2.21.0", "not.valid.semver"); + }); + assertThrows(RuntimeException.class, () -> { + comparator.checkVersionCompatibility("2.21", "2.21"); + }); + assertThrows(RuntimeException.class, () -> { + comparator.checkVersionCompatibility("2.21.0", "2.21"); + }); + assertThrows(RuntimeException.class, () -> { + comparator.checkVersionCompatibility("2", "2.21.0"); + }); + assertThrows(RuntimeException.class, () -> { + comparator.checkVersionCompatibility("", ""); + }); + } + + @Test + void versionsDoMatchTest() throws VersionMismatchException { + VersionComparator comparator = new VersionComparator(); + comparator.checkVersionCompatibility("2.21.0", "2.21.0"); + comparator.checkVersionCompatibility("2.21.1", "2.21.0"); + comparator.checkVersionCompatibility("2.19.20", "2.19.0"); + comparator.checkVersionCompatibility("2.21.1-beta", "2.21.0"); + comparator.checkVersionCompatibility("3.0.0", "3.0.0-beta4"); + } + + @Test + void versionsDoNotMatchButAreCompatibleTest() throws VersionMismatchException { + VersionComparator comparator = new VersionComparator(); + try { + comparator.checkVersionCompatibility("2.20.0", "2.21.0"); + } catch (VersionMismatchException e) { + assertFalse(e.isIncompatible()); + } + try { + comparator.checkVersionCompatibility("2.20.5", "2.21.0"); + } catch (VersionMismatchException e) { + assertFalse(e.isIncompatible()); + } + try { + comparator.checkVersionCompatibility("2.20.5-dev", "2.21.0"); + } catch (VersionMismatchException e) { + assertFalse(e.isIncompatible()); + } + try { + comparator.checkVersionCompatibility("2.0.0", "2.0.0-dev1"); + } catch (VersionMismatchException e) { + assertFalse(e.isIncompatible()); + } + } + + @Test + void versionsIncompatibleTest() { + VersionComparator comparator = new VersionComparator(); + assertThrows(VersionMismatchException.class, () -> { + comparator.checkVersionCompatibility("2.0.0", "3.0.0"); + }); + assertThrows(VersionMismatchException.class, () -> { + comparator.checkVersionCompatibility("3.0.0", "2.0.0"); + }); + assertThrows(VersionMismatchException.class, () -> { + comparator.checkVersionCompatibility("2.21.0", "2.23.0"); + }); + assertThrows(VersionMismatchException.class, () -> { + comparator.checkVersionCompatibility("2.21.0", "2.30.0"); + }); + assertThrows(VersionMismatchException.class, () -> { + comparator.checkVersionCompatibility("2.22.0-dev1", "2.21.0"); + }); + assertThrows(VersionMismatchException.class, () -> { + comparator.checkVersionCompatibility("2.22.0", "2.21.0-dev1"); + }); + } + +} diff --git a/pom.xml b/pom.xml index d75f21db..503461a1 100644 --- a/pom.xml +++ b/pom.xml @@ -75,6 +75,7 @@ 1.6.8 3.0.0 3.2.0 + 3.8.3 11 11