├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── maven.yml ├── .gitignore ├── .mvn └── jvm.config ├── LICENSE ├── README.md ├── SECURITY.md ├── jicoco-config ├── bnd.bnd ├── pom.xml └── src │ ├── main │ └── kotlin │ │ └── org │ │ └── jitsi │ │ └── config │ │ ├── AbstractReadOnlyConfigurationService.kt │ │ ├── ConfigExtensions.kt │ │ ├── ConfigSourceWrapper.kt │ │ ├── ConfigurationServiceConfigSource.kt │ │ ├── JitsiConfig.kt │ │ ├── ReadOnlyConfigurationService.kt │ │ └── TypesafeConfigSource.kt │ └── test │ └── kotlin │ └── org │ └── jitsi │ └── config │ ├── AbstractReadOnlyConfigurationServiceTest.kt │ ├── ConfigExtensionsKtTest.kt │ └── TypesafeConfigSourceTest.kt ├── jicoco-health-checker ├── checkstyle.xml ├── pom.xml └── src │ └── main │ └── kotlin │ └── org │ └── jitsi │ └── health │ └── HealthChecker.kt ├── jicoco-jetty ├── checkstyle.xml ├── pom.xml └── src │ ├── main │ ├── java │ │ └── org │ │ │ └── jitsi │ │ │ ├── meet │ │ │ └── ShutdownService.java │ │ │ └── rest │ │ │ ├── Health.java │ │ │ └── Version.java │ └── kotlin │ │ └── org │ │ └── jitsi │ │ ├── rest │ │ ├── JettyBundleActivatorConfig.kt │ │ └── JettyHelpers.kt │ │ └── shutdown │ │ └── ShutdownServiceImpl.kt │ └── test │ ├── java │ └── org │ │ └── jitsi │ │ └── rest │ │ ├── HealthTest.java │ │ └── VersionTest.java │ └── kotlin │ └── org │ └── jitsi │ └── shutdown │ └── ShutdownServiceImplTest.kt ├── jicoco-jwt ├── pom.xml └── src │ ├── main │ └── kotlin │ │ └── org │ │ └── jitsi │ │ └── jwt │ │ ├── JitsiToken.kt │ │ ├── JwtInfo.kt │ │ ├── RefreshingJwt.kt │ │ └── RefreshingProperty.kt │ └── test │ └── kotlin │ └── org │ └── jitsi │ └── jwt │ ├── JitsiTokenTest.kt │ └── RefreshingPropertyTest.kt ├── jicoco-mediajson ├── pom.xml └── src │ ├── main │ └── kotlin │ │ └── org │ │ └── jitsi │ │ └── mediajson │ │ └── MediaJson.kt │ └── test │ └── kotlin │ └── org │ └── jitsi │ └── mediajson │ └── MediaJsonTest.kt ├── jicoco-metrics ├── checkstyle.xml ├── pom.xml └── src │ ├── main │ └── kotlin │ │ └── org │ │ └── jitsi │ │ └── metrics │ │ ├── BooleanMetric.kt │ │ ├── CounterMetric.kt │ │ ├── DoubleGaugeMetric.kt │ │ ├── HistogramMetric.kt │ │ ├── InfoMetric.kt │ │ ├── LongGaugeMetric.kt │ │ ├── Metric.kt │ │ ├── MetricsContainer.kt │ │ └── MetricsUpdater.kt │ └── test │ └── kotlin │ └── org │ └── jitsi │ └── metrics │ ├── MetricTest.kt │ └── MetricsContainerTest.kt ├── jicoco-mucclient ├── checkstyle.xml ├── pom.xml └── src │ ├── main │ ├── java │ │ └── org │ │ │ └── jitsi │ │ │ ├── retry │ │ │ ├── RetryStrategy.java │ │ │ ├── RetryTask.java │ │ │ └── SimpleRetryTask.java │ │ │ └── xmpp │ │ │ ├── TrustAllHostnameVerifier.java │ │ │ ├── TrustAllX509TrustManager.java │ │ │ └── mucclient │ │ │ ├── ConnectionStateListener.java │ │ │ ├── IQListener.java │ │ │ ├── MucClient.java │ │ │ ├── MucClientConfiguration.java │ │ │ └── MucClientManager.java │ └── kotlin │ │ └── org │ │ └── jitsi │ │ └── xmpp │ │ └── util │ │ └── ErrorUtil.kt │ └── test │ └── java │ └── org │ └── jitsi │ └── retry │ └── RetryStrategyTest.java ├── jicoco-test-kotlin ├── pom.xml └── src │ └── main │ └── kotlin │ └── org │ └── jitsi │ └── config │ ├── ConfigTestHelpers.kt │ └── TestReadOnlyConfigurationService.kt └── pom.xml /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | charset = utf-8 5 | 6 | [*.{kt,kts}] 7 | max_line_length=120 8 | 9 | ktlint_code_style = intellij_idea 10 | 11 | # I find trailing commas annoying 12 | ktlint_standard_trailing-comma-on-call-site = disabled 13 | ktlint_standard_trailing-comma-on-declaration-site = disabled 14 | 15 | [pom.xml] 16 | indent_style = space 17 | indent_size = 4 18 | max_line_length = 120 19 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.java text eol=lf diff=java 2 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Java CI with Maven 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | env: 13 | # Java version to use for the release 14 | RELEASE_JAVA_VERSION: 11 15 | 16 | jobs: 17 | test: 18 | runs-on: ubuntu-latest 19 | 20 | strategy: 21 | matrix: 22 | java: [ 11, 17, 21 ] 23 | 24 | name: Java ${{ matrix.java }} 25 | 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | 30 | - name: Set up JDK ${{ matrix.java }} 31 | uses: actions/setup-java@v4 32 | with: 33 | distribution: temurin 34 | java-version: ${{ matrix.java }} 35 | cache: maven 36 | 37 | - name: Build and test with Maven 38 | run: mvn -B -Pcoverage,central-modules,noncentral-modules,release verify 39 | 40 | - name: Upload coverage report 41 | if: matrix.java == env.RELEASE_JAVA_VERSION 42 | uses: codecov/codecov-action@v4 43 | with: 44 | token: ${{ secrets.CODECOV_TOKEN }} 45 | 46 | release: 47 | if: github.ref == 'refs/heads/master' 48 | needs: test 49 | runs-on: ubuntu-latest 50 | steps: 51 | - name: Checkout 52 | uses: actions/checkout@v4 53 | with: 54 | fetch-depth: 0 55 | 56 | - name: Set up JDK 57 | uses: actions/setup-java@v4 58 | with: 59 | distribution: temurin 60 | java-version: ${{ env.RELEASE_JAVA_VERSION }} 61 | cache: maven 62 | server-id: ossrh 63 | server-username: SONATYPE_USER 64 | server-password: SONATYPE_PW 65 | 66 | - name: Set version 67 | run: | 68 | VERSION=`git describe --match "v[0-9\.]*" --long --dirty --always` 69 | mvn -B versions:set -DnewVersion=${VERSION:1} -DgenerateBackupPoms=false 70 | 71 | - name: Release to Maven Central 72 | env: 73 | SONATYPE_USER: ${{ secrets.SONATYPE_USER }} 74 | SONATYPE_PW: ${{ secrets.SONATYPE_PW }} 75 | run: | 76 | cat <(echo -e "${{ secrets.GPG_KEY }}") | gpg --batch --import 77 | gpg --list-secret-keys --keyid-format LONG 78 | mvn \ 79 | --no-transfer-progress \ 80 | --batch-mode \ 81 | -Dgpg.passphrase="${{ secrets.GPG_PW }}" \ 82 | -Pcentral-modules,release,signing \ 83 | -DskipTests \ 84 | deploy 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target/ 2 | *.iml 3 | **/.idea/ 4 | *.swp 5 | .kotlintest 6 | -------------------------------------------------------------------------------- /.mvn/jvm.config: -------------------------------------------------------------------------------- 1 | --add-opens java.base/java.lang=ALL-UNNAMED 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Intro 2 | 3 | This project contains low level XMPP/HTTP handling that is common to Jicofo, Jigasi and the Jitsi Videobridge. 4 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | ## Reporting security issuess 4 | 5 | We take security very seriously and develop all Jitsi projects to be secure and safe. 6 | 7 | If you find (or simply suspect) a security issue in any of the Jitsi projects, please send us an email to security@jitsi.org. 8 | 9 | **We encourage responsible disclosure for the sake of our users, so please reach out before posting in a public space.** 10 | -------------------------------------------------------------------------------- /jicoco-config/bnd.bnd: -------------------------------------------------------------------------------- 1 | Export-Package: org.jitsi.config.* 2 | Import-Package: *;version=! 3 | Bundle-SymbolicName: org.jitsi.config 4 | -noextraheaders: true 5 | -snapshot: SNAPSHOT 6 | -removeheaders: Require-Capability, Bundle-SCM 7 | -------------------------------------------------------------------------------- /jicoco-config/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 21 | 4.0.0 22 | 23 | 24 | org.jitsi 25 | jicoco-parent 26 | 1.1-SNAPSHOT 27 | 28 | 29 | jicoco-config 30 | 1.1-SNAPSHOT 31 | jicoco-config 32 | Jitsi Common Components (Configuration Utilities) 33 | 34 | 35 | 36 | org.jetbrains.kotlin 37 | kotlin-stdlib-jdk8 38 | 39 | 40 | ${project.groupId} 41 | jitsi-utils 42 | 43 | 44 | ${project.groupId} 45 | jitsi-metaconfig 46 | 1.0-12-g02d4bd5 47 | 48 | 49 | com.typesafe 50 | config 51 | 1.4.2 52 | 53 | 54 | org.jetbrains.kotlin 55 | kotlin-reflect 56 | ${kotlin.version} 57 | 58 | 59 | 60 | io.kotest 61 | kotest-runner-junit5-jvm 62 | ${kotest.version} 63 | test 64 | 65 | 66 | io.kotest 67 | kotest-assertions-core-jvm 68 | ${kotest.version} 69 | test 70 | 71 | 72 | 73 | 74 | src/main/kotlin 75 | src/test/kotlin 76 | 77 | 78 | org.jetbrains.kotlin 79 | kotlin-maven-plugin 80 | ${kotlin.version} 81 | 82 | 83 | compile 84 | compile 85 | 86 | compile 87 | 88 | 89 | 90 | -opt-in=kotlin.ExperimentalStdlibApi 91 | 92 | 93 | 94 | 95 | test-compile 96 | test-compile 97 | 98 | test-compile 99 | 100 | 101 | 102 | -opt-in=kotlin.ExperimentalStdlibApi 103 | 104 | 105 | 106 | 107 | 108 | 11 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /jicoco-config/src/main/kotlin/org/jitsi/config/AbstractReadOnlyConfigurationService.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.config 18 | 19 | import org.jitsi.service.configuration.ConfigVetoableChangeListener 20 | import org.jitsi.service.configuration.ConfigurationService 21 | import org.jitsi.utils.logging2.LoggerImpl 22 | import java.beans.PropertyChangeListener 23 | import java.util.Properties 24 | 25 | /** 26 | * A stripped-down implementation of [ConfigurationService] which serves two purposes: 27 | * 1) Injected as an OSGi implementation of [ConfigurationService] for libs which still 28 | * expect to find a [ConfigurationService] via OSGi 29 | * 2) Wrapped by [ConfigurationServiceConfigSource] to be used in new config 30 | * 31 | * NOTE: this abstract base class exists so that a test implementation can be written 32 | * which pulls [properties] from somewhere else. 33 | */ 34 | abstract class AbstractReadOnlyConfigurationService : ConfigurationService { 35 | protected val logger = LoggerImpl(this::class.qualifiedName) 36 | protected abstract val properties: Properties 37 | 38 | override fun getString(propertyName: String): String? = getProperty(propertyName)?.toString()?.trim() 39 | 40 | override fun getString(propertyName: String, defaultValue: String?): String? = 41 | getString(propertyName) ?: defaultValue 42 | 43 | override fun getBoolean(propertyName: String, defaultValue: Boolean): Boolean = 44 | getString(propertyName)?.toBoolean() ?: defaultValue 45 | 46 | override fun getDouble(propertyName: String, defaultValue: Double): Double = 47 | getString(propertyName)?.toDouble() ?: defaultValue 48 | 49 | override fun getInt(propertyName: String, defaultValue: Int): Int = getString(propertyName)?.toInt() ?: defaultValue 50 | 51 | override fun getLong(propertyName: String, defaultValue: Long): Long = 52 | getString(propertyName)?.toLong() ?: defaultValue 53 | 54 | override fun getAllPropertyNames(): MutableList = properties.keys.map { it as String }.toMutableList() 55 | 56 | override fun getProperty(propertyName: String): Any? = properties[propertyName] ?: System.getProperty(propertyName) 57 | 58 | override fun getPropertyNamesByPrefix(prefix: String, exactPrefixMatch: Boolean): MutableList { 59 | val matchingPropNames = mutableListOf() 60 | for (name in allPropertyNames) { 61 | if (exactPrefixMatch) { 62 | if (name.substringBeforeLast(delimiter = ".", missingDelimiterValue = "") == prefix) { 63 | matchingPropNames += name 64 | } 65 | } else if (name.startsWith(prefix)) { 66 | matchingPropNames += name 67 | } 68 | } 69 | return matchingPropNames 70 | } 71 | 72 | override fun logConfigurationProperties(passwordPattern: String) { 73 | val regex = Regex(passwordPattern).takeIf { passwordPattern.isNotEmpty() } 74 | 75 | for (name in allPropertyNames) { 76 | var value = getProperty(name) ?: continue 77 | 78 | if (regex?.matches(name) == true) { 79 | value = "**********" 80 | } 81 | logger.info("$name = $value") 82 | } 83 | } 84 | 85 | override fun getConfigurationFilename(): String = throw Exception("Unsupported") 86 | 87 | override fun getScHomeDirLocation(): String = throw Exception("Unsupported") 88 | 89 | override fun getScHomeDirName(): String = throw Exception("Unsupported") 90 | 91 | override fun addPropertyChangeListener(listener: PropertyChangeListener?) = throw Exception("Unsupported") 92 | 93 | override fun addPropertyChangeListener(propertyName: String?, listener: PropertyChangeListener?) = 94 | throw Exception("Unsupported") 95 | 96 | override fun addVetoableChangeListener(listener: ConfigVetoableChangeListener?) = throw Exception("Unsupported") 97 | 98 | override fun addVetoableChangeListener(propertyName: String?, listener: ConfigVetoableChangeListener?) = 99 | throw Exception("Unsupported") 100 | 101 | override fun removePropertyChangeListener(listener: PropertyChangeListener?) = throw Exception("Unsupported") 102 | 103 | override fun removePropertyChangeListener(propertyName: String?, listener: PropertyChangeListener?) = 104 | throw Exception("Unsupported") 105 | 106 | override fun removeVetoableChangeListener(listener: ConfigVetoableChangeListener?) = throw Exception("Unsupported") 107 | 108 | override fun removeVetoableChangeListener(propertyName: String?, listener: ConfigVetoableChangeListener?) = 109 | throw Exception("Unsupported") 110 | 111 | override fun purgeStoredConfiguration() = throw Exception("Unsupported") 112 | 113 | override fun storeConfiguration() = throw Exception("Unsupported") 114 | 115 | override fun setProperties(properties: MutableMap?) = throw Exception("Unsupported") 116 | 117 | override fun setProperty(propertyName: String?, property: Any?) = throw Exception("Unsupported") 118 | 119 | override fun setProperty(propertyName: String?, property: Any?, isSystem: Boolean) = throw Exception("Unsupported") 120 | 121 | override fun removeProperty(propertyName: String?) = throw Exception("Unsupported") 122 | 123 | override fun getPropertyNamesBySuffix(suffix: String?): MutableList = throw Exception("Unsupported") 124 | } 125 | -------------------------------------------------------------------------------- /jicoco-config/src/main/kotlin/org/jitsi/config/ConfigExtensions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.config 18 | 19 | import com.typesafe.config.Config 20 | import com.typesafe.config.ConfigFactory 21 | import com.typesafe.config.ConfigValueFactory 22 | import org.jitsi.utils.ConfigUtils 23 | import java.util.regex.Pattern 24 | 25 | private fun shouldMask(pattern: Pattern?, key: String): Boolean { 26 | pattern ?: return false 27 | return pattern.matcher(key).find() 28 | } 29 | 30 | const val MASK = "******" 31 | 32 | /** 33 | * Returns a new [Config] with any values matching 34 | * [ConfigUtils.PASSWORD_SYS_PROPS] changed to [MASK]. NOTE 35 | * that this will change the type of the value to a [String] 36 | */ 37 | fun Config.mask(): Config { 38 | // Or should this be held in the config itself? 39 | val fieldRegex = ConfigUtils.PASSWORD_SYS_PROPS ?: return ConfigFactory.load(this) 40 | val pattern = 41 | Pattern.compile(fieldRegex, Pattern.CASE_INSENSITIVE) 42 | return entrySet().fold(this) { config, (key, _) -> 43 | when { 44 | shouldMask(pattern, key) -> config.withValue(key, ConfigValueFactory.fromAnyRef(MASK)) 45 | else -> config 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /jicoco-config/src/main/kotlin/org/jitsi/config/ConfigSourceWrapper.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.config 18 | 19 | import org.jitsi.metaconfig.ConfigSource 20 | import kotlin.reflect.KType 21 | 22 | /** 23 | * A wrapper around a [ConfigSource] that allows changing the underlying 24 | * [ConfigSource] at any time. We use this in [JitsiConfig] so that test 25 | * code can swap out the underlying [ConfigSource]. 26 | */ 27 | class ConfigSourceWrapper( 28 | var innerSource: ConfigSource 29 | ) : ConfigSource { 30 | 31 | override val name: String 32 | get() = innerSource.name 33 | 34 | override fun getterFor(type: KType): (String) -> Any = innerSource.getterFor(type) 35 | } 36 | -------------------------------------------------------------------------------- /jicoco-config/src/main/kotlin/org/jitsi/config/ConfigurationServiceConfigSource.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.config 18 | 19 | import org.jitsi.metaconfig.ConfigException 20 | import org.jitsi.metaconfig.ConfigSource 21 | import org.jitsi.service.configuration.ConfigurationService 22 | import kotlin.reflect.KType 23 | import kotlin.reflect.typeOf 24 | 25 | /** 26 | * A [ConfigSource] implementation which is backed by a [ConfigurationService] instance. 27 | */ 28 | class ConfigurationServiceConfigSource( 29 | override val name: String, 30 | private val config: ConfigurationService 31 | ) : ConfigSource { 32 | 33 | /** 34 | * Note that we can't use getBoolean, getInt, etc. in [ConfigurationService] because they 35 | * all take a default value, which we don't want (because if the value isn't found we want 36 | * to throw [ConfigException.UnableToRetrieve.NotFound] so the calling code can fall back to 37 | * another property). 38 | */ 39 | override fun getterFor(type: KType): (String) -> Any { 40 | return when (type) { 41 | typeOf() -> { key -> config.getStringOrThrow(key) } 42 | typeOf() -> { key -> config.getStringOrThrow(key).toBoolean() } 43 | typeOf() -> { key -> config.getStringOrThrow(key).toDouble() } 44 | typeOf() -> { key -> config.getStringOrThrow(key).toInt() } 45 | typeOf() -> { key -> config.getStringOrThrow(key).toLong() } 46 | // Map is a special case and we interpret it as: 47 | // For the given prefix, return me all the properties which start 48 | // with that prefix mapped to their values (retrieved as Strings) 49 | typeOf>() -> { key -> 50 | val props = mutableMapOf() 51 | for (propName in config.getPropertyNamesByPrefix(key, false)) { 52 | props[propName] = config.getString(propName) 53 | } 54 | props 55 | } 56 | else -> throw ConfigException.UnsupportedType("Type $type not supported in source '$name'") 57 | } 58 | } 59 | 60 | private fun ConfigurationService.getStringOrThrow(key: String): String = 61 | getString(key) ?: throw ConfigException.UnableToRetrieve.NotFound("Key '$key' not found in source '$name'") 62 | } 63 | -------------------------------------------------------------------------------- /jicoco-config/src/main/kotlin/org/jitsi/config/JitsiConfig.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.config 18 | 19 | import com.typesafe.config.Config 20 | import com.typesafe.config.ConfigFactory 21 | import org.jitsi.metaconfig.ConfigSource 22 | import org.jitsi.service.configuration.ConfigurationService 23 | import org.jitsi.utils.logging2.LoggerImpl 24 | 25 | /** 26 | * Holds common [ConfigSource] instances for retrieving configuration. 27 | * 28 | * Should be renamed to JitsiConfig once the old one is removed. 29 | */ 30 | @Suppress("unused", "MemberVisibilityCanBePrivate") 31 | class JitsiConfig { 32 | companion object { 33 | val logger = LoggerImpl(JitsiConfig::class.simpleName) 34 | 35 | /** 36 | * A [ConfigSource] loaded via [ConfigFactory]. 37 | */ 38 | var typesafeConfig: ConfigSource = TypesafeConfigSource("typesafe config", loadNewConfig()) 39 | private set 40 | 41 | private var numTypesafeReloads = 0 42 | 43 | /** 44 | * The 'new' [ConfigSource] that should be used by configuration properties. Able to be changed for testing. 45 | */ 46 | private val _newConfig: ConfigSourceWrapper = ConfigSourceWrapper(typesafeConfig).also { 47 | logger.info("Initialized newConfig: ${typesafeConfig.description}") 48 | } 49 | val newConfig: ConfigSource 50 | get() = _newConfig 51 | 52 | /** 53 | * A [ConfigurationService] which can be installed via OSGi for legacy code which still requires it. 54 | */ 55 | @JvmStatic 56 | val SipCommunicatorProps: ConfigurationService = ReadOnlyConfigurationService() 57 | 58 | /** 59 | * A [ConfigSource] wrapper around the legacy [ConfigurationService]. 60 | */ 61 | val SipCommunicatorPropsConfigSource: ConfigSource = 62 | ConfigurationServiceConfigSource("sip communicator props", SipCommunicatorProps) 63 | 64 | /** 65 | * The 'legacy' [ConfigSource] that should be used by configuration properties. Able to be changed for testing. 66 | */ 67 | private val _legacyConfig: ConfigSourceWrapper = ConfigSourceWrapper(SipCommunicatorPropsConfigSource).also { 68 | logger.info("Initialized legacyConfig: ${SipCommunicatorPropsConfigSource.description}") 69 | } 70 | val legacyConfig: ConfigSource 71 | get() = _legacyConfig 72 | 73 | fun useDebugNewConfig(config: ConfigSource) { 74 | logger.info("Replacing newConfig with ${config.description}") 75 | _newConfig.innerSource = config 76 | } 77 | 78 | fun useDebugLegacyConfig(config: ConfigSource) { 79 | logger.info("Replacing legacyConfig with ${config.description}") 80 | _legacyConfig.innerSource = config 81 | } 82 | 83 | private fun loadNewConfig(): Config { 84 | // Parse an application replacement (something passed via -Dconfig.file), if there is one 85 | return ConfigFactory.parseApplicationReplacement().orElse(ConfigFactory.empty()) 86 | // Fallback to application.(conf|json|properties) 87 | .withFallback(ConfigFactory.parseResourcesAnySyntax("application")) 88 | // Fallback to reference.(conf|json|properties) 89 | .withFallback(ConfigFactory.defaultReference()) 90 | .resolve() 91 | } 92 | 93 | fun reloadNewConfig() { 94 | logger.info("Reloading the Typesafe config source (previously reloaded $numTypesafeReloads times).") 95 | ConfigFactory.invalidateCaches() 96 | numTypesafeReloads++ 97 | typesafeConfig = TypesafeConfigSource( 98 | "typesafe config (reloaded $numTypesafeReloads times)", 99 | loadNewConfig() 100 | ) 101 | _newConfig.innerSource = typesafeConfig 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /jicoco-config/src/main/kotlin/org/jitsi/config/ReadOnlyConfigurationService.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.config 18 | 19 | import org.jitsi.service.configuration.ConfigurationService 20 | import java.nio.file.Paths 21 | import java.util.Properties 22 | 23 | /** 24 | * An implementation of [AbstractReadOnlyConfigurationService] which supports reading 25 | * the properties from a file whose location is determined by the 26 | * [ConfigurationService.PNAME_SC_HOME_DIR_LOCATION] and [ConfigurationService.PNAME_SC_HOME_DIR_NAME] 27 | * properties. 28 | */ 29 | class ReadOnlyConfigurationService : AbstractReadOnlyConfigurationService() { 30 | override val properties: Properties = Properties() 31 | 32 | init { 33 | reloadConfiguration() 34 | } 35 | 36 | override fun reloadConfiguration() { 37 | val scHomeDirLocation = System.getenv(ConfigurationService.PNAME_SC_HOME_DIR_LOCATION) 38 | ?: System.getProperty(ConfigurationService.PNAME_SC_HOME_DIR_LOCATION) 39 | ?: run { 40 | logger.info("${ConfigurationService.PNAME_SC_HOME_DIR_LOCATION} not set") 41 | return 42 | } 43 | val scHomeDirName = System.getenv(ConfigurationService.PNAME_SC_HOME_DIR_NAME) 44 | ?: System.getProperty(ConfigurationService.PNAME_SC_HOME_DIR_NAME) 45 | ?: run { 46 | logger.info("${ConfigurationService.PNAME_SC_HOME_DIR_NAME} not set") 47 | return 48 | } 49 | val fileName = "sip-communicator.properties" 50 | with(Paths.get(scHomeDirLocation, scHomeDirName, fileName)) { 51 | logger.info("loading config file at path $this") 52 | try { 53 | val reader = toFile().bufferedReader() 54 | properties.load(reader) 55 | } catch (t: Throwable) { 56 | logger.info("Error loading config file: $t") 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /jicoco-config/src/main/kotlin/org/jitsi/config/TypesafeConfigSource.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.config 18 | 19 | import com.typesafe.config.Config 20 | import com.typesafe.config.ConfigObject 21 | import org.jitsi.metaconfig.ConfigException 22 | import org.jitsi.metaconfig.ConfigSource 23 | import java.time.Duration 24 | import java.util.regex.Pattern 25 | import kotlin.reflect.KClass 26 | import kotlin.reflect.KType 27 | import kotlin.reflect.full.isSubtypeOf 28 | import kotlin.reflect.typeOf 29 | 30 | /** 31 | * A [ConfigSource] implementation backed by a [Config] instance. 32 | */ 33 | class TypesafeConfigSource( 34 | override val name: String, 35 | private val config: Config 36 | ) : ConfigSource { 37 | 38 | override val description: String = config.origin().description() 39 | 40 | override fun getterFor(type: KType): (String) -> Any { 41 | if (type.isSubtypeOf(typeOf>())) { 42 | @Suppress("UNCHECKED_CAST") 43 | return getterForEnum(type.classifier as KClass) 44 | } 45 | return when (type) { 46 | typeOf() -> wrap { key -> 47 | // Typesafe is case-sensitive and does not accept "True" or "False" as valid boolean values. 48 | when (config.getString(key).lowercase()) { 49 | "true" -> true 50 | "false" -> false 51 | else -> config.getBoolean(key) 52 | } 53 | } 54 | typeOf() -> wrap { key -> config.getInt(key) } 55 | typeOf() -> wrap { key -> config.getLong(key) } 56 | // Support expressions such as "5%" 57 | typeOf() -> wrap { key -> 58 | try { 59 | config.getDouble(key) 60 | } catch (wrongTypeException: com.typesafe.config.ConfigException.WrongType) { 61 | val stringValue: String = config.getString(key).trim() 62 | if (stringValue.endsWith("%")) { 63 | try { 64 | 0.01 * stringValue.dropLast(1).toDouble() 65 | } catch (e: Throwable) { 66 | throw wrongTypeException 67 | } 68 | } else { 69 | throw wrongTypeException 70 | } 71 | } 72 | } 73 | typeOf() -> wrap { key -> config.getString(key) } 74 | typeOf>() -> wrap { key -> config.getStringList(key) } 75 | typeOf>() -> wrap { key -> config.getIntList(key) } 76 | typeOf() -> wrap { key -> config.getDuration(key) } 77 | typeOf() -> wrap { key -> config.getObject(key) } 78 | typeOf>() -> wrap { key -> config.getConfigList(key) } 79 | typeOf() -> wrap { key -> Pattern.compile(config.getString(key)) } 80 | else -> throw ConfigException.UnsupportedType("Type $type unsupported") 81 | } 82 | } 83 | 84 | private fun > getterForEnum(clazz: KClass): (String) -> Any { 85 | return wrap { key -> config.getEnum(clazz.java, key) } 86 | } 87 | 88 | /** 89 | * Translate [com.typesafe.config.ConfigException]s into [ConfigException] 90 | */ 91 | private fun wrap(block: (String) -> Any): (String) -> Any { 92 | return { key -> 93 | try { 94 | block(key) 95 | } catch (e: com.typesafe.config.ConfigException.Missing) { 96 | throw ConfigException.UnableToRetrieve.NotFound("Key '$key' not found in source '$name'") 97 | } catch (e: com.typesafe.config.ConfigException.WrongType) { 98 | throw ConfigException.UnableToRetrieve.WrongType("Key '$key' in source '$name': ${e.message}") 99 | } catch (e: com.typesafe.config.ConfigException) { 100 | throw ConfigException.UnableToRetrieve.NotFound(e.message ?: "typesafe exception: ${e::class}") 101 | } catch (t: Throwable) { 102 | throw ConfigException.UnableToRetrieve.Error(t) 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /jicoco-config/src/test/kotlin/org/jitsi/config/AbstractReadOnlyConfigurationServiceTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.config 18 | 19 | import io.kotest.core.spec.IsolationMode 20 | import io.kotest.core.spec.style.ShouldSpec 21 | import io.kotest.extensions.system.withSystemProperties 22 | import io.kotest.matchers.collections.shouldContainAll 23 | import io.kotest.matchers.collections.shouldHaveSize 24 | import io.kotest.matchers.shouldBe 25 | import java.util.Properties 26 | 27 | class AbstractReadOnlyConfigurationServiceTest : ShouldSpec() { 28 | override fun isolationMode(): IsolationMode? = IsolationMode.InstancePerLeaf 29 | 30 | private val config = TestReadOnlyConfigurationService() 31 | 32 | init { 33 | context("retrieving a property") { 34 | context("present in both the config and the system") { 35 | config["some.prop"] = "42" 36 | withSystemProperties("some.prop" to "43") { 37 | should("use the config property") { 38 | config.getInt("some.prop", 0) shouldBe 42 39 | } 40 | } 41 | } 42 | context("not present anywhere") { 43 | should("return null") { 44 | config.getProperty("missing") shouldBe null 45 | } 46 | } 47 | } 48 | context("retrieving all properties by prefix") { 49 | config["a.b.c.d"] = "one" 50 | config["a.b.c.e"] = "two" 51 | config["a.b"] = "three" 52 | context("when an exact prefix match is requested") { 53 | val props = config.getPropertyNamesByPrefix("a.b.c", exactPrefixMatch = true) 54 | should("retrieve the right properties") { 55 | props shouldHaveSize 2 56 | props shouldContainAll listOf("a.b.c.d", "a.b.c.e") 57 | } 58 | } 59 | context("when an exact prefix match is not requested") { 60 | val props = config.getPropertyNamesByPrefix("a", exactPrefixMatch = false) 61 | should("retrieve the right properties") { 62 | props shouldHaveSize 3 63 | props shouldContainAll listOf("a.b.c.d", "a.b.c.e", "a.b") 64 | } 65 | } 66 | } 67 | } 68 | } 69 | 70 | private class TestReadOnlyConfigurationService( 71 | override val properties: Properties = Properties() 72 | ) : AbstractReadOnlyConfigurationService(), MutableMap by properties { 73 | override fun reloadConfiguration() {} 74 | } 75 | -------------------------------------------------------------------------------- /jicoco-config/src/test/kotlin/org/jitsi/config/ConfigExtensionsKtTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.config 18 | 19 | import com.typesafe.config.ConfigFactory 20 | import io.kotest.core.spec.IsolationMode 21 | import io.kotest.core.spec.style.ShouldSpec 22 | import io.kotest.matchers.shouldBe 23 | import org.jitsi.utils.ConfigUtils 24 | 25 | class ConfigExtensionsKtTest : ShouldSpec() { 26 | override fun isolationMode(): IsolationMode? = IsolationMode.InstancePerLeaf 27 | 28 | init { 29 | context("mask") { 30 | val config = ConfigFactory.parseString( 31 | """ 32 | a { 33 | pass-prop = s3cr3t 34 | normal-prop = 10 35 | b { 36 | nested-pass-prop = 42 37 | nested-normal-prop = hello 38 | } 39 | } 40 | """.trimIndent() 41 | ) 42 | context("with a set field regex") { 43 | ConfigUtils.PASSWORD_SYS_PROPS = "pass" 44 | val maskedConfig = config.mask() 45 | should("mask out the right values") { 46 | maskedConfig.getString("a.pass-prop") shouldBe MASK 47 | maskedConfig.getString("a.b.nested-pass-prop") shouldBe MASK 48 | } 49 | should("not mask out other values") { 50 | maskedConfig.getInt("a.normal-prop") shouldBe 10 51 | maskedConfig.getString("a.b.nested-normal-prop") shouldBe "hello" 52 | } 53 | should("not affect the original config") { 54 | config.getString("a.pass-prop") shouldBe "s3cr3t" 55 | config.getInt("a.normal-prop") shouldBe 10 56 | config.getInt("a.b.nested-pass-prop") shouldBe 42 57 | config.getString("a.b.nested-normal-prop") shouldBe "hello" 58 | } 59 | } 60 | context("when the field regex is null") { 61 | ConfigUtils.PASSWORD_SYS_PROPS = null 62 | val maskedConfig = config.mask() 63 | context("should not change anything") { 64 | maskedConfig.getString("a.pass-prop") shouldBe "s3cr3t" 65 | maskedConfig.getInt("a.normal-prop") shouldBe 10 66 | maskedConfig.getInt("a.b.nested-pass-prop") shouldBe 42 67 | maskedConfig.getString("a.b.nested-normal-prop") shouldBe "hello" 68 | } 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /jicoco-config/src/test/kotlin/org/jitsi/config/TypesafeConfigSourceTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.config 18 | 19 | import com.typesafe.config.Config 20 | import com.typesafe.config.ConfigFactory 21 | import com.typesafe.config.ConfigObject 22 | import io.kotest.assertions.throwables.shouldThrow 23 | import io.kotest.core.spec.IsolationMode 24 | import io.kotest.core.spec.style.ShouldSpec 25 | import io.kotest.matchers.collections.shouldHaveSize 26 | import io.kotest.matchers.shouldBe 27 | import org.jitsi.metaconfig.ConfigException 28 | import org.jitsi.metaconfig.ConfigSource 29 | import java.time.Duration 30 | import java.util.regex.Pattern 31 | import kotlin.reflect.typeOf 32 | 33 | class TypesafeConfigSourceTest : ShouldSpec() { 34 | override fun isolationMode(): IsolationMode? = IsolationMode.InstancePerLeaf 35 | 36 | init { 37 | context("Retrieving a value of type") { 38 | context("Boolean") { 39 | mapOf( 40 | "true" to true, "True" to true, "TRUE" to true, "\"true\"" to true, "\"True\"" to true, 41 | "false" to false, "False" to false, "FALSE" to false, 42 | "\"false\"" to false, "\"False\"" to false 43 | ).forEach { (k, v) -> 44 | context("Parsing $k") { 45 | withConfig("boolean=$k") { 46 | getValue("boolean") shouldBe v 47 | } 48 | } 49 | } 50 | listOf("X", "32", "\"\"").forEach { 51 | context("Parsing $it") { 52 | withConfig("boolean=$it") { 53 | shouldThrow { getValue("boolean") } 54 | } 55 | } 56 | } 57 | } 58 | context("Int") { 59 | withConfig("int=42") { 60 | getValue("int") shouldBe 42 61 | } 62 | } 63 | context("Long") { 64 | withConfig("long=42") { 65 | getValue("long") shouldBe 42L 66 | } 67 | } 68 | context("Double") { 69 | mapOf( 70 | "42" to 42.0, "42.5" to 42.5, "42.5e-2" to 42.5e-2, 71 | "-42" to -42.0, "-42.5" to -42.5, "-42.5e-2" to -42.5e-2, 72 | "42.5%" to 42.5e-2, "42.5 %" to 42.5e-2, "42 %" to 42e-2, 73 | "-42.5%" to -42.5e-2, "-42.5 %" to -42.5e-2, "-42 %" to -42e-2 74 | ).forEach { (k, v) -> 75 | 76 | context("Parsing $k") { 77 | withConfig("double=$k") { 78 | getValue("double") shouldBe v 79 | } 80 | } 81 | } 82 | listOf("\"\"", "X", "[1,2,3]", "5X", "5.2%%").forEach { 83 | context("Parsing $it") { 84 | withConfig("double=$it") { 85 | shouldThrow { getValue("double") } 86 | } 87 | } 88 | } 89 | } 90 | context("String") { 91 | withConfig("string=\"hello, world\"") { 92 | getValue("string") shouldBe "hello, world" 93 | } 94 | } 95 | context("List") { 96 | withConfig("strings = [ \"one\", \"two\", \"three\" ]") { 97 | getValue>("strings") shouldBe listOf("one", "two", "three") 98 | } 99 | } 100 | context("List") { 101 | withConfig("ints = [ 41, 42, 43 ]") { 102 | getValue>("ints") shouldBe listOf(41, 42, 43) 103 | } 104 | } 105 | context("Duration") { 106 | withConfig("duration = 1 minute") { 107 | getValue("duration") shouldBe Duration.ofMinutes(1) 108 | } 109 | } 110 | context("ConfigObject") { 111 | withConfig( 112 | """ 113 | obj = { 114 | num = 42 115 | str = "hello" 116 | } 117 | """.trimIndent() 118 | ) { 119 | val obj = getValue("obj").toConfig() 120 | obj.getInt("num") shouldBe 42 121 | obj.getString("str") shouldBe "hello" 122 | } 123 | } 124 | context("List") { 125 | withConfig( 126 | """ 127 | objs = [ 128 | { 129 | num = 42 130 | str = "hello" 131 | }, 132 | { 133 | num = 43 134 | str = "goodbye" 135 | } 136 | ] 137 | """.trimIndent() 138 | ) { 139 | val objs = getValue>("objs") 140 | objs shouldHaveSize 2 141 | objs[0].getInt("num") shouldBe 42 142 | objs[0].getString("str") shouldBe "hello" 143 | objs[1].getInt("num") shouldBe 43 144 | objs[1].getString("str") shouldBe "goodbye" 145 | } 146 | } 147 | context("Enum") { 148 | withConfig("color=BLUE") { 149 | getValue("color") shouldBe Color.BLUE 150 | } 151 | } 152 | context("Pattern") { 153 | withConfig("pattern = \"abc\"") { 154 | getValue("pattern").pattern() shouldBe "abc" 155 | } 156 | context("when the pattern is invalid") { 157 | withConfig("pattern = \"(\"") { 158 | shouldThrow { 159 | getValue("pattern").pattern() 160 | } 161 | } 162 | } 163 | } 164 | } 165 | } 166 | } 167 | 168 | private fun withConfig(configStr: String, block: ConfigScope.() -> Unit) { 169 | val config = TypesafeConfigSource("testConfig", ConfigFactory.parseString(configStr)) 170 | ConfigScope(config).apply(block) 171 | } 172 | 173 | private class ConfigScope(private val config: ConfigSource) { 174 | inline fun getValue(path: String): T { 175 | val getter = config.getterFor(typeOf()) 176 | return getter(path) as T 177 | } 178 | } 179 | 180 | private enum class Color { 181 | BLUE 182 | } 183 | -------------------------------------------------------------------------------- /jicoco-health-checker/checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /jicoco-health-checker/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 21 | 4.0.0 22 | 23 | 24 | org.jitsi 25 | jicoco-parent 26 | 1.1-SNAPSHOT 27 | 28 | 29 | jicoco-health-checker 30 | 1.1-SNAPSHOT 31 | jicoco-health-checker 32 | Jitsi Common Components - Health Checker 33 | 34 | 35 | 36 | org.jetbrains.kotlin 37 | kotlin-stdlib-jdk8 38 | 39 | 40 | 41 | ${project.groupId} 42 | jitsi-utils 43 | 44 | 45 | 46 | 47 | 48 | org.jetbrains.kotlin 49 | kotlin-maven-plugin 50 | ${kotlin.version} 51 | 52 | 53 | compile 54 | compile 55 | 56 | compile 57 | 58 | 59 | 60 | src/main/kotlin 61 | 62 | 63 | 64 | 65 | test-compile 66 | test-compile 67 | 68 | test-compile 69 | 70 | 71 | 72 | src/test/kotlin 73 | src/test/java 74 | 75 | 76 | 77 | 78 | 79 | 11 80 | 81 | 82 | 83 | org.apache.maven.plugins 84 | maven-compiler-plugin 85 | 3.10.1 86 | 87 | 88 | default-compile 89 | none 90 | 91 | 92 | default-testCompile 93 | none 94 | 95 | 96 | java-compile 97 | compile 98 | 99 | compile 100 | 101 | 102 | 103 | java-test-compile 104 | test-compile 105 | 106 | testCompile 107 | 108 | 109 | 110 | 111 | 11 112 | 113 | -Xlint:all,-serial 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /jicoco-health-checker/src/main/kotlin/org/jitsi/health/HealthChecker.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.health 18 | 19 | import org.jitsi.utils.concurrent.PeriodicRunnable 20 | import org.jitsi.utils.concurrent.RecurringRunnableExecutor 21 | import org.jitsi.utils.logging2.Logger 22 | import org.jitsi.utils.logging2.LoggerImpl 23 | import org.jitsi.utils.secs 24 | import java.time.Clock 25 | import java.time.Duration 26 | import java.time.Instant 27 | import kotlin.properties.Delegates 28 | 29 | /** 30 | * A [HealthCheckService] implementation which checks health via the provided 31 | * [healthCheckFunc] function. 32 | */ 33 | class HealthChecker( 34 | /** 35 | * The interval at which health checks will be performed. 36 | */ 37 | interval: Duration = 10.secs, 38 | /** 39 | * If no health checks have been performed in the last {@code timeout} 40 | * period, the service is considered unhealthy. 41 | */ 42 | var timeout: Duration = 30.secs, 43 | /** 44 | * The maximum duration that a call to {@link #performCheck()} is allowed 45 | * to take. If a call takes longer, the service is considered unhealthy. 46 | *

47 | * Note that if a check never completes, we rely on {@link #timeout} instead. 48 | */ 49 | var maxCheckDuration: Duration = 3.secs, 50 | /** 51 | * If set, a single health check failure after the initial 52 | * {@link #STICKY_FAILURES_GRACE_PERIOD} will be result in the service 53 | * being permanently unhealthy. 54 | */ 55 | var stickyFailures: Boolean = false, 56 | /** 57 | * Failures in this period (since the start of the service) are not sticky. 58 | */ 59 | var stickyFailuresGracePeriod: Duration = stickyFailuresGracePeriodDefault, 60 | private val healthCheckFunc: () -> Result, 61 | private val clock: Clock = Clock.systemUTC() 62 | ) : HealthCheckService, PeriodicRunnable(interval.toMillis()) { 63 | private val logger: Logger = LoggerImpl(javaClass.name) 64 | 65 | /** 66 | * The executor which runs {@link #performCheck()} periodically. 67 | */ 68 | private var executor: RecurringRunnableExecutor? = null 69 | 70 | /** 71 | * The exception resulting from the last health check. When the health 72 | * check is successful, this is {@code null}. 73 | */ 74 | private var lastResult: Result = Result() 75 | 76 | /** 77 | * The time the last health check finished being performed. 78 | */ 79 | private var lastResultTime = clock.instant() 80 | 81 | /** 82 | * The time when this service was started. 83 | */ 84 | private var serviceStartTime = Instant.MAX 85 | 86 | /** 87 | * Whether we've seen a health check failure (after the grace period). 88 | */ 89 | private var hasFailed = false 90 | 91 | /** 92 | * The interval at which health checks will be performed. 93 | */ 94 | var interval: Duration by Delegates.observable(interval) { _, _, newValue -> 95 | period = newValue.toMillis() 96 | } 97 | 98 | /** 99 | * Returns the result of the last performed health check, or a new exception 100 | * if no health check has been performed recently. 101 | * @return 102 | */ 103 | override val result: Result 104 | get() { 105 | val timeSinceLastResult: Duration = Duration.between(lastResultTime, clock.instant()) 106 | if (timeSinceLastResult > timeout) { 107 | return Result( 108 | success = false, 109 | hardFailure = true, 110 | message = "No health checks performed recently, the last result was $timeSinceLastResult ago." 111 | ) 112 | } 113 | return lastResult 114 | } 115 | 116 | fun start() { 117 | if (executor == null) { 118 | executor = RecurringRunnableExecutor(javaClass.name) 119 | } 120 | executor!!.registerRecurringRunnable(this) 121 | 122 | logger.info( 123 | "Started with interval=$period, timeout=$timeout, " + 124 | "maxDuration=$maxCheckDuration, stickyFailures=$stickyFailures." 125 | ) 126 | } 127 | 128 | @Throws(Exception::class) 129 | fun stop() { 130 | executor?.apply { 131 | deRegisterRecurringRunnable(this@HealthChecker) 132 | close() 133 | } 134 | executor = null 135 | logger.info("Stopped") 136 | } 137 | 138 | /** 139 | * Performs a health check and updates this instance's state. Runs 140 | * periodically in {@link #executor}. 141 | */ 142 | override fun run() { 143 | super.run() 144 | 145 | val checkStart = clock.instant() 146 | var newResult: Result = try { 147 | healthCheckFunc() 148 | } catch (e: Exception) { 149 | val now = clock.instant() 150 | val timeSinceStart = Duration.between(serviceStartTime, now) 151 | if (timeSinceStart > stickyFailuresGracePeriod) { 152 | hasFailed = true 153 | } 154 | Result( 155 | success = false, 156 | hardFailure = true, 157 | message = "Failed to run health check: ${e.message}" 158 | ) 159 | } 160 | 161 | lastResultTime = clock.instant() 162 | val checkDuration = Duration.between(checkStart, lastResultTime) 163 | if (checkDuration > maxCheckDuration) { 164 | newResult = Result(success = false, message = "Performing a health check took too long: $checkDuration") 165 | } 166 | 167 | val previousResult = lastResult 168 | lastResult = if (stickyFailures && hasFailed && newResult.success) { 169 | // We didn't fail this last test, but we've failed before and 170 | // sticky failures are enabled. 171 | Result(success = false, sticky = true, message = "Sticky failure.") 172 | } else { 173 | newResult 174 | } 175 | 176 | if (newResult.success) { 177 | val message = 178 | "Performed a successful health check in $checkDuration. Sticky failure: ${stickyFailures && hasFailed}" 179 | if (previousResult.success) logger.debug(message) else logger.info(message) 180 | } else { 181 | logger.error("Health check failed in $checkDuration: $newResult") 182 | } 183 | } 184 | 185 | companion object { 186 | val stickyFailuresGracePeriodDefault: Duration = Duration.ofMinutes(5) 187 | } 188 | } 189 | 190 | data class Result( 191 | /** Whether the health check was successful or now. */ 192 | val success: Boolean = true, 193 | /** If the fail check failed (success=false) whether it was a hard or soft failure. */ 194 | val hardFailure: Boolean = true, 195 | /** 196 | * Optional HTTP response code to use in HTTP responses. When set to `null` it is chosen automatically based on 197 | * [#success] and [#hardFailure] 198 | */ 199 | val responseCode: Int? = null, 200 | /** Whether the failure is reported only because an earlier failure occurred and the "sticky" mode is enabled. */ 201 | val sticky: Boolean = false, 202 | /** Optional error message. */ 203 | val message: String? = null 204 | ) 205 | 206 | interface HealthCheckService { 207 | /** The result of a health check. */ 208 | val result: Result 209 | } 210 | -------------------------------------------------------------------------------- /jicoco-jetty/checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /jicoco-jetty/src/main/java/org/jitsi/meet/ShutdownService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2015 - present, 8x8 Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.jitsi.meet; 17 | 18 | /** 19 | * Abstracts the shutdown-related procedures of the application. 20 | * 21 | * @author Linus Wallgren 22 | */ 23 | public interface ShutdownService 24 | { 25 | void beginShutdown(); 26 | } 27 | 28 | -------------------------------------------------------------------------------- /jicoco-jetty/src/main/java/org/jitsi/rest/Health.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.rest; 18 | 19 | import jakarta.ws.rs.*; 20 | import jakarta.ws.rs.core.*; 21 | import org.jetbrains.annotations.*; 22 | import org.jitsi.health.*; 23 | 24 | /** 25 | * A generic health check REST endpoint which checks the health using a 26 | * a {@link HealthCheckService}, if one is present. 27 | * 28 | */ 29 | @Path("/about/health") 30 | public class Health 31 | { 32 | @NotNull 33 | private final HealthCheckService healthCheckService; 34 | 35 | public Health(@NotNull HealthCheckService healthCheckService) 36 | { 37 | this.healthCheckService = healthCheckService; 38 | } 39 | 40 | @GET 41 | @Produces(MediaType.APPLICATION_JSON) 42 | public Response getHealth() 43 | { 44 | Result result = healthCheckService.getResult(); 45 | if (!result.getSuccess()) 46 | { 47 | int status 48 | = result.getResponseCode() != null ? result.getResponseCode() : result.getHardFailure() ? 500 : 503; 49 | Response.ResponseBuilder response = Response.status(status); 50 | if (result.getMessage() != null) 51 | { 52 | response.entity(result.getMessage()); 53 | } 54 | 55 | return response.build(); 56 | } 57 | 58 | return Response.ok().build(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /jicoco-jetty/src/main/java/org/jitsi/rest/Version.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.rest; 18 | 19 | import com.fasterxml.jackson.annotation.*; 20 | import jakarta.ws.rs.*; 21 | import jakarta.ws.rs.core.*; 22 | import org.jetbrains.annotations.*; 23 | 24 | /** 25 | * A generic version REST endpoint. 26 | * 27 | */ 28 | @Path("/about/version") 29 | public class Version 30 | { 31 | @NotNull 32 | private final VersionInfo versionInfo; 33 | 34 | public Version(@NotNull org.jitsi.utils.version.Version version) 35 | { 36 | versionInfo = new VersionInfo(version.getApplicationName(), version.toString(), System.getProperty("os.name")); 37 | } 38 | 39 | @GET 40 | @Produces(MediaType.APPLICATION_JSON) 41 | public VersionInfo getVersion() 42 | { 43 | return versionInfo; 44 | } 45 | 46 | static class VersionInfo { 47 | @JsonProperty String name; 48 | @JsonProperty String version; 49 | @JsonProperty String os; 50 | 51 | public VersionInfo() {} 52 | public VersionInfo(String name, String version, String os) 53 | { 54 | this.name = name; 55 | this.version = version; 56 | this.os = os; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /jicoco-jetty/src/main/kotlin/org/jitsi/rest/JettyBundleActivatorConfig.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.rest 18 | 19 | import org.jitsi.config.JitsiConfig 20 | import org.jitsi.metaconfig.config 21 | import org.jitsi.metaconfig.optionalconfig 22 | 23 | /** 24 | * Configuration properties used by [AbstractJettyBundleActivator] 25 | */ 26 | class JettyBundleActivatorConfig( 27 | private val legacyPropertyPrefix: String, 28 | private val newPropertyPrefix: String 29 | ) { 30 | /** 31 | * The port on which the Jetty server is to listen for HTTP requests 32 | */ 33 | val port: Int by config { 34 | "$legacyPropertyPrefix.jetty.port".from(JitsiConfig.legacyConfig) 35 | "$newPropertyPrefix.port".from(JitsiConfig.newConfig) 36 | "default" { 8080 } 37 | } 38 | 39 | /** 40 | * The address on which the Jetty server will listen 41 | */ 42 | val host: String? by optionalconfig { 43 | "$legacyPropertyPrefix.jetty.host".from(JitsiConfig.legacyConfig) 44 | "$newPropertyPrefix.host".from(JitsiConfig.newConfig) 45 | } 46 | 47 | /** 48 | * The [java.security.KeyStore] path to be utilized by [org.eclipse.jetty.util.ssl.SslContextFactory] 49 | * when Jetty serves over HTTPS. 50 | */ 51 | val keyStorePath: String? by optionalconfig { 52 | "$legacyPropertyPrefix.jetty.sslContextFactory.keyStorePath".from(JitsiConfig.legacyConfig) 53 | "$newPropertyPrefix.key-store-path".from(JitsiConfig.newConfig) 54 | } 55 | 56 | /** 57 | * Whether or not this server should use TLS 58 | */ 59 | val isTls: Boolean 60 | get() = keyStorePath != null 61 | 62 | /** 63 | * The [java.security.KeyStore] password to be used by [org.eclipse.jetty.util.ssl.SslContextFactory] 64 | * when Jetty serves over HTTPS 65 | */ 66 | val keyStorePassword: String? by optionalconfig { 67 | "$legacyPropertyPrefix.jetty.sslContextFactory.keyStorePassword".from(JitsiConfig.legacyConfig) 68 | "$newPropertyPrefix.key-store-password".from(JitsiConfig.newConfig) 69 | } 70 | 71 | /** 72 | * Whether or not client certificate authentication is to be required by 73 | * [org.eclipse.jetty.util.ssl.SslContextFactory] when Jetty serves over HTTPS 74 | */ 75 | val needClientAuth: Boolean by config { 76 | "$legacyPropertyPrefix.jetty.sslContextFactory.needClientAuth".from(JitsiConfig.legacyConfig) 77 | "$newPropertyPrefix.need-client-auth".from(JitsiConfig.newConfig) 78 | "default" { false } 79 | } 80 | 81 | /** 82 | * The port on which the Jetty server is to listen for HTTPS requests 83 | */ 84 | val tlsPort: Int by config { 85 | "$legacyPropertyPrefix.jetty.tls.port".from(JitsiConfig.legacyConfig) 86 | "$newPropertyPrefix.tls-port".from(JitsiConfig.newConfig) 87 | "default" { 8443 } 88 | } 89 | 90 | /** 91 | * Whether Jetty server version should be sent in HTTP responses 92 | */ 93 | val sendServerVersion: Boolean by config { 94 | "$newPropertyPrefix.send-server-version".from(JitsiConfig.newConfig) 95 | "default" { false } 96 | } 97 | 98 | val tlsProtocols: List by config { 99 | "$newPropertyPrefix.tls-protocols".from(JitsiConfig.newConfig) 100 | "default" { DEFAULT_TLS_PROTOCOLS } 101 | } 102 | 103 | val tlsCipherSuites: List by config { 104 | "$newPropertyPrefix.tls-cipher-suites".from(JitsiConfig.newConfig) 105 | "default" { DEFAULT_TLS_CIPHER_SUITES } 106 | } 107 | 108 | override fun toString() = "host=$host, port=$port, tlsPort=$tlsPort, isTls=$isTls, keyStorePath=$keyStorePath, " + 109 | "sendServerVersion=$sendServerVersion, $tlsProtocols=$tlsProtocols, tlsCipherSuites=$tlsCipherSuites" 110 | 111 | companion object { 112 | val TLS_1_2 = "TLSv1.2" 113 | val TLS_1_3 = "TLSv1.3" 114 | val DEFAULT_TLS_PROTOCOLS = listOf(TLS_1_2, TLS_1_3) 115 | val DEFAULT_TLS_CIPHER_SUITES = listOf( 116 | "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", 117 | "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", 118 | "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", 119 | "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", 120 | "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", 121 | "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", 122 | "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", 123 | "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256" 124 | ) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /jicoco-jetty/src/main/kotlin/org/jitsi/rest/JettyHelpers.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("JettyHelpers") 2 | /* 3 | * Copyright @ 2018 - present 8x8, Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package org.jitsi.rest 19 | 20 | import jakarta.servlet.DispatcherType 21 | import org.eclipse.jetty.http.HttpStatus 22 | import org.eclipse.jetty.server.HttpConfiguration 23 | import org.eclipse.jetty.server.HttpConnectionFactory 24 | import org.eclipse.jetty.server.SecureRequestCustomizer 25 | import org.eclipse.jetty.server.Server 26 | import org.eclipse.jetty.server.ServerConnector 27 | import org.eclipse.jetty.server.SslConnectionFactory 28 | import org.eclipse.jetty.servlet.ServletContextHandler 29 | import org.eclipse.jetty.servlet.ServletHolder 30 | import org.eclipse.jetty.servlets.CrossOriginFilter 31 | import org.eclipse.jetty.util.ssl.SslContextFactory 32 | import java.nio.file.Paths 33 | import java.util.EnumSet 34 | 35 | /** 36 | * Create a non-secure Jetty server instance listening on the given [port] and [host] address. 37 | * [sendServerVersion] controls whether Jetty should send its server version in the error responses or not. 38 | */ 39 | fun createJettyServer(config: JettyBundleActivatorConfig): Server { 40 | val httpConfig = HttpConfiguration().apply { 41 | sendServerVersion = config.sendServerVersion 42 | addCustomizer { _, _, request -> 43 | if (request.method.equals("TRACE", ignoreCase = true)) { 44 | request.isHandled = true 45 | request.response.status = HttpStatus.METHOD_NOT_ALLOWED_405 46 | } 47 | } 48 | } 49 | val server = Server().apply { 50 | handler = ServletContextHandler() 51 | } 52 | val connector = ServerConnector(server, HttpConnectionFactory(httpConfig)).apply { 53 | port = config.port 54 | host = config.host 55 | } 56 | server.addConnector(connector) 57 | return server 58 | } 59 | 60 | /** 61 | * Create a secure Jetty server instance listening on the given [port] and [host] address and using the 62 | * KeyStore located at [keyStorePath], optionally protected by [keyStorePassword]. [needClientAuth] sets whether 63 | * client auth is needed for SSL (see [SslContextFactory.setNeedClientAuth]). 64 | * [sendServerVersion] controls whether Jetty should send its server version in the error responses or not. 65 | */ 66 | fun createSecureJettyServer(config: JettyBundleActivatorConfig): Server { 67 | val sslContextFactoryKeyStoreFile = Paths.get(config.keyStorePath!!).toFile() 68 | val sslContextFactory = SslContextFactory.Server().apply { 69 | setIncludeProtocols(*config.tlsProtocols.toTypedArray()) 70 | setIncludeCipherSuites(*config.tlsCipherSuites.toTypedArray()) 71 | 72 | isRenegotiationAllowed = false 73 | if (config.keyStorePassword != null) { 74 | keyStorePassword = config.keyStorePassword 75 | } 76 | keyStorePath = sslContextFactoryKeyStoreFile.path 77 | needClientAuth = config.needClientAuth 78 | } 79 | val httpConfig = HttpConfiguration().apply { 80 | securePort = config.tlsPort 81 | secureScheme = "https" 82 | addCustomizer(SecureRequestCustomizer()) 83 | sendServerVersion = config.sendServerVersion 84 | } 85 | val server = Server().apply { 86 | handler = ServletContextHandler() 87 | } 88 | 89 | val connector = ServerConnector( 90 | server, 91 | SslConnectionFactory( 92 | sslContextFactory, 93 | "http/1.1" 94 | ), 95 | HttpConnectionFactory(httpConfig) 96 | ).apply { 97 | host = config.host 98 | port = config.tlsPort 99 | } 100 | server.addConnector(connector) 101 | return server 102 | } 103 | 104 | /** 105 | * Create a Jetty [Server] instance based on the given [config]. 106 | */ 107 | fun createServer(config: JettyBundleActivatorConfig): Server { 108 | return if (config.isTls) { 109 | createSecureJettyServer(config) 110 | } else { 111 | createJettyServer(config) 112 | } 113 | } 114 | 115 | fun JettyBundleActivatorConfig.isEnabled(): Boolean = port != -1 || tlsPort != -1 116 | 117 | // Note: it's technically possible that this cast fails, but 118 | // shouldn't happen in practice given that the above methods always install 119 | // a ServletContextHandler handler. 120 | val Server.servletContextHandler: ServletContextHandler 121 | get() = handler as ServletContextHandler 122 | 123 | fun ServletContextHandler.enableCors(pathSpec: String = "/*") { 124 | addFilter(CrossOriginFilter::class.java, pathSpec, EnumSet.of(DispatcherType.REQUEST)).apply { 125 | setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*") 126 | setInitParameter(CrossOriginFilter.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, "*") 127 | setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "GET,POST") 128 | setInitParameter(CrossOriginFilter.ALLOWED_HEADERS_PARAM, "X-Requested-With,Content-Type,Accept,Origin") 129 | } 130 | } 131 | 132 | fun Server.addServlet(servlet: ServletHolder, pathSpec: String) = 133 | this.servletContextHandler.addServlet(servlet, pathSpec) 134 | -------------------------------------------------------------------------------- /jicoco-jetty/src/main/kotlin/org/jitsi/shutdown/ShutdownServiceImpl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.shutdown 18 | 19 | import org.jitsi.meet.ShutdownService 20 | import java.util.concurrent.CountDownLatch 21 | import java.util.concurrent.atomic.AtomicBoolean 22 | 23 | class ShutdownServiceImpl : ShutdownService { 24 | private val shutdownStarted = AtomicBoolean(false) 25 | 26 | private val shutdownSync = CountDownLatch(1) 27 | 28 | override fun beginShutdown() { 29 | if (shutdownStarted.compareAndSet(false, true)) { 30 | shutdownSync.countDown() 31 | } 32 | } 33 | 34 | fun waitForShutdown() { 35 | shutdownSync.await() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /jicoco-jetty/src/test/java/org/jitsi/rest/HealthTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.rest; 18 | 19 | import jakarta.ws.rs.core.*; 20 | import org.eclipse.jetty.http.*; 21 | import org.glassfish.jersey.server.*; 22 | import org.glassfish.jersey.test.*; 23 | import org.jitsi.health.*; 24 | 25 | import org.junit.jupiter.api.*; 26 | 27 | import static org.junit.jupiter.api.Assertions.*; 28 | import static org.mockito.Mockito.*; 29 | 30 | public class HealthTest extends JerseyTest 31 | { 32 | protected HealthCheckService healthCheckService; 33 | protected static final String BASE_URL = "/about/health"; 34 | 35 | @Override 36 | protected Application configure() 37 | { 38 | healthCheckService = mock(HealthCheckService.class); 39 | 40 | enable(TestProperties.LOG_TRAFFIC); 41 | enable(TestProperties.DUMP_ENTITY); 42 | return new ResourceConfig() { 43 | { 44 | register(new Health(healthCheckService)); 45 | } 46 | }; 47 | } 48 | 49 | @Test 50 | public void testSuccessfulHealthCheck() 51 | { 52 | when(healthCheckService.getResult()).thenReturn(new Result()); 53 | 54 | Response resp = target(BASE_URL).request().get(); 55 | assertEquals(HttpStatus.OK_200, resp.getStatus()); 56 | } 57 | 58 | @Test 59 | public void testFailingHealthCheck() 60 | { 61 | when(healthCheckService.getResult()).thenReturn(new Result(false, true, null, false, null)); 62 | Response resp = target(BASE_URL).request().get(); 63 | assertEquals(HttpStatus.INTERNAL_SERVER_ERROR_500, resp.getStatus()); 64 | } 65 | 66 | @Test 67 | public void testFailingHealthCheckWithCustomResponseCode() 68 | { 69 | when(healthCheckService.getResult()).thenReturn(new Result(false, false, 502, false, null)); 70 | Response resp = target(BASE_URL).request().get(); 71 | assertEquals(HttpStatus.BAD_GATEWAY_502, resp.getStatus()); 72 | } 73 | 74 | @Test 75 | public void testExceptionDuringHealthCheck() 76 | { 77 | when(healthCheckService.getResult()).thenThrow(new RuntimeException("Health check failed")); 78 | Response resp = target(BASE_URL).request().get(); 79 | assertEquals(HttpStatus.INTERNAL_SERVER_ERROR_500, resp.getStatus()); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /jicoco-jetty/src/test/java/org/jitsi/rest/VersionTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.rest; 18 | 19 | import static org.junit.jupiter.api.Assertions.*; 20 | 21 | import jakarta.ws.rs.core.*; 22 | import org.eclipse.jetty.http.*; 23 | import org.glassfish.jersey.server.*; 24 | import org.glassfish.jersey.test.*; 25 | import org.jitsi.utils.version.*; 26 | import org.junit.jupiter.api.*; 27 | 28 | public class VersionTest 29 | extends JerseyTest 30 | { 31 | protected static final String BASE_URL = "/about/version"; 32 | 33 | @Override 34 | protected Application configure() 35 | { 36 | enable(TestProperties.LOG_TRAFFIC); 37 | enable(TestProperties.DUMP_ENTITY); 38 | return new ResourceConfig() 39 | { 40 | { 41 | register(new Version(new VersionImpl("appName", 2, 0))); 42 | } 43 | }; 44 | } 45 | 46 | @Test 47 | public void testVersion() 48 | { 49 | Response resp = target(BASE_URL).request().get(); 50 | assertEquals(HttpStatus.OK_200, resp.getStatus()); 51 | Version.VersionInfo versionInfo = 52 | resp.readEntity(Version.VersionInfo.class); 53 | assertEquals("appName", versionInfo.name); 54 | assertEquals("2.0", versionInfo.version); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /jicoco-jetty/src/test/kotlin/org/jitsi/shutdown/ShutdownServiceImplTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.shutdown 18 | 19 | import io.kotest.core.spec.IsolationMode 20 | import io.kotest.core.spec.style.ShouldSpec 21 | import io.kotest.matchers.shouldBe 22 | import kotlinx.coroutines.async 23 | import kotlin.time.Duration.Companion.milliseconds 24 | import kotlin.time.Duration.Companion.seconds 25 | import kotlin.time.ExperimentalTime 26 | 27 | @ExperimentalTime 28 | class ShutdownServiceImplTest : ShouldSpec({ 29 | isolationMode = IsolationMode.InstancePerLeaf 30 | 31 | val shutdownService = ShutdownServiceImpl() 32 | 33 | context("beginning shutdown") { 34 | should("notify waiters").config(timeout = 5.seconds) { 35 | val result = async { 36 | shutdownService.waitForShutdown() 37 | true 38 | } 39 | shutdownService.beginShutdown() 40 | result.await() shouldBe true 41 | } 42 | } 43 | context("waiting after shutdown is done") { 44 | shutdownService.beginShutdown() 45 | should("return 'immediately'").config(timeout = 500.milliseconds) { 46 | shutdownService.waitForShutdown() 47 | } 48 | } 49 | }) 50 | -------------------------------------------------------------------------------- /jicoco-jwt/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 4.0.0 19 | 20 | org.jitsi 21 | jicoco-parent 22 | 1.1-SNAPSHOT 23 | 24 | jicoco-jwt 25 | 1.1-SNAPSHOT 26 | jicoco-jwt 27 | Jitsi Common Components: JWT 28 | 29 | 30 | org.jitsi 31 | jitsi-utils 32 | 33 | 34 | org.jitsi 35 | jicoco-config 36 | ${project.version} 37 | 38 | 39 | org.bouncycastle 40 | bcpkix-jdk18on 41 | ${bouncycastle.version} 42 | 43 | 44 | io.jsonwebtoken 45 | jjwt-api 46 | ${jwt.version} 47 | 48 | 49 | io.jsonwebtoken 50 | jjwt-impl 51 | ${jwt.version} 52 | runtime 53 | 54 | 55 | io.jsonwebtoken 56 | jjwt-jackson 57 | ${jwt.version} 58 | runtime 59 | 60 | 61 | com.fasterxml.jackson.module 62 | jackson-module-kotlin 63 | ${jackson.version} 64 | 65 | 66 | 67 | 68 | io.kotest 69 | kotest-runner-junit5-jvm 70 | ${kotest.version} 71 | test 72 | 73 | 74 | io.kotest 75 | kotest-assertions-core-jvm 76 | ${kotest.version} 77 | test 78 | 79 | 80 | 81 | 82 | 83 | org.jetbrains.kotlin 84 | kotlin-maven-plugin 85 | ${kotlin.version} 86 | 87 | 88 | compile 89 | compile 90 | 91 | compile 92 | 93 | 94 | 95 | -opt-in=kotlin.ExperimentalStdlibApi 96 | 97 | 98 | ${project.basedir}/src/main/kotlin 99 | 100 | 101 | 102 | 103 | test-compile 104 | test-compile 105 | 106 | test-compile 107 | 108 | 109 | 110 | -opt-in=kotlin.ExperimentalStdlibApi 111 | 112 | 113 | ${project.basedir}/src/test/kotlin 114 | 115 | 116 | 117 | 118 | 119 | 11 120 | 121 | 122 | 123 | org.apache.maven.plugins 124 | maven-compiler-plugin 125 | 3.10.1 126 | 127 | 128 | 129 | default-compile 130 | none 131 | 132 | 133 | 134 | default-testCompile 135 | none 136 | 137 | 138 | java-compile 139 | compile 140 | 141 | compile 142 | 143 | 144 | 145 | java-test-compile 146 | test-compile 147 | 148 | testCompile 149 | 150 | 151 | 152 | 153 | 11 154 | 155 | -Xlint:all 156 | 157 | 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /jicoco-jwt/src/main/kotlin/org/jitsi/jwt/JitsiToken.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2025 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.jitsi.jwt 17 | 18 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties 19 | import com.fasterxml.jackson.annotation.JsonProperty 20 | import com.fasterxml.jackson.core.JsonParser 21 | import com.fasterxml.jackson.core.JsonProcessingException 22 | import com.fasterxml.jackson.databind.JsonMappingException 23 | import com.fasterxml.jackson.databind.MapperFeature 24 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 25 | import com.fasterxml.jackson.module.kotlin.readValue 26 | import java.util.* 27 | 28 | @JsonIgnoreProperties(ignoreUnknown = true) 29 | data class JitsiToken( 30 | val iss: String? = null, 31 | val aud: String? = null, 32 | val iat: Long? = null, 33 | val nbf: Long? = null, 34 | val exp: Long? = null, 35 | val name: String? = null, 36 | val picture: String? = null, 37 | @JsonProperty("user_id") 38 | val userId: String? = null, 39 | val email: String? = null, 40 | @JsonProperty("email_verified") 41 | val emailVerified: Boolean? = null, 42 | val room: String? = null, 43 | val sub: String? = null, 44 | val context: Context? = null 45 | ) { 46 | @JsonIgnoreProperties(ignoreUnknown = true) 47 | data class Context( 48 | val user: User?, 49 | val group: String? = null, 50 | val tenant: String? = null, 51 | val features: Features? = null 52 | ) 53 | 54 | @JsonIgnoreProperties(ignoreUnknown = true) 55 | data class User( 56 | val id: String? = null, 57 | val name: String? = null, 58 | val avatar: String? = null, 59 | val email: String? = null 60 | ) 61 | 62 | @JsonIgnoreProperties(ignoreUnknown = true) 63 | data class Features( 64 | val flip: Boolean? = null, 65 | val livestreaming: Boolean? = null, 66 | @JsonProperty("outbound-call") 67 | val outboundCall: Boolean? = null, 68 | val recording: Boolean? = null, 69 | @JsonProperty("sip-outbound-call") 70 | val sipOutboundCall: Boolean? = null, 71 | val transcription: Boolean? = null 72 | ) 73 | 74 | companion object { 75 | private val mapper = jacksonObjectMapper().apply { 76 | enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) 77 | enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION) 78 | } 79 | private val base64decoder = Base64.getUrlDecoder() 80 | 81 | /** 82 | * Parse a JWT into the JitsiToken structure without validating the signature. 83 | */ 84 | @Throws(JsonProcessingException::class, JsonMappingException::class, IllegalArgumentException::class) 85 | fun parseWithoutValidation(string: String): JitsiToken = string.split(".").let { 86 | if (it.size >= 2) { 87 | parseJson(base64decoder.decode(it[1]).toString(Charsets.UTF_8)) 88 | } else { 89 | throw IllegalArgumentException("Invalid JWT format") 90 | } 91 | } 92 | 93 | /** 94 | * Parse a JSON string into the JitsiToken structure. 95 | */ 96 | @Throws(JsonProcessingException::class, JsonMappingException::class) 97 | fun parseJson(string: String): JitsiToken { 98 | return mapper.readValue(string) 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /jicoco-jwt/src/main/kotlin/org/jitsi/jwt/JwtInfo.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.jitsi.jwt 17 | 18 | import com.typesafe.config.ConfigObject 19 | import org.bouncycastle.openssl.PEMKeyPair 20 | import org.bouncycastle.openssl.PEMParser 21 | import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter 22 | import org.jitsi.utils.logging2.createLogger 23 | import java.io.FileReader 24 | import java.security.PrivateKey 25 | import java.time.Duration 26 | 27 | data class JwtInfo( 28 | val privateKey: PrivateKey, 29 | val kid: String, 30 | val issuer: String, 31 | val audience: String, 32 | val ttl: Duration 33 | ) { 34 | companion object { 35 | private val logger = createLogger() 36 | fun fromConfig(jwtConfigObj: ConfigObject): JwtInfo { 37 | // Any missing or incorrect value here will throw, which is what we want: 38 | // If anything is wrong, we should fail to create the JwtInfo 39 | val jwtConfig = jwtConfigObj.toConfig() 40 | logger.info("got jwtConfig: ${jwtConfig.root().render()}") 41 | try { 42 | return JwtInfo( 43 | privateKey = parseKeyFile(jwtConfig.getString("signing-key-path")), 44 | kid = jwtConfig.getString("kid"), 45 | issuer = jwtConfig.getString("issuer"), 46 | audience = jwtConfig.getString("audience"), 47 | ttl = jwtConfig.getDuration("ttl").withMinimum(Duration.ofMinutes(10)) 48 | ) 49 | } catch (t: Throwable) { 50 | logger.info("Unable to create JwtInfo: $t") 51 | throw t 52 | } 53 | } 54 | } 55 | } 56 | 57 | private fun parseKeyFile(keyFilePath: String): PrivateKey { 58 | val parser = PEMParser(FileReader(keyFilePath)) 59 | return (parser.readObject() as PEMKeyPair).let { pemKeyPair -> 60 | JcaPEMKeyConverter().getKeyPair(pemKeyPair).private 61 | } 62 | } 63 | 64 | /** 65 | * Returns [min] if this Duration is less than that minimum, otherwise this 66 | */ 67 | private fun Duration.withMinimum(min: Duration): Duration = maxOf(this, min) 68 | -------------------------------------------------------------------------------- /jicoco-jwt/src/main/kotlin/org/jitsi/jwt/RefreshingJwt.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.jitsi.jwt 17 | 18 | import io.jsonwebtoken.Jwts 19 | import java.time.Clock 20 | import java.time.Duration 21 | import java.util.* 22 | 23 | class RefreshingJwt( 24 | private val jwtInfo: JwtInfo?, 25 | private val clock: Clock = Clock.systemUTC() 26 | ) : RefreshingProperty( 27 | // We refresh 5 minutes before the expiration 28 | jwtInfo?.ttl?.minus(Duration.ofMinutes(5)) ?: Duration.ofSeconds(Long.MAX_VALUE), 29 | clock, 30 | { 31 | jwtInfo?.let { 32 | Jwts.builder().apply { 33 | header().add("kid", it.kid) 34 | issuer(it.issuer) 35 | audience().add(it.audience) 36 | expiration(Date.from(clock.instant().plus(it.ttl))) 37 | signWith(it.privateKey, Jwts.SIG.RS256) 38 | }.compact() 39 | } 40 | } 41 | ) 42 | -------------------------------------------------------------------------------- /jicoco-jwt/src/main/kotlin/org/jitsi/jwt/RefreshingProperty.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.jwt 18 | 19 | import org.jitsi.utils.logging2.createLogger 20 | import java.time.Clock 21 | import java.time.Duration 22 | import java.time.Instant 23 | import kotlin.reflect.KProperty 24 | 25 | /** 26 | * A property delegate which recreates a value when it's accessed after having been 27 | * 'alive' for more than [timeout] via the given [creationFunc] 28 | */ 29 | open class RefreshingProperty( 30 | private val timeout: Duration, 31 | private val clock: Clock = Clock.systemUTC(), 32 | private val creationFunc: () -> T? 33 | ) { 34 | private var value: T? = null 35 | private var valueCreationTimestamp: Instant? = null 36 | 37 | private val logger = createLogger() 38 | 39 | @Synchronized 40 | operator fun getValue(thisRef: Any?, property: KProperty<*>): T? { 41 | val now = clock.instant() 42 | if (valueExpired(now)) { 43 | value = try { 44 | logger.debug("Refreshing property ${property.name} (not yet initialized or expired)...") 45 | creationFunc() 46 | } catch (exception: Exception) { 47 | logger.warn( 48 | "Property refresh caused exception, will use null for property ${property.name}: ", 49 | exception 50 | ) 51 | null 52 | } 53 | valueCreationTimestamp = now 54 | } 55 | return value 56 | } 57 | 58 | private fun valueExpired(now: Instant): Boolean { 59 | return value == null || Duration.between(valueCreationTimestamp, now) >= timeout 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /jicoco-jwt/src/test/kotlin/org/jitsi/jwt/JitsiTokenTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2025 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.jitsi.jwt 17 | 18 | import com.fasterxml.jackson.core.JsonParseException 19 | import com.fasterxml.jackson.core.JsonProcessingException 20 | import io.kotest.assertions.throwables.shouldThrow 21 | import io.kotest.core.spec.style.ShouldSpec 22 | import io.kotest.matchers.shouldBe 23 | import io.kotest.matchers.shouldNotBe 24 | import java.util.* 25 | 26 | class JitsiTokenTest : ShouldSpec({ 27 | context("parsing a meet-jit-si firebase token") { 28 | JitsiToken.parseJson(meetJitsiJson).apply { 29 | this shouldNotBe null 30 | name shouldBe "Boris Grozev" 31 | picture shouldBe "https://example.com/avatar.png" 32 | iss shouldBe "issuer" 33 | aud shouldBe "audience" 34 | userId shouldBe "user_id" 35 | sub shouldBe "sub" 36 | iat shouldBe 1748367729 37 | exp shouldBe 1748371329 38 | email shouldBe "boris@example.com" 39 | emailVerified shouldBe true 40 | context shouldBe null 41 | } 42 | } 43 | context("parsing an 8x8.vc token") { 44 | JitsiToken.parseJson(prodJson).apply { 45 | this shouldNotBe null 46 | aud shouldBe "jitsi" 47 | iss shouldBe "chat" 48 | exp shouldBe 1748454424 49 | nbf shouldBe 1748368024 50 | room shouldBe "" 51 | sub shouldBe "" 52 | 53 | context shouldNotBe null 54 | context?.group shouldBe "54321" 55 | context?.tenant shouldBe "tenant" 56 | 57 | context?.user shouldNotBe null 58 | context?.user?.id shouldBe "12345" 59 | context?.user?.name shouldBe "Boris Grozev" 60 | context?.user?.avatar shouldBe "https://example.com/avatar.png" 61 | context?.user?.email shouldBe "Boris@example.com" 62 | 63 | context?.features shouldNotBe null 64 | context?.features?.flip shouldBe true 65 | context?.features?.livestreaming shouldBe true 66 | context?.features?.outboundCall shouldBe false 67 | context?.features?.recording shouldBe false 68 | context?.features?.sipOutboundCall shouldBe true 69 | context?.features?.transcription shouldBe null 70 | } 71 | } 72 | context("parsing an invalid token") { 73 | shouldThrow { 74 | JitsiToken.parseJson("{ invalid ") 75 | } 76 | } 77 | context("parsing a JWT") { 78 | listOf(meetJitsiJson, prodJson).forEach { json -> 79 | val jwtNoSig = "${header.base64Encode()}.${json.base64Encode()}" 80 | JitsiToken.parseWithoutValidation(jwtNoSig) shouldBe JitsiToken.parseJson(json) 81 | 82 | val jwt = "$jwtNoSig.signature" 83 | JitsiToken.parseWithoutValidation(jwt) shouldBe JitsiToken.parseJson(json) 84 | } 85 | } 86 | context("parsing an invalid JWT") { 87 | shouldThrow { 88 | JitsiToken.parseWithoutValidation("invalid") 89 | } 90 | shouldThrow { 91 | JitsiToken.parseWithoutValidation("invalid.%") 92 | } 93 | shouldThrow { 94 | JitsiToken.parseWithoutValidation("invalid.jwt") 95 | } 96 | } 97 | }) 98 | 99 | private val meetJitsiJson = """ 100 | { 101 | "name": "Boris Grozev", 102 | "picture": "https://example.com/avatar.png", 103 | "iss": "issuer", 104 | "aud": "audience", 105 | "auth_time": 1731944223, 106 | "user_id": "user_id", 107 | "sub": "sub", 108 | "iat": 1748367729, 109 | "exp": 1748371329, 110 | "email": "boris@example.com", 111 | "email_verified": true, 112 | "firebase": { 113 | "identities": { 114 | "google.com": [ 115 | "1234" 116 | ], 117 | "email": [ 118 | "boris@example.com" 119 | ] 120 | }, 121 | "sign_in_provider": "google.com" 122 | } 123 | } 124 | """.trimIndent() 125 | 126 | private val prodJson = """ 127 | { 128 | "aud": "jitsi", 129 | "context": { 130 | "user": { 131 | "id": "12345", 132 | "name": "Boris Grozev", 133 | "avatar": "https://example.com/avatar.png", 134 | "email": "Boris@example.com" 135 | }, 136 | "group": "54321", 137 | "tenant": "tenant", 138 | "features": { 139 | "flip": "true", 140 | "livestreaming": true, 141 | "outbound-call": "false", 142 | "recording": false, 143 | "sip-outbound-call": "true" 144 | } 145 | }, 146 | "exp": 1748454424, 147 | "iss": "chat", 148 | "nbf": 1748368024, 149 | "room": "", 150 | "sub": "" 151 | } 152 | """.trimIndent() 153 | 154 | private val header = """{"alg":"RS256","kid":"kid","typ":"JWT"""".trimIndent() 155 | 156 | private fun String.base64Encode(): String { 157 | return Base64.getUrlEncoder().withoutPadding().encodeToString(this.toByteArray()) 158 | } 159 | -------------------------------------------------------------------------------- /jicoco-jwt/src/test/kotlin/org/jitsi/jwt/RefreshingPropertyTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.jwt 18 | 19 | import io.kotest.assertions.throwables.shouldThrow 20 | import io.kotest.core.spec.style.ShouldSpec 21 | import io.kotest.matchers.shouldBe 22 | import io.kotest.matchers.string.shouldContain 23 | import org.jitsi.utils.time.FakeClock 24 | import java.time.Duration 25 | 26 | class RefreshingPropertyTest : ShouldSpec({ 27 | val clock = FakeClock() 28 | 29 | context("A refreshing property") { 30 | val obj = object { 31 | private var generation = 0 32 | val prop: Int? by RefreshingProperty(Duration.ofSeconds(1), clock) { 33 | println("Refreshing, generation was $generation") 34 | generation++ 35 | } 36 | } 37 | should("return the right initial value") { 38 | obj.prop shouldBe 0 39 | } 40 | context("after the timeout has elapsed") { 41 | clock.elapse(Duration.ofSeconds(1)) 42 | should("refresh after the timeout has elapsed") { 43 | obj.prop shouldBe 1 44 | } 45 | should("not refresh again") { 46 | obj.prop shouldBe 1 47 | } 48 | context("and then a long amount of time passes") { 49 | clock.elapse(Duration.ofMinutes(30)) 50 | should("refresh again") { 51 | obj.prop shouldBe 2 52 | } 53 | } 54 | } 55 | context("whose creator function throws an exception") { 56 | val exObj = object { 57 | val prop: Int? by RefreshingProperty(Duration.ofSeconds(1), clock) { 58 | throw Exception("boom") 59 | } 60 | } 61 | should("return null") { 62 | exObj.prop shouldBe null 63 | } 64 | } 65 | context("whose creator function throws an Error") { 66 | val exObj = object { 67 | val prop: Int? by RefreshingProperty(Duration.ofSeconds(1), clock) { 68 | throw NoClassDefFoundError("javax.xml.bind.DatatypeConverter") 69 | } 70 | } 71 | val error = shouldThrow { 72 | println(exObj.prop) 73 | } 74 | error.message shouldContain "javax.xml.bind.DatatypeConverter" 75 | } 76 | } 77 | }) 78 | -------------------------------------------------------------------------------- /jicoco-mediajson/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 4.0.0 19 | 20 | org.jitsi 21 | jicoco-parent 22 | 1.1-SNAPSHOT 23 | 24 | jicoco-mediajson 25 | 1.1-SNAPSHOT 26 | jicoco-mediajson 27 | Jitsi Common Components (Media JSON) 28 | 29 | 30 | com.fasterxml.jackson.module 31 | jackson-module-kotlin 32 | ${jackson.version} 33 | 34 | 35 | 36 | io.kotest 37 | kotest-runner-junit5-jvm 38 | ${kotest.version} 39 | test 40 | 41 | 42 | io.kotest 43 | kotest-assertions-core-jvm 44 | ${kotest.version} 45 | test 46 | 47 | 48 | com.googlecode.json-simple 49 | json-simple 50 | ${json.simple.version} 51 | 53 | 54 | 55 | junit 56 | junit 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | org.jetbrains.kotlin 65 | kotlin-maven-plugin 66 | ${kotlin.version} 67 | 68 | 69 | compile 70 | compile 71 | 72 | compile 73 | 74 | 75 | 76 | -opt-in=kotlin.ExperimentalStdlibApi 77 | 78 | 79 | ${project.basedir}/src/main/kotlin 80 | 81 | 82 | 83 | 84 | test-compile 85 | test-compile 86 | 87 | test-compile 88 | 89 | 90 | 91 | -opt-in=kotlin.ExperimentalStdlibApi 92 | 93 | 94 | ${project.basedir}/src/test/kotlin 95 | 96 | 97 | 98 | 99 | 100 | 11 101 | 102 | 103 | 104 | org.apache.maven.plugins 105 | maven-compiler-plugin 106 | 3.10.1 107 | 108 | 109 | 110 | default-compile 111 | none 112 | 113 | 114 | 115 | default-testCompile 116 | none 117 | 118 | 119 | java-compile 120 | compile 121 | 122 | compile 123 | 124 | 125 | 126 | java-test-compile 127 | test-compile 128 | 129 | testCompile 130 | 131 | 132 | 133 | 134 | 11 135 | 136 | -Xlint:all 137 | 138 | 139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /jicoco-mediajson/src/main/kotlin/org/jitsi/mediajson/MediaJson.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2024 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.jitsi.mediajson 17 | 18 | import com.fasterxml.jackson.annotation.JsonSubTypes 19 | import com.fasterxml.jackson.annotation.JsonTypeInfo 20 | import com.fasterxml.jackson.core.JsonGenerator 21 | import com.fasterxml.jackson.core.JsonParser 22 | import com.fasterxml.jackson.databind.DeserializationContext 23 | import com.fasterxml.jackson.databind.DeserializationFeature 24 | import com.fasterxml.jackson.databind.JsonDeserializer 25 | import com.fasterxml.jackson.databind.JsonSerializer 26 | import com.fasterxml.jackson.databind.SerializerProvider 27 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize 28 | import com.fasterxml.jackson.databind.annotation.JsonSerialize 29 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 30 | 31 | private val objectMapper = jacksonObjectMapper().apply { 32 | configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) 33 | } 34 | 35 | /** 36 | * This is based on the format used by VoxImplant here, hence the encoding of certain numeric fields as strings: 37 | * https://voximplant.com/docs/guides/voxengine/websocket 38 | */ 39 | @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "event") 40 | @JsonSubTypes( 41 | JsonSubTypes.Type(value = MediaEvent::class, name = "media"), 42 | JsonSubTypes.Type(value = StartEvent::class, name = "start"), 43 | ) 44 | sealed class Event(val event: String) { 45 | fun toJson(): String = objectMapper.writeValueAsString(this) 46 | companion object { 47 | fun parse(s: String): Event = objectMapper.readValue(s, Event::class.java) 48 | fun parse(s: List): List = s.map { objectMapper.readValue(it, Event::class.java) } 49 | } 50 | } 51 | 52 | data class MediaEvent( 53 | @JsonSerialize(using = Int2StringSerializer::class) 54 | @JsonDeserialize(using = String2IntDeserializer::class) 55 | val sequenceNumber: Int, 56 | val media: Media 57 | ) : Event("media") 58 | 59 | data class StartEvent( 60 | @JsonSerialize(using = Int2StringSerializer::class) 61 | @JsonDeserialize(using = String2IntDeserializer::class) 62 | val sequenceNumber: Int, 63 | val start: Start 64 | ) : Event("start") 65 | 66 | data class MediaFormat( 67 | val encoding: String, 68 | val sampleRate: Int, 69 | val channels: Int 70 | ) 71 | data class Start( 72 | val tag: String, 73 | val mediaFormat: MediaFormat, 74 | val customParameters: CustomParameters? = null 75 | ) 76 | 77 | data class CustomParameters( 78 | val endpointId: String? 79 | ) 80 | 81 | data class Media( 82 | val tag: String, 83 | @JsonSerialize(using = Int2StringSerializer::class) 84 | @JsonDeserialize(using = String2IntDeserializer::class) 85 | val chunk: Int, 86 | @JsonSerialize(using = Long2StringSerializer::class) 87 | @JsonDeserialize(using = String2LongDeserializer::class) 88 | val timestamp: Long, 89 | val payload: String 90 | ) 91 | 92 | class Int2StringSerializer : JsonSerializer() { 93 | override fun serialize(value: Int, gen: JsonGenerator, p: SerializerProvider) { 94 | gen.writeString(value.toString()) 95 | } 96 | } 97 | class String2IntDeserializer : JsonDeserializer() { 98 | override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Int { 99 | return p.readValueAs(Int::class.java).toInt() 100 | } 101 | } 102 | class Long2StringSerializer : JsonSerializer() { 103 | override fun serialize(value: Long, gen: JsonGenerator, p: SerializerProvider) { 104 | gen.writeString(value.toString()) 105 | } 106 | } 107 | class String2LongDeserializer : JsonDeserializer() { 108 | override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Long { 109 | return p.readValueAs(Long::class.java).toLong() 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /jicoco-mediajson/src/test/kotlin/org/jitsi/mediajson/MediaJsonTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2024 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.jitsi.mediajson 17 | 18 | import com.fasterxml.jackson.databind.exc.InvalidFormatException 19 | import io.kotest.assertions.throwables.shouldThrow 20 | import io.kotest.core.spec.style.ShouldSpec 21 | import io.kotest.matchers.nulls.shouldNotBeNull 22 | import io.kotest.matchers.shouldBe 23 | import io.kotest.matchers.types.shouldBeInstanceOf 24 | import org.json.simple.JSONObject 25 | import org.json.simple.parser.JSONParser 26 | 27 | class MediaJsonTest : ShouldSpec() { 28 | val parser = JSONParser() 29 | 30 | init { 31 | val seq = 123 32 | val tag = "t" 33 | context("StartEvent") { 34 | val enc = "opus" 35 | val sampleRate = 48000 36 | val channels = 2 37 | val event = StartEvent(seq, Start(tag, MediaFormat(enc, sampleRate, channels))) 38 | 39 | context("Serializing") { 40 | val parsed = parser.parse(event.toJson()) 41 | 42 | parsed.shouldBeInstanceOf() 43 | parsed["event"] shouldBe "start" 44 | // intentionally encoded as a string 45 | parsed["sequenceNumber"] shouldBe seq.toString() 46 | val start = parsed["start"] 47 | start.shouldBeInstanceOf() 48 | start["tag"] shouldBe tag 49 | start["customParameters"] shouldBe null 50 | val mediaFormat = start["mediaFormat"] 51 | mediaFormat.shouldBeInstanceOf() 52 | mediaFormat["encoding"] shouldBe enc 53 | mediaFormat["sampleRate"] shouldBe sampleRate 54 | mediaFormat["channels"] shouldBe channels 55 | } 56 | context("Parsing") { 57 | val parsed = Event.parse(event.toJson()) 58 | (parsed == event) shouldBe true 59 | (parsed === event) shouldBe false 60 | 61 | val parsedList = Event.parse(listOf(event.toJson(), event.toJson())) 62 | parsedList.shouldBeInstanceOf>() 63 | parsedList.size shouldBe 2 64 | parsedList[0] shouldBe event 65 | parsedList[1] shouldBe event 66 | } 67 | } 68 | context("MediaEvent") { 69 | val chunk = 213 70 | val timestamp = 0x1_0000_ffff 71 | val payload = "p" 72 | val event = MediaEvent(seq, Media(tag, chunk, timestamp, payload)) 73 | 74 | context("Serializing") { 75 | val parsed = parser.parse(event.toJson()) 76 | parsed.shouldBeInstanceOf() 77 | parsed["event"] shouldBe "media" 78 | // intentionally encoded as a string 79 | parsed["sequenceNumber"] shouldBe seq.toString() 80 | val media = parsed["media"] 81 | media.shouldBeInstanceOf() 82 | media["tag"] shouldBe tag 83 | // intentionally encoded as a string 84 | media["chunk"] shouldBe chunk.toString() 85 | // intentionally encoded as a string 86 | media["timestamp"] shouldBe timestamp.toString() 87 | media["payload"] shouldBe payload 88 | } 89 | context("Parsing") { 90 | val parsed = Event.parse(event.toJson()) 91 | (parsed == event) shouldBe true 92 | (parsed === event) shouldBe false 93 | } 94 | } 95 | context("Parsing valid samples") { 96 | context("Start") { 97 | val parsed = Event.parse( 98 | """ 99 | { 100 | "event": "start", 101 | "sequenceNumber": "0", 102 | "start": { 103 | "tag": "incoming", 104 | "mediaFormat": { 105 | "encoding": "audio/x-mulaw", 106 | "sampleRate": 8000, 107 | "channels": 1 108 | }, 109 | "customParameters": { 110 | "text1":"12312", 111 | "endpointId": "abcdabcd" 112 | } 113 | } 114 | } 115 | """.trimIndent() 116 | ) 117 | 118 | parsed.shouldBeInstanceOf() 119 | parsed.event shouldBe "start" 120 | parsed.sequenceNumber shouldBe 0 121 | parsed.start.tag shouldBe "incoming" 122 | parsed.start.mediaFormat.encoding shouldBe "audio/x-mulaw" 123 | parsed.start.mediaFormat.sampleRate shouldBe 8000 124 | parsed.start.mediaFormat.channels shouldBe 1 125 | parsed.start.customParameters.shouldNotBeNull() 126 | parsed.start.customParameters?.endpointId shouldBe "abcdabcd" 127 | } 128 | context("Start with sequence number as int") { 129 | val parsed = Event.parse( 130 | """ 131 | { 132 | "event": "start", 133 | "sequenceNumber": 0, 134 | "start": { 135 | "tag": "incoming", 136 | "mediaFormat": { 137 | "encoding": "audio/x-mulaw", 138 | "sampleRate": 8000, 139 | "channels": 1 140 | }, 141 | "customParameters": { 142 | "text1":"12312", 143 | "endpointId":"abcdabcd" 144 | } 145 | } 146 | } 147 | """.trimIndent() 148 | ) 149 | 150 | parsed.shouldBeInstanceOf() 151 | parsed.sequenceNumber shouldBe 0 152 | parsed.start.customParameters.shouldNotBeNull() 153 | parsed.start.customParameters?.endpointId shouldBe "abcdabcd" 154 | } 155 | context("Media") { 156 | val parsed = Event.parse( 157 | """ 158 | { 159 | "event": "media", 160 | "sequenceNumber": "2", 161 | "media": { 162 | "tag": "incoming", 163 | "chunk": "1", 164 | "timestamp": "5", 165 | "payload": "no+JhoaJjpzSHxAKBgYJ...==" 166 | } 167 | } 168 | """.trimIndent() 169 | ) 170 | 171 | parsed.shouldBeInstanceOf() 172 | parsed.event shouldBe "media" 173 | parsed.sequenceNumber shouldBe 2 174 | parsed.media.tag shouldBe "incoming" 175 | parsed.media.chunk shouldBe 1 176 | parsed.media.timestamp shouldBe 5 177 | parsed.media.payload shouldBe "no+JhoaJjpzSHxAKBgYJ...==" 178 | } 179 | context("Media with seq/chunk/timestamp as numbers") { 180 | val parsed = Event.parse( 181 | """ 182 | { 183 | "event": "media", 184 | "sequenceNumber": 2, 185 | "media": { 186 | "tag": "incoming", 187 | "chunk": 1, 188 | "timestamp": 5, 189 | "payload": "no+JhoaJjpzSHxAKBgYJ...==" 190 | } 191 | } 192 | """.trimIndent() 193 | ) 194 | 195 | parsed.shouldBeInstanceOf() 196 | parsed.event shouldBe "media" 197 | parsed.sequenceNumber shouldBe 2 198 | parsed.media.tag shouldBe "incoming" 199 | parsed.media.chunk shouldBe 1 200 | parsed.media.timestamp shouldBe 5 201 | parsed.media.payload shouldBe "no+JhoaJjpzSHxAKBgYJ...==" 202 | } 203 | } 204 | context("Parsing invalid samples") { 205 | context("Invalid sequence number") { 206 | shouldThrow { 207 | Event.parse( 208 | """ 209 | { 210 | "event": "media", 211 | "sequenceNumber": "not a number", 212 | "media": { 213 | "tag": "incoming", 214 | "chunk": "1", 215 | "timestamp": "5", 216 | "payload": "no+JhoaJjpzSHxAKBgYJ...==" 217 | } 218 | } 219 | """.trimIndent() 220 | ) 221 | } 222 | } 223 | context("Invalid chunk") { 224 | shouldThrow { 225 | Event.parse( 226 | """ 227 | { 228 | "event": "media", 229 | "sequenceNumber": "1", 230 | "media": { 231 | "tag": "incoming", 232 | "chunk": "not a number", 233 | "timestamp": "5", 234 | "payload": "no+JhoaJjpzSHxAKBgYJ...==" 235 | } 236 | } 237 | """.trimIndent() 238 | ) 239 | } 240 | } 241 | context("Invalid timestamp") { 242 | shouldThrow { 243 | Event.parse( 244 | """ 245 | { 246 | "event": "media", 247 | "sequenceNumber": "1", 248 | "media": { 249 | "tag": "incoming", 250 | "chunk": "1", 251 | "timestamp": "not a number", 252 | "payload": "no+JhoaJjpzSHxAKBgYJ...==" 253 | } 254 | } 255 | """.trimIndent() 256 | ) 257 | } 258 | } 259 | } 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /jicoco-metrics/checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /jicoco-metrics/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 21 | 4.0.0 22 | 23 | 24 | org.jitsi 25 | jicoco-parent 26 | 1.1-SNAPSHOT 27 | 28 | 29 | jicoco-metrics 30 | 1.1-SNAPSHOT 31 | jicoco-metrics 32 | Jitsi Common Components (Metrics) 33 | 34 | 35 | 36 | org.jetbrains.kotlin 37 | kotlin-stdlib-jdk8 38 | 39 | 40 | ${project.groupId} 41 | jitsi-utils 42 | 43 | 44 | io.prometheus 45 | simpleclient 46 | ${prometheus.version} 47 | 48 | 49 | io.prometheus 50 | simpleclient_common 51 | ${prometheus.version} 52 | 53 | 54 | 55 | org.junit.platform 56 | junit-platform-launcher 57 | 1.10.0 58 | test 59 | 60 | 61 | org.junit.jupiter 62 | junit-jupiter-api 63 | ${junit.version} 64 | test 65 | 66 | 67 | org.junit.jupiter 68 | junit-jupiter-engine 69 | ${junit.version} 70 | test 71 | 72 | 73 | io.kotest 74 | kotest-runner-junit5-jvm 75 | ${kotest.version} 76 | test 77 | 78 | 79 | io.kotest 80 | kotest-assertions-core-jvm 81 | ${kotest.version} 82 | test 83 | 84 | 85 | org.glassfish.jersey.test-framework 86 | jersey-test-framework-core 87 | ${jersey.version} 88 | test 89 | 90 | 91 | junit 92 | junit 93 | 94 | 95 | 96 | 97 | org.junit.vintage 98 | junit-vintage-engine 99 | ${junit.version} 100 | test 101 | 102 | 103 | 104 | 105 | 106 | 107 | org.jetbrains.kotlin 108 | kotlin-maven-plugin 109 | ${kotlin.version} 110 | 111 | 112 | compile 113 | compile 114 | 115 | compile 116 | 117 | 118 | 119 | -opt-in=kotlin.ExperimentalStdlibApi 120 | 121 | 122 | ${project.basedir}/src/main/kotlin 123 | 124 | 125 | 126 | 127 | test-compile 128 | test-compile 129 | 130 | test-compile 131 | 132 | 133 | 134 | -opt-in=kotlin.ExperimentalStdlibApi 135 | 136 | 137 | ${project.basedir}/src/test/kotlin 138 | ${project.basedir}/src/test/java 139 | 140 | 141 | 142 | 143 | 144 | 11 145 | 146 | 147 | 148 | org.apache.maven.plugins 149 | maven-compiler-plugin 150 | 3.10.1 151 | 152 | 153 | 154 | default-compile 155 | none 156 | 157 | 158 | 159 | default-testCompile 160 | none 161 | 162 | 163 | java-compile 164 | compile 165 | 166 | compile 167 | 168 | 169 | 170 | java-test-compile 171 | test-compile 172 | 173 | testCompile 174 | 175 | 176 | 177 | 178 | 11 179 | 180 | -Xlint:all 181 | 182 | 183 | 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /jicoco-metrics/src/main/kotlin/org/jitsi/metrics/BooleanMetric.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2022 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.jitsi.metrics 17 | 18 | import io.prometheus.client.CollectorRegistry 19 | import io.prometheus.client.Gauge 20 | 21 | /** 22 | * A metric that represents booleans using Prometheus [Gauges][Gauge]. 23 | * A non-zero value corresponds to `true`, zero corresponds to `false`. 24 | */ 25 | class BooleanMetric @JvmOverloads constructor( 26 | /** the name of this metric */ 27 | override val name: String, 28 | /** the description of this metric */ 29 | help: String, 30 | /** the namespace (prefix) of this metric */ 31 | namespace: String, 32 | /** an optional initial value for this metric */ 33 | internal val initialValue: Boolean = false, 34 | /** Label names for this metric. If non-empty, the initial value must be false and all get/update calls MUST 35 | * specify values for the labels. Calls to simply get() or set() will fail with an exception. */ 36 | val labelNames: List = emptyList() 37 | ) : Metric() { 38 | private val gauge = run { 39 | val builder = Gauge.build(name, help).namespace(namespace) 40 | if (labelNames.isNotEmpty()) { 41 | builder.labelNames(*labelNames.toTypedArray()) 42 | if (initialValue) { 43 | throw IllegalArgumentException("Cannot set an initial value for a labeled gauge") 44 | } 45 | } 46 | builder.create().apply { 47 | if (initialValue) { 48 | set(1.0) 49 | } 50 | } 51 | } 52 | 53 | override val supportsJson: Boolean = labelNames.isEmpty() 54 | override fun get() = gauge.get() != 0.0 55 | fun get(labels: List) = gauge.labels(*labels.toTypedArray()).get() != 0.0 56 | 57 | override fun reset() = synchronized(gauge) { 58 | gauge.clear() 59 | if (initialValue) { 60 | gauge.set(1.0) 61 | } 62 | } 63 | 64 | override fun register(registry: CollectorRegistry) = this.also { registry.register(gauge) } 65 | 66 | /** 67 | * Atomically sets the gauge to the given value. 68 | */ 69 | @JvmOverloads 70 | fun set(newValue: Boolean, labels: List = emptyList()): Unit = synchronized(gauge) { 71 | if (labels.isEmpty()) { 72 | gauge.set(if (newValue) 1.0 else 0.0) 73 | } else { 74 | gauge.labels(*labels.toTypedArray()).set(if (newValue) 1.0 else 0.0) 75 | } 76 | } 77 | 78 | /** 79 | * Atomically sets the gauge to the given value, returning the updated value. 80 | * 81 | * @return the updated value 82 | */ 83 | @JvmOverloads 84 | fun setAndGet(newValue: Boolean, labels: List = emptyList()): Boolean = synchronized(gauge) { 85 | set(newValue, labels) 86 | return newValue 87 | } 88 | 89 | /** Remove the child with the given labels (the metric with those labels will stop being emitted) */ 90 | fun remove(labels: List = emptyList()) = synchronized(gauge) { 91 | if (labels.isNotEmpty()) { 92 | gauge.remove(*labels.toTypedArray()) 93 | } 94 | } 95 | internal fun collect() = gauge.collect() 96 | } 97 | -------------------------------------------------------------------------------- /jicoco-metrics/src/main/kotlin/org/jitsi/metrics/CounterMetric.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2022 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.jitsi.metrics 17 | 18 | import io.prometheus.client.CollectorRegistry 19 | import io.prometheus.client.Counter 20 | 21 | /** 22 | * A long metric wrapper for a Prometheus [Counter], which is monotonically increasing. 23 | * Provides atomic operations such as [incAndGet]. 24 | * 25 | * @see [Prometheus Counter](https://prometheus.io/docs/concepts/metric_types/.counter) 26 | * 27 | * @see [Prometheus Gauge](https://prometheus.io/docs/concepts/metric_types/.gauge) 28 | */ 29 | class CounterMetric @JvmOverloads constructor( 30 | /** the name of this metric */ 31 | override val name: String, 32 | /** the description of this metric */ 33 | help: String, 34 | /** the namespace (prefix) of this metric */ 35 | namespace: String, 36 | /** an optional initial value for this metric */ 37 | internal val initialValue: Long = 0L, 38 | /** Label names for this metric. If non-empty, the initial value must be 0 and all get/update calls MUST 39 | * specify values for the labels. Calls to simply [get()] or [inc()] will fail with an exception. */ 40 | val labelNames: List = emptyList() 41 | ) : Metric() { 42 | private val counter = run { 43 | val builder = Counter.build(name, help).namespace(namespace) 44 | if (labelNames.isNotEmpty()) { 45 | builder.labelNames(*labelNames.toTypedArray()) 46 | if (initialValue != 0L) { 47 | throw IllegalArgumentException("Cannot set an initial value for a labeled counter") 48 | } 49 | } 50 | builder.create().apply { 51 | if (initialValue != 0L) { 52 | inc(initialValue.toDouble()) 53 | } 54 | } 55 | } 56 | 57 | /** When we have labels [get()] throws an exception and the JSON format is not supported. */ 58 | override val supportsJson: Boolean = labelNames.isEmpty() 59 | 60 | override fun get() = counter.get().toLong() 61 | fun get(labels: List) = counter.labels(*labels.toTypedArray()).get().toLong() 62 | 63 | override fun reset() { 64 | synchronized(counter) { 65 | counter.apply { 66 | clear() 67 | if (initialValue != 0L) { 68 | inc(initialValue.toDouble()) 69 | } 70 | } 71 | } 72 | } 73 | 74 | override fun register(registry: CollectorRegistry) = this.also { registry.register(counter) } 75 | 76 | /** 77 | * Atomically adds the given value to this counter. 78 | */ 79 | @JvmOverloads 80 | fun add(delta: Long, labels: List = emptyList()) = synchronized(counter) { 81 | if (labels.isEmpty()) { 82 | counter.inc(delta.toDouble()) 83 | } else { 84 | counter.labels(*labels.toTypedArray()).inc(delta.toDouble()) 85 | } 86 | } 87 | 88 | /** 89 | * Atomically adds the given value to this counter, returning the updated value. 90 | * 91 | * @return the updated value 92 | */ 93 | @JvmOverloads 94 | fun addAndGet(delta: Long, labels: List = emptyList()): Long = synchronized(counter) { 95 | return if (labels.isEmpty()) { 96 | counter.inc(delta.toDouble()) 97 | counter.get().toLong() 98 | } else { 99 | counter.labels(*labels.toTypedArray()).inc(delta.toDouble()) 100 | counter.labels(*labels.toTypedArray()).get().toLong() 101 | } 102 | } 103 | 104 | /** 105 | * Atomically increments the value of this counter by one, returning the updated value. 106 | * 107 | * @return the updated value 108 | */ 109 | @JvmOverloads 110 | fun incAndGet(labels: List = emptyList()) = addAndGet(1, labels) 111 | 112 | /** 113 | * Atomically increments the value of this counter by one. 114 | */ 115 | @JvmOverloads 116 | fun inc(labels: List = emptyList()) = synchronized(counter) { 117 | if (labels.isEmpty()) { 118 | counter.inc() 119 | } else { 120 | counter.labels(*labels.toTypedArray()).inc() 121 | } 122 | } 123 | 124 | /** Remove the child with the given labels (the metric with those labels will stop being emitted) */ 125 | fun remove(labels: List = emptyList()) = synchronized(counter) { 126 | if (labels.isNotEmpty()) { 127 | counter.remove(*labels.toTypedArray()) 128 | } 129 | } 130 | 131 | internal fun collect() = counter.collect() 132 | } 133 | -------------------------------------------------------------------------------- /jicoco-metrics/src/main/kotlin/org/jitsi/metrics/DoubleGaugeMetric.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2022 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.jitsi.metrics 17 | 18 | import io.prometheus.client.CollectorRegistry 19 | import io.prometheus.client.Gauge 20 | 21 | /** 22 | * A double metric wrapper for Prometheus [Gauges][Gauge]. 23 | * Provides atomic operations such as [incAndGet]. 24 | * 25 | * @see [Prometheus Gauge](https://prometheus.io/docs/concepts/metric_types/.gauge) 26 | */ 27 | class DoubleGaugeMetric @JvmOverloads constructor( 28 | /** the name of this metric */ 29 | override val name: String, 30 | /** the description of this metric */ 31 | help: String, 32 | /** the namespace (prefix) of this metric */ 33 | namespace: String, 34 | /** an optional initial value for this metric */ 35 | internal val initialValue: Double = 0.0, 36 | /** Label names for this metric. If non-empty, the initial value must be 0 and all get/update calls MUST 37 | * specify values for the labels. Calls to simply [get()] or [set(Double)] will fail with an exception. */ 38 | val labelNames: List = emptyList() 39 | ) : Metric() { 40 | private val gauge = run { 41 | val builder = Gauge.build(name, help).namespace(namespace) 42 | if (labelNames.isNotEmpty()) { 43 | builder.labelNames(*labelNames.toTypedArray()) 44 | if (initialValue != 0.0) { 45 | throw IllegalArgumentException("Cannot set an initial value for a labeled gauge") 46 | } 47 | } 48 | builder.create().apply { 49 | if (initialValue != 0.0) { 50 | set(initialValue) 51 | } 52 | } 53 | } 54 | 55 | /** When we have labels [get()] throws an exception and the JSON format is not supported. */ 56 | override val supportsJson: Boolean = labelNames.isEmpty() 57 | 58 | override fun get() = gauge.get() 59 | fun get(labelNames: List) = gauge.labels(*labelNames.toTypedArray()).get() 60 | 61 | override fun reset() = synchronized(gauge) { 62 | gauge.clear() 63 | if (initialValue != 0.0) { 64 | gauge.set(initialValue) 65 | } 66 | } 67 | 68 | override fun register(registry: CollectorRegistry) = this.also { registry.register(gauge) } 69 | 70 | /** 71 | * Sets the value of this gauge to the given value. 72 | */ 73 | @JvmOverloads 74 | fun set(newValue: Double, labels: List = emptyList()) { 75 | if (labels.isEmpty()) { 76 | gauge.set(newValue) 77 | } else { 78 | gauge.labels(*labels.toTypedArray()).set(newValue) 79 | } 80 | } 81 | 82 | /** 83 | * Atomically sets the gauge to the given value, returning the updated value. 84 | * 85 | * @return the updated value 86 | */ 87 | @JvmOverloads 88 | fun setAndGet(newValue: Double, labels: List = emptyList()): Double = synchronized(gauge) { 89 | return if (labels.isEmpty()) { 90 | gauge.set(newValue) 91 | gauge.get() 92 | } else { 93 | with(gauge.labels(*labels.toTypedArray())) { 94 | set(newValue) 95 | get() 96 | } 97 | } 98 | } 99 | 100 | /** 101 | * Atomically adds the given value to this gauge, returning the updated value. 102 | * 103 | * @return the updated value 104 | */ 105 | @JvmOverloads 106 | fun addAndGet(delta: Double, labels: List = emptyList()): Double = synchronized(gauge) { 107 | return if (labels.isEmpty()) { 108 | gauge.inc(delta) 109 | gauge.get() 110 | } else { 111 | with(gauge.labels(*labels.toTypedArray())) { 112 | inc(delta) 113 | get() 114 | } 115 | } 116 | } 117 | 118 | /** 119 | * Atomically increments the value of this gauge by one, returning the updated value. 120 | * 121 | * @return the updated value 122 | */ 123 | @JvmOverloads 124 | fun incAndGet(labels: List = emptyList()) = addAndGet(1.0, labels) 125 | 126 | /** 127 | * Atomically decrements the value of this gauge by one, returning the updated value. 128 | * 129 | * @return the updated value 130 | */ 131 | @JvmOverloads 132 | fun decAndGet(labels: List = emptyList()) = addAndGet(-1.0, labels) 133 | 134 | /** Remove the child with the given labels (the metric with those labels will stop being emitted) */ 135 | fun remove(labels: List = emptyList()) = synchronized(gauge) { 136 | if (labels.isNotEmpty()) { 137 | gauge.remove(*labels.toTypedArray()) 138 | } 139 | } 140 | 141 | internal fun collect() = gauge.collect() 142 | } 143 | -------------------------------------------------------------------------------- /jicoco-metrics/src/main/kotlin/org/jitsi/metrics/HistogramMetric.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2023 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.jitsi.metrics 17 | 18 | import io.prometheus.client.CollectorRegistry 19 | import io.prometheus.client.Histogram 20 | import org.json.simple.JSONObject 21 | 22 | class HistogramMetric( 23 | override val name: String, 24 | /** the description of this metric */ 25 | private val help: String, 26 | /** the namespace (prefix) of this metric */ 27 | val namespace: String, 28 | vararg buckets: Double 29 | ) : Metric() { 30 | val histogram: Histogram = Histogram.build(name, help).namespace(namespace).buckets(*buckets).create() 31 | 32 | override fun get(): JSONObject = JSONObject().apply { 33 | histogram.collect().forEach { 34 | it.samples.forEach { sample -> 35 | if (sample.name.startsWith("${namespace}_${name}_")) { 36 | val shortName = sample.name.substring("${namespace}_${name}_".length) 37 | if (shortName == "bucket" && sample.labelNames.size == 1) { 38 | put("${shortName}_${sample.labelNames[0]}_${sample.labelValues[0]}", sample.value) 39 | } else { 40 | put(shortName, sample.value) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | override fun reset() = histogram.clear() 48 | 49 | override fun register(registry: CollectorRegistry): Metric = this.also { registry.register(histogram) } 50 | } 51 | -------------------------------------------------------------------------------- /jicoco-metrics/src/main/kotlin/org/jitsi/metrics/InfoMetric.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2022 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.jitsi.metrics 17 | 18 | import io.prometheus.client.CollectorRegistry 19 | import io.prometheus.client.Info 20 | 21 | /** 22 | * `InfoMetric` wraps around a single key-value information pair. 23 | * Useful for general information such as build versions, JVB region, etc. 24 | * In the Prometheus exposition format, these are shown as labels of either a custom metric (OpenMetrics) 25 | * or a [Gauge][io.prometheus.client.Gauge] (0.0.4 plain text). 26 | */ 27 | class InfoMetric @JvmOverloads constructor( 28 | /** the name of this metric */ 29 | override val name: String, 30 | /** the description of this metric */ 31 | help: String, 32 | /** the namespace (prefix) of this metric */ 33 | namespace: String, 34 | /** the value of this info metric */ 35 | internal val value: String = "", 36 | /** Label names for this metric */ 37 | val labelNames: List = emptyList() 38 | ) : Metric() { 39 | private val info = run { 40 | val builder = Info.build(name, help).namespace(namespace) 41 | if (labelNames.isNotEmpty()) { 42 | builder.labelNames(*labelNames.toTypedArray()) 43 | } 44 | builder.create().apply { 45 | if (labelNames.isEmpty()) { 46 | info(name, value) 47 | } 48 | } 49 | } 50 | 51 | override fun get() = if (labelNames.isEmpty()) value else throw UnsupportedOperationException() 52 | fun get(labels: List = emptyList()) = 53 | if (labels.isEmpty()) value else info.labels(*labels.toTypedArray()).get()[name] 54 | 55 | override fun reset() = if (labelNames.isEmpty()) info.info(name, value) else info.clear() 56 | 57 | override fun register(registry: CollectorRegistry) = this.also { registry.register(info) } 58 | 59 | /** Remove the child with the given labels (the metric with those labels will stop being emitted) */ 60 | fun remove(labels: List = emptyList()) = synchronized(info) { 61 | if (labels.isNotEmpty()) { 62 | info.remove(*labels.toTypedArray()) 63 | } 64 | } 65 | 66 | fun set(labels: List, value: String) { 67 | if (labels.isNotEmpty()) { 68 | info.labels(*labels.toTypedArray()).info(name, value) 69 | } 70 | } 71 | internal fun collect() = info.collect() 72 | } 73 | -------------------------------------------------------------------------------- /jicoco-metrics/src/main/kotlin/org/jitsi/metrics/LongGaugeMetric.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2022 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.jitsi.metrics 17 | 18 | import io.prometheus.client.CollectorRegistry 19 | import io.prometheus.client.Gauge 20 | 21 | /** 22 | * A long metric wrapper for Prometheus [Gauges][Gauge]. 23 | * Provides atomic operations such as [incAndGet]. 24 | * 25 | * @see [Prometheus Gauge](https://prometheus.io/docs/concepts/metric_types/.gauge) 26 | */ 27 | class LongGaugeMetric @JvmOverloads constructor( 28 | /** the name of this metric */ 29 | override val name: String, 30 | /** the description of this metric */ 31 | help: String, 32 | /** the namespace (prefix) of this metric */ 33 | namespace: String, 34 | /** an optional initial value for this metric */ 35 | internal val initialValue: Long = 0L, 36 | /** Label names for this metric. If non-empty, the initial value must be 0 and all get/update calls MUST 37 | * specify values for the labels. Calls to simply get() or set() will fail with an exception. */ 38 | val labelNames: List = emptyList() 39 | ) : Metric() { 40 | private val gauge = run { 41 | val builder = Gauge.build(name, help).namespace(namespace) 42 | if (labelNames.isNotEmpty()) { 43 | builder.labelNames(*labelNames.toTypedArray()) 44 | if (initialValue != 0L) { 45 | throw IllegalArgumentException("Cannot set an initial value for a labeled gauge") 46 | } 47 | } 48 | builder.create().apply { 49 | if (initialValue != 0L) { 50 | set(initialValue.toDouble()) 51 | } 52 | } 53 | } 54 | 55 | /** When we have labels [get()] throws an exception and the JSON format is not supported. */ 56 | override val supportsJson: Boolean = labelNames.isEmpty() 57 | override fun get() = gauge.get().toLong() 58 | fun get(labels: List) = gauge.labels(*labels.toTypedArray()).get().toLong() 59 | 60 | override fun reset() = synchronized(gauge) { 61 | gauge.clear() 62 | if (initialValue != 0L) { 63 | gauge.inc(initialValue.toDouble()) 64 | } 65 | } 66 | 67 | override fun register(registry: CollectorRegistry) = this.also { registry.register(gauge) } 68 | 69 | /** 70 | * Atomically sets the gauge to the given value. 71 | */ 72 | @JvmOverloads 73 | fun set(newValue: Long, labels: List = emptyList()): Unit = synchronized(gauge) { 74 | if (labels.isEmpty()) { 75 | gauge.set(newValue.toDouble()) 76 | } else { 77 | gauge.labels(*labels.toTypedArray()).set(newValue.toDouble()) 78 | } 79 | } 80 | 81 | /** 82 | * Atomically increments the value of this gauge by one. 83 | */ 84 | @JvmOverloads 85 | fun inc(labels: List = emptyList()) = synchronized(gauge) { 86 | if (labels.isEmpty()) { 87 | gauge.inc() 88 | } else { 89 | gauge.labels(*labels.toTypedArray()).inc() 90 | } 91 | } 92 | 93 | /** 94 | * Atomically decrements the value of this gauge by one. 95 | */ 96 | @JvmOverloads 97 | fun dec(labels: List = emptyList()) = synchronized(gauge) { 98 | if (labels.isEmpty()) { 99 | gauge.dec() 100 | } else { 101 | gauge.labels(*labels.toTypedArray()).dec() 102 | } 103 | } 104 | 105 | /** 106 | * Atomically adds the given value to this gauge, returning the updated value. 107 | * 108 | * @return the updated value 109 | */ 110 | @JvmOverloads 111 | fun addAndGet(delta: Long, labels: List = emptyList()): Long = synchronized(gauge) { 112 | return if (labels.isEmpty()) { 113 | gauge.inc(delta.toDouble()) 114 | gauge.get().toLong() 115 | } else { 116 | with(gauge.labels(*labels.toTypedArray())) { 117 | inc(delta.toDouble()) 118 | get().toLong() 119 | } 120 | } 121 | } 122 | 123 | /** 124 | * Atomically increments the value of this gauge by one, returning the updated value. 125 | * 126 | * @return the updated value 127 | */ 128 | @JvmOverloads 129 | fun incAndGet(labels: List = emptyList()) = addAndGet(1, labels) 130 | 131 | /** 132 | * Atomically decrements the value of this gauge by one, returning the updated value. 133 | * 134 | * @return the updated value 135 | */ 136 | @JvmOverloads 137 | fun decAndGet(labels: List = emptyList()) = addAndGet(-1, labels) 138 | 139 | /** Remove the child with the given labels (the metric with those labels will stop being emitted) */ 140 | fun remove(labels: List = emptyList()) = synchronized(gauge) { 141 | if (labels.isNotEmpty()) { 142 | gauge.remove(*labels.toTypedArray()) 143 | } 144 | } 145 | internal fun collect() = gauge.collect() 146 | } 147 | -------------------------------------------------------------------------------- /jicoco-metrics/src/main/kotlin/org/jitsi/metrics/Metric.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2022 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.jitsi.metrics 17 | 18 | import io.prometheus.client.CollectorRegistry 19 | 20 | /** 21 | * `Metric` provides methods common to all Prometheus metric type wrappers. 22 | * 23 | * A wrapper that extends `Metric` produces and consumes values of type `T`. 24 | * Metrics are held in a [MetricsContainer]. 25 | */ 26 | sealed class Metric { 27 | 28 | /** 29 | * The name of this metric. 30 | */ 31 | abstract val name: String 32 | 33 | /** 34 | * Supplies the current value of this metric. 35 | */ 36 | abstract fun get(): T 37 | 38 | /** 39 | * Resets the value of this metric to its initial value. 40 | */ 41 | internal abstract fun reset() 42 | 43 | /** 44 | * Registers this metric with the given [CollectorRegistry] and returns it. 45 | */ 46 | internal abstract fun register(registry: CollectorRegistry): Metric 47 | 48 | internal open val supportsJson: Boolean = true 49 | } 50 | -------------------------------------------------------------------------------- /jicoco-metrics/src/main/kotlin/org/jitsi/metrics/MetricsUpdater.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2023 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.jitsi.metrics 17 | 18 | import org.jitsi.utils.logging2.createLogger 19 | import java.time.Duration 20 | import java.util.concurrent.CopyOnWriteArrayList 21 | import java.util.concurrent.ScheduledExecutorService 22 | import java.util.concurrent.ScheduledFuture 23 | import java.util.concurrent.TimeUnit 24 | 25 | class MetricsUpdater( 26 | private val executor: ScheduledExecutorService, 27 | private val updateInterval: Duration 28 | ) { 29 | private val logger = createLogger() 30 | private val subtasks: MutableList<() -> Unit> = CopyOnWriteArrayList() 31 | private var updateTask: ScheduledFuture<*>? = null 32 | 33 | // Allow updates to be disabled for tests 34 | var disablePeriodicUpdates = false 35 | 36 | fun addUpdateTask(subtask: () -> Unit) { 37 | if (disablePeriodicUpdates) { 38 | logger.warn("Periodic updates are disabled, will not execute update task.") 39 | return 40 | } 41 | 42 | subtasks.add(subtask) 43 | synchronized(this) { 44 | if (updateTask == null) { 45 | logger.info("Scheduling metrics update task with interval $updateInterval.") 46 | updateTask = executor.scheduleAtFixedRate( 47 | { updateMetrics() }, 48 | 0, 49 | updateInterval.toMillis(), 50 | TimeUnit.MILLISECONDS 51 | ) 52 | } 53 | } 54 | } 55 | 56 | fun updateMetrics() { 57 | synchronized(this) { 58 | logger.debug("Running ${subtasks.size} subtasks.") 59 | subtasks.forEach { 60 | try { 61 | it.invoke() 62 | } catch (e: Exception) { 63 | logger.warn("Exception while running subtask", e) 64 | } 65 | } 66 | } 67 | } 68 | 69 | fun stop() = synchronized(this) { 70 | updateTask?.cancel(false) 71 | updateTask = null 72 | subtasks.clear() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /jicoco-metrics/src/test/kotlin/org/jitsi/metrics/MetricsContainerTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2022 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.metrics 18 | 19 | import io.kotest.assertions.throwables.shouldThrow 20 | import io.kotest.core.spec.style.ShouldSpec 21 | import io.kotest.matchers.collections.shouldContainExactly 22 | import io.kotest.matchers.shouldBe 23 | import io.prometheus.client.CollectorRegistry 24 | import io.prometheus.client.exporter.common.TextFormat 25 | 26 | class MetricsContainerTest : ShouldSpec() { 27 | 28 | private val mc = MetricsContainer() 29 | private val otherRegistry = CollectorRegistry() 30 | 31 | init { 32 | context("Registering metrics") { 33 | val booleanMetric = mc.registerBooleanMetric("boolean", "A boolean metric") 34 | val counter = mc.registerCounter("counter", "A counter metric") 35 | val info = mc.registerInfo("info", "An info metric", "value") 36 | val longGauge = mc.registerLongGauge("gauge", "A gauge metric") 37 | 38 | context("twice in the same registry") { 39 | context("while checking for name conflicts") { 40 | should("throw a RuntimeException") { 41 | shouldThrow { mc.registerBooleanMetric("boolean", "A boolean metric") } 42 | // "counter" is renamed to "counter_total" so both should throw an exception 43 | shouldThrow { mc.registerCounter("counter", "A counter metric") } 44 | shouldThrow { mc.registerCounter("counter_total", "A counter metric") } 45 | // we test this because the Prometheus JVM library stores Counters without the "_total" suffix 46 | shouldThrow { mc.registerCounter("boolean_total", "A counter metric") } 47 | } 48 | } 49 | context("without checking for name conflicts") { 50 | mc.checkForNameConflicts = false 51 | should("return an existing metric") { 52 | booleanMetric shouldBe mc.registerBooleanMetric("boolean", "A boolean metric") 53 | // "counter" is renamed to "counter_total" so both should return the same metric 54 | counter shouldBe mc.registerCounter("counter", "A counter metric") 55 | counter shouldBe mc.registerCounter("counter_total", "A counter metric") 56 | info shouldBe mc.registerInfo("info", "An info metric", "value") 57 | longGauge shouldBe mc.registerLongGauge("gauge", "A gauge metric") 58 | } 59 | mc.checkForNameConflicts = true 60 | } 61 | } 62 | context("in a new registry") { 63 | should("successfully register metrics") { 64 | booleanMetric.register(otherRegistry) 65 | counter.register(otherRegistry) 66 | info.register(otherRegistry) 67 | longGauge.register(otherRegistry) 68 | } 69 | should("contain the same metrics in both registries") { 70 | val a = CollectorRegistry.defaultRegistry.metricFamilySamples().toList() 71 | val b = otherRegistry.metricFamilySamples().toList() 72 | a shouldContainExactly b 73 | } 74 | } 75 | context("and altering their values") { 76 | booleanMetric.set(!booleanMetric.get()) 77 | counter.add(5) 78 | longGauge.set(5) 79 | context("then resetting all metrics in the MetricsContainer") { 80 | mc.resetAll() 81 | should("set all metric values to their initial values") { 82 | booleanMetric.get() shouldBe booleanMetric.initialValue 83 | counter.get() shouldBe counter.initialValue 84 | longGauge.get() shouldBe longGauge.initialValue 85 | } 86 | } 87 | } 88 | } 89 | context("Getting metrics with different accepted content types") { 90 | should("return the correct content type") { 91 | mc.getMetrics(emptyList()).second shouldBe TextFormat.CONTENT_TYPE_OPENMETRICS_100 92 | mc.getMetrics(listOf("text/plain")).second shouldBe TextFormat.CONTENT_TYPE_004 93 | mc.getMetrics(listOf("application/json")).second shouldBe "application/json" 94 | mc.getMetrics(listOf("application/openmetrics-text")).second shouldBe 95 | TextFormat.CONTENT_TYPE_OPENMETRICS_100 96 | mc.getMetrics(listOf("application/openmetrics-text", "application/json")).second shouldBe 97 | TextFormat.CONTENT_TYPE_OPENMETRICS_100 98 | mc.getMetrics(listOf("application/json", "application/openmetrics-text")).second shouldBe 99 | "application/json" 100 | mc.getMetrics( 101 | listOf( 102 | "application/json", 103 | "application/other", 104 | "application/openmetrics-text" 105 | ) 106 | ).second shouldBe 107 | "application/json" 108 | mc.getMetrics(listOf("application/json", "*/*", "application/openmetrics-text")).second shouldBe 109 | "application/json" 110 | mc.getMetrics(listOf("*/*", "application/json", "*/*", "application/openmetrics-text")).second shouldBe 111 | TextFormat.CONTENT_TYPE_OPENMETRICS_100 112 | shouldThrow { 113 | mc.getMetrics(listOf("application/something", "application/something-else")) 114 | } 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /jicoco-mucclient/checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /jicoco-mucclient/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 21 | 4.0.0 22 | 23 | 24 | org.jitsi 25 | jicoco-parent 26 | 1.1-SNAPSHOT 27 | 28 | 29 | jicoco-mucclient 30 | 1.1-SNAPSHOT 31 | jicoco-mucclient 32 | Jitsi Common Components - MucClient 33 | 34 | 35 | 36 | org.jetbrains.kotlin 37 | kotlin-stdlib-jdk8 38 | 39 | 40 | org.igniterealtime.smack 41 | smack-core 42 | ${smack.version} 43 | 44 | 45 | org.igniterealtime.smack 46 | smack-extensions 47 | ${smack.version} 48 | 49 | 50 | org.igniterealtime.smack 51 | smack-tcp 52 | ${smack.version} 53 | 54 | 55 | org.igniterealtime.smack 56 | smack-xmlparser-stax 57 | ${smack.version} 58 | 59 | 60 | 61 | ${project.groupId} 62 | jitsi-utils 63 | 64 | 65 | ${project.groupId} 66 | jicoco-config 67 | ${project.version} 68 | 69 | 70 | 71 | 72 | org.junit.platform 73 | junit-platform-launcher 74 | 1.10.0 75 | test 76 | 77 | 78 | org.junit.jupiter 79 | junit-jupiter-api 80 | ${junit.version} 81 | test 82 | 83 | 84 | org.junit.jupiter 85 | junit-jupiter-engine 86 | ${junit.version} 87 | test 88 | 89 | 90 | org.glassfish.jersey.test-framework 91 | jersey-test-framework-core 92 | ${jersey.version} 93 | test 94 | 95 | 96 | junit 97 | junit 98 | 99 | 100 | 101 | 102 | org.glassfish.jersey.test-framework.providers 103 | jersey-test-framework-provider-jetty 104 | ${jersey.version} 105 | test 106 | 107 | 108 | junit 109 | junit 110 | 111 | 112 | 113 | 114 | org.glassfish.jersey.test-framework.providers 115 | jersey-test-framework-provider-grizzly2 116 | ${jersey.version} 117 | test 118 | 119 | 120 | junit 121 | junit 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | org.jetbrains.kotlin 130 | kotlin-maven-plugin 131 | ${kotlin.version} 132 | 133 | 134 | compile 135 | compile 136 | 137 | compile 138 | 139 | 140 | 141 | src/main/kotlin 142 | src/main/java 143 | 144 | 145 | 146 | 147 | test-compile 148 | test-compile 149 | 150 | test-compile 151 | 152 | 153 | 154 | src/test/kotlin 155 | src/test/java 156 | 157 | 158 | 159 | 160 | 161 | 11 162 | 163 | 164 | 165 | org.apache.maven.plugins 166 | maven-compiler-plugin 167 | 3.10.1 168 | 169 | 170 | default-compile 171 | none 172 | 173 | 174 | default-testCompile 175 | none 176 | 177 | 178 | java-compile 179 | compile 180 | 181 | compile 182 | 183 | 184 | 185 | java-test-compile 186 | test-compile 187 | 188 | testCompile 189 | 190 | 191 | 192 | 193 | 11 194 | 195 | -Xlint:all,-serial 196 | 197 | 198 | 199 | 200 | org.apache.maven.plugins 201 | maven-checkstyle-plugin 202 | 3.1.2 203 | 204 | checkstyle.xml 205 | 206 | 207 | 208 | com.puppycrawl.tools 209 | checkstyle 210 | 10.1 211 | 212 | 213 | 214 | 215 | 216 | check 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | -------------------------------------------------------------------------------- /jicoco-mucclient/src/main/java/org/jitsi/retry/RetryStrategy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2015 - present, 8x8 Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.jitsi.retry; 17 | 18 | import org.jitsi.utils.logging.*; 19 | 20 | import java.util.concurrent.*; 21 | 22 | /** 23 | * A retry strategy for doing some job. It allows to specify initial delay 24 | * before the task is started as well as retry delay. It will be executed as 25 | * long as the task Callable returns true. It is 26 | * also possible to configure whether the retries should be continued after 27 | * unexpected exception or not. 28 | * If we decide to not continue retries from outside the task 29 | * {@link RetryStrategy#cancel()} method will prevent from scheduling future 30 | * retries(but it will not interrupt currently executing one). Check with 31 | * {@link RetryStrategy#isCancelled()} to stop the operation in progress. 32 | * 33 | * See "RetryStrategyTest" for usage samples. 34 | * 35 | * @author Pawel Domas 36 | */ 37 | public class RetryStrategy 38 | { 39 | /** 40 | * The logger 41 | */ 42 | private final static Logger logger = Logger.getLogger(RetryStrategy.class); 43 | 44 | /** 45 | * Scheduled executor service used to schedule retry task. 46 | */ 47 | private final ScheduledExecutorService executor; 48 | 49 | /** 50 | * RetryTask instance which describes the retry task and provides 51 | * things like retry interval and callable method to be executed. 52 | */ 53 | private RetryTask task; 54 | 55 | /** 56 | * Future instance used to eventually cancel the retry task. 57 | */ 58 | private ScheduledFuture future; 59 | 60 | /** 61 | * Inner class implementing Runnable that does additional 62 | * processing around Callable retry job. 63 | */ 64 | private final TaskRunner taskRunner = new TaskRunner(); 65 | 66 | 67 | /** 68 | * Creates new RetryStrategy instance that will use 69 | * ScheduledExecutorService with pool size of 1 thread to schedule 70 | * retry attempts. 71 | */ 72 | public RetryStrategy() 73 | { 74 | this(Executors.newScheduledThreadPool(1)); 75 | } 76 | 77 | /** 78 | * Creates new instance of RetryStrategy that will use given 79 | * ScheduledExecutorService to schedule retry attempts. 80 | * 81 | * @param retryExecutor ScheduledExecutorService that will be used 82 | * for scheduling retry attempts. 83 | * 84 | * @throws NullPointerException if given retryExecutor is 85 | * null 86 | */ 87 | public RetryStrategy(ScheduledExecutorService retryExecutor) 88 | { 89 | if (retryExecutor == null) 90 | { 91 | throw new NullPointerException("executor"); 92 | } 93 | 94 | this.executor = retryExecutor; 95 | } 96 | 97 | /** 98 | * Cancels any future retry attempts. Currently running tasks are not 99 | * interrupted. 100 | */ 101 | synchronized public void cancel() 102 | { 103 | if (future != null) 104 | { 105 | future.cancel(false); 106 | future = null; 107 | } 108 | 109 | task.setCancelled(true); 110 | } 111 | 112 | /** 113 | * Returns true if this retry strategy has been cancelled or 114 | * false otherwise. 115 | */ 116 | synchronized public boolean isCancelled() 117 | { 118 | return task != null && task.isCancelled(); 119 | } 120 | 121 | /** 122 | * Start given RetryTask that will be executed for the first time 123 | * after {@link RetryTask#getInitialDelay()}. After first execution next 124 | * retry attempts will be rescheduled as long as it's callable method 125 | * returns true or until ({@link #cancel()} is called. 126 | * 127 | * @param task the retry task to be employed by this retry strategy instance 128 | */ 129 | synchronized public void runRetryingTask(final RetryTask task) 130 | { 131 | if (task == null) 132 | throw new NullPointerException("task"); 133 | 134 | this.task = task; 135 | this.future 136 | = executor.schedule( 137 | taskRunner, 138 | task.getInitialDelay(), 139 | TimeUnit.MILLISECONDS); 140 | } 141 | 142 | /** 143 | * Schedules new retry attempt if we d 144 | */ 145 | synchronized private void scheduleRetry() 146 | { 147 | if (task == null || task.isCancelled()) 148 | return; 149 | 150 | this.future 151 | = executor.schedule( 152 | taskRunner, 153 | task.getRetryDelay(), 154 | TimeUnit.MILLISECONDS); 155 | } 156 | 157 | /** 158 | * Some extra processing around running retry callable. 159 | */ 160 | class TaskRunner implements Runnable 161 | { 162 | @Override 163 | public void run() 164 | { 165 | try 166 | { 167 | if (task.getCallable().call()) 168 | scheduleRetry(); 169 | } 170 | catch (Exception e) 171 | { 172 | logger.error(e, e); 173 | 174 | if (task.willRetryAfterException()) 175 | scheduleRetry(); 176 | } 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /jicoco-mucclient/src/main/java/org/jitsi/retry/RetryTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2015 - present, 8x8 Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.jitsi.retry; 17 | 18 | import java.util.concurrent.*; 19 | 20 | /** 21 | * Class describes a retry task executed by {@link RetryStrategy}. 22 | * It has the following properties: 23 | *

  • {@link #initialDelay} - specifies the time before this task is launched 24 | * for the first time
  • 25 | *
  • {@link #retryDelay} - tells how much time we wait before next retry 26 | * attempt. Subclass can override {@link #getRetryDelay()} in order to provide 27 | * dynamic value which can be different for each retry
  • 28 | *
  • {@link #getCallable()}
  • - a Callable which is 29 | * the job to be executed by retry strategy. The task will be retried as long as 30 | * it returns true or until the job is cancelled. 31 | *
  • {@link #retryAfterException}
  • - indicates if retries should be 32 | * continued after uncaught exception is thrown by retry callable task 33 | *
  • {@link #cancelled}
  • - indicates if {@link RetryStrategy} and this 34 | * task has been cancelled using {@link RetryStrategy#cancel()}. This does not 35 | * interrupt currently executing task. 36 | * 37 | * @author Pawel Domas 38 | */ 39 | public abstract class RetryTask 40 | { 41 | /** 42 | * Value in ms. Specifies the time before this task is launched for 43 | * the first time. 44 | */ 45 | private final long initialDelay; 46 | 47 | /** 48 | * Value in ms. Tells how much time we wait before next retry attempt. 49 | */ 50 | private final long retryDelay; 51 | 52 | /** 53 | * Indicates if retries should be continued after uncaught exception is 54 | * thrown by retry callable task. 55 | */ 56 | private boolean retryAfterException; 57 | 58 | /** 59 | * Indicates if {@link RetryStrategy} and this task has been cancelled using 60 | * {@link RetryStrategy#cancel()}. This does not interrupt currently 61 | * executing task. 62 | */ 63 | private boolean cancelled; 64 | 65 | /** 66 | * Initializes new instance of RetryTask. 67 | * @param initialDelay how long we're going to wait before running task 68 | * callable for the first time(in ms). 69 | * @param retryDelay how often are we going to retry(in ms). 70 | * @param retryOnException should we continue retry after callable throws 71 | * unexpected Exception. 72 | */ 73 | public RetryTask(long initialDelay, 74 | long retryDelay, 75 | boolean retryOnException) 76 | { 77 | this.initialDelay = initialDelay; 78 | this.retryDelay = retryDelay; 79 | this.retryAfterException = retryOnException; 80 | } 81 | 82 | /** 83 | * Returns the time in ms before this task is launched for the first time. 84 | */ 85 | public long getInitialDelay() 86 | { 87 | return initialDelay; 88 | } 89 | 90 | /** 91 | * Returns the delay in ms that we wait before next retry attempt. 92 | */ 93 | public long getRetryDelay() 94 | { 95 | return retryDelay; 96 | } 97 | 98 | /** 99 | * Returns a Callable which is the job to be executed 100 | * by retry strategy. The task will be retried as long as it returns 101 | * true or until the job is cancelled. 102 | */ 103 | abstract public Callable getCallable(); 104 | 105 | /** 106 | * Indicates if we're going to continue retry task scheduling after the 107 | * callable throws unexpected exception. 108 | */ 109 | public boolean willRetryAfterException() 110 | { 111 | return retryAfterException; 112 | } 113 | 114 | /** 115 | * Should we continue retries after the callable throws unexpected exception 116 | * ? 117 | * @param retryAfterException true to continue retries even though 118 | * unexpected exception is thrown by the callable, otherwise retry 119 | * strategy will be cancelled when that happens. 120 | */ 121 | public void setRetryAfterException(boolean retryAfterException) 122 | { 123 | this.retryAfterException = retryAfterException; 124 | } 125 | 126 | /** 127 | * Returns true if this task has been cancelled. 128 | */ 129 | public boolean isCancelled() 130 | { 131 | return cancelled; 132 | } 133 | 134 | /** 135 | * Method is called by RetryStrategy when it gets cancelled. 136 | * @param cancelled true when this task is being cancelled. 137 | */ 138 | public void setCancelled(boolean cancelled) 139 | { 140 | this.cancelled = cancelled; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /jicoco-mucclient/src/main/java/org/jitsi/retry/SimpleRetryTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2015 - present, 8x8 Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.jitsi.retry; 17 | 18 | import java.util.concurrent.*; 19 | 20 | /** 21 | * Simple implementation of {@link #getCallable()} which stores callable method 22 | * in the constructor. 23 | * 24 | * @author Pawel Domas 25 | */ 26 | public class SimpleRetryTask 27 | extends RetryTask 28 | { 29 | /** 30 | * Retry job callable to be executed on each retry attempt. 31 | */ 32 | protected Callable retryJob; 33 | 34 | /** 35 | * Initializes new instance of SimpleRetryTask. 36 | * 37 | * @param initialDelay how long we're going to wait before running task 38 | * callable for the first time(in ms). 39 | * @param retryDelay how often are we going to retry(in ms). 40 | * @param retryOnException should we continue retry after callable throws 41 | * unexpected Exception. 42 | * @param retryJob the callable job to be executed on retry. 43 | */ 44 | public SimpleRetryTask(long initialDelay, 45 | long retryDelay, 46 | boolean retryOnException, 47 | Callable retryJob) 48 | { 49 | super(initialDelay, retryDelay, retryOnException); 50 | 51 | this.retryJob = retryJob; 52 | } 53 | 54 | /** 55 | * {@inheritDoc} 56 | */ 57 | @Override 58 | public Callable getCallable() 59 | { 60 | return retryJob; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /jicoco-mucclient/src/main/java/org/jitsi/xmpp/TrustAllHostnameVerifier.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present, 8x8 Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.jitsi.xmpp; 18 | 19 | import javax.net.ssl.HostnameVerifier; 20 | import javax.net.ssl.SSLSession; 21 | 22 | /** 23 | * @author bbaldino 24 | */ 25 | public class TrustAllHostnameVerifier implements HostnameVerifier 26 | { 27 | @Override 28 | public boolean verify(String s, SSLSession sslSession) 29 | { 30 | return true; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /jicoco-mucclient/src/main/java/org/jitsi/xmpp/TrustAllX509TrustManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present, 8x8 Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.jitsi.xmpp; 18 | 19 | import javax.net.ssl.X509TrustManager; 20 | import java.security.cert.X509Certificate; 21 | 22 | /** 23 | * @author bbaldino 24 | */ 25 | public class TrustAllX509TrustManager implements X509TrustManager { 26 | @Override 27 | public void checkClientTrusted(X509Certificate[] c, String s) 28 | { 29 | } 30 | 31 | @Override 32 | public void checkServerTrusted(X509Certificate[] c, String s) 33 | { 34 | } 35 | 36 | @Override 37 | public X509Certificate[] getAcceptedIssuers() 38 | { 39 | return new X509Certificate[0]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /jicoco-mucclient/src/main/java/org/jitsi/xmpp/mucclient/ConnectionStateListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2022 - present, 8x8 Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.jitsi.xmpp.mucclient; 18 | 19 | import org.jetbrains.annotations.*; 20 | 21 | public interface ConnectionStateListener 22 | { 23 | void connected(@NotNull MucClient mucClient); 24 | 25 | void closed(@NotNull MucClient mucClient); 26 | 27 | void closedOnError(@NotNull MucClient mucClient); 28 | 29 | void reconnecting(@NotNull MucClient mucClient); 30 | 31 | void reconnectionFailed(@NotNull MucClient mucClient); 32 | 33 | void pingFailed(@NotNull MucClient mucClient); 34 | } 35 | -------------------------------------------------------------------------------- /jicoco-mucclient/src/main/java/org/jitsi/xmpp/mucclient/IQListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present, 8x8 Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.jitsi.xmpp.mucclient; 18 | 19 | import org.jivesoftware.smack.packet.*; 20 | 21 | /** 22 | * An interface for handling IQs coming from a specific {@link MucClient}. 23 | * 24 | * @author Boris Grozev 25 | */ 26 | public interface IQListener 27 | { 28 | /** 29 | * Handles an IQ. Default implementation which ignores the {@link MucClient} 30 | * which from which the IQ comes. 31 | * 32 | * @param iq the IQ to be handled. 33 | * @return the IQ to be sent as a response or {@code null}. 34 | */ 35 | default IQ handleIq(IQ iq) 36 | { 37 | return null; 38 | } 39 | 40 | /** 41 | * Handles an IQ. Default implementation which ignores the {@link MucClient} 42 | * which from which the IQ comes. 43 | * 44 | * @param iq the IQ to be handled. 45 | * @param mucClient the {@link MucClient} from which the IQ comes. 46 | * @return the IQ to be sent as a response or {@code null}. 47 | */ 48 | default IQ handleIq(IQ iq, MucClient mucClient) 49 | { 50 | return handleIq(iq); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /jicoco-mucclient/src/main/kotlin/org/jitsi/xmpp/util/ErrorUtil.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2015 - present, 8x8 Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.jitsi.xmpp.util 17 | 18 | import org.jivesoftware.smack.packet.ExtensionElement 19 | import org.jivesoftware.smack.packet.IQ 20 | import org.jivesoftware.smack.packet.StanzaError 21 | 22 | @JvmOverloads 23 | fun createError( 24 | request: IQ, 25 | errorCondition: StanzaError.Condition, 26 | errorMessage: String? = null, 27 | extension: ExtensionElement? = null 28 | ) = createError( 29 | request, 30 | errorCondition, 31 | errorMessage, 32 | if (extension == null) emptyList() else listOf(extension) 33 | ) 34 | 35 | /** 36 | * Create an error response for a given IQ request. 37 | * 38 | * @param request the request IQ for which the error response will be created. 39 | * @param errorCondition the XMPP error condition for the error response. 40 | * @param errorMessage optional error text message to be included in the error response. 41 | * @param extensions optional extensions to include as a children of the error element. 42 | * 43 | * @return an IQ which is an XMPP error response to given request. 44 | */ 45 | fun createError( 46 | request: IQ, 47 | errorCondition: StanzaError.Condition, 48 | errorMessage: String? = null, 49 | extensions: List 50 | ): IQ { 51 | val error = StanzaError.getBuilder(errorCondition) 52 | errorMessage?.let { error.setDescriptiveEnText(it) } 53 | if (extensions.isNotEmpty()) { 54 | error.setExtensions(extensions) 55 | } 56 | 57 | return IQ.createErrorResponse(request, error.build()) 58 | } 59 | -------------------------------------------------------------------------------- /jicoco-mucclient/src/test/java/org/jitsi/retry/RetryStrategyTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2015 - present, 8x8 Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.jitsi.retry; 17 | 18 | import java.time.*; 19 | import java.util.concurrent.*; 20 | import org.jitsi.utils.concurrent.*; 21 | import org.junit.jupiter.api.*; 22 | 23 | import static org.junit.jupiter.api.Assertions.*; 24 | 25 | public class RetryStrategyTest 26 | { 27 | @Test 28 | public void testRetryCount() 29 | { 30 | FakeScheduledExecutorService fakeExecutor = new FakeScheduledExecutorService(); 31 | RetryStrategy retryStrategy = new RetryStrategy(fakeExecutor); 32 | 33 | long initialDelay = 150L; 34 | long retryDelay = 50L; 35 | int targetRetryCount = 3; 36 | 37 | TestCounterTask retryTask 38 | = new TestCounterTask( 39 | initialDelay, retryDelay, false, targetRetryCount); 40 | 41 | retryStrategy.runRetryingTask(retryTask); 42 | fakeExecutor.run(); 43 | 44 | // Check if the task has not been executed before initial delay 45 | assertEquals(0, retryTask.counter); 46 | 47 | fakeExecutor.getClock().elapse(Duration.ofMillis(initialDelay + 10L)); 48 | fakeExecutor.run(); 49 | 50 | // Should be 1 after 1st pass 51 | assertEquals(1, retryTask.counter); 52 | 53 | // Now sleep two time retry delay 54 | fakeExecutor.getClock().elapse(Duration.ofMillis(retryDelay + 10L)); 55 | fakeExecutor.run(); 56 | fakeExecutor.getClock().elapse(Duration.ofMillis(retryDelay + 10L)); 57 | fakeExecutor.run(); 58 | assertEquals(3, retryTask.counter); 59 | 60 | // Sleep a bit more to check if it has stopped 61 | fakeExecutor.getClock().elapse(Duration.ofMillis(retryDelay + 10L)); 62 | fakeExecutor.run(); 63 | assertEquals(3, retryTask.counter); 64 | } 65 | 66 | @Test 67 | public void testRetryWithException() 68 | { 69 | FakeScheduledExecutorService fakeExecutor = new FakeScheduledExecutorService(); 70 | RetryStrategy retryStrategy = new RetryStrategy(fakeExecutor); 71 | 72 | long initialDelay = 30L; 73 | long retryDelay = 50L; 74 | int targetRetryCount = 3; 75 | 76 | TestCounterTask retryTask 77 | = new TestCounterTask( 78 | initialDelay, retryDelay, false, targetRetryCount); 79 | 80 | // Should throw an Exception on 2nd pass and stop 81 | retryTask.exceptionOnCount = 1; 82 | 83 | retryStrategy.runRetryingTask(retryTask); 84 | 85 | fakeExecutor.getClock().elapse(Duration.ofMillis(initialDelay + 10L)); 86 | fakeExecutor.run(); 87 | for (int i = 0; i < 3; i++) 88 | { 89 | fakeExecutor.getClock().elapse(Duration.ofMillis(retryDelay + 10L)); 90 | fakeExecutor.run(); 91 | } 92 | 93 | assertEquals(1, retryTask.counter); 94 | 95 | // Now modify strategy to not cancel on exception 96 | retryTask.reset(); 97 | 98 | // Check if reset worked 99 | assertEquals(0, retryTask.counter); 100 | 101 | // Will fail at count = 1, but should continue 102 | retryTask.exceptionOnCount = 1; 103 | retryTask.setRetryAfterException(true); 104 | 105 | retryStrategy.runRetryingTask(retryTask); 106 | 107 | 108 | fakeExecutor.getClock().elapse(Duration.ofMillis(initialDelay + 10L)); 109 | fakeExecutor.run(); 110 | for (int i = 0; i < 4; i++) 111 | { 112 | fakeExecutor.getClock().elapse(Duration.ofMillis(retryDelay + 10L)); 113 | fakeExecutor.run(); 114 | } 115 | 116 | assertEquals(3, retryTask.counter); 117 | } 118 | 119 | @Test 120 | public void testCancel() 121 | { 122 | FakeScheduledExecutorService fakeExecutor = new FakeScheduledExecutorService(); 123 | RetryStrategy retryStrategy = new RetryStrategy(fakeExecutor); 124 | 125 | long initialDelay = 30L; 126 | long retryDelay = 50L; 127 | int targetRetryCount = 3; 128 | 129 | TestCounterTask retryTask 130 | = new TestCounterTask( 131 | initialDelay, retryDelay, false, targetRetryCount); 132 | 133 | retryStrategy.runRetryingTask(retryTask); 134 | 135 | fakeExecutor.getClock().elapse(Duration.ofMillis(initialDelay + 10L)); 136 | fakeExecutor.run(); 137 | 138 | retryStrategy.cancel(); 139 | 140 | assertEquals(1, retryTask.counter); 141 | 142 | fakeExecutor.getClock().elapse(Duration.ofMillis(retryDelay)); 143 | fakeExecutor.run(); 144 | fakeExecutor.getClock().elapse(Duration.ofMillis(retryDelay)); 145 | fakeExecutor.run(); 146 | 147 | assertEquals(1, retryTask.counter); 148 | } 149 | 150 | private static class TestCounterTask 151 | extends RetryTask 152 | { 153 | int counter; 154 | 155 | int targetRetryCount; 156 | 157 | int exceptionOnCount = -1; 158 | 159 | public TestCounterTask(long initialDelay, 160 | long retryDelay, 161 | boolean retryOnException, 162 | int targetRetryCount) 163 | { 164 | super(initialDelay, retryDelay, retryOnException); 165 | 166 | this.targetRetryCount = targetRetryCount; 167 | } 168 | 169 | public void reset() 170 | { 171 | counter = 0; 172 | exceptionOnCount = -1; 173 | } 174 | 175 | @Override 176 | public Callable getCallable() 177 | { 178 | return () -> 179 | { 180 | if (exceptionOnCount == counter) 181 | { 182 | // Will not throw on next attempt 183 | exceptionOnCount = -1; 184 | // Throw error 185 | throw new Exception("Simulated error in retry job"); 186 | } 187 | 188 | // Retry as long as the counter stays below the target 189 | return ++counter < targetRetryCount; 190 | }; 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /jicoco-test-kotlin/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 21 | 4.0.0 22 | 23 | 24 | org.jitsi 25 | jicoco-parent 26 | 1.1-SNAPSHOT 27 | 28 | 29 | jicoco-test-kotlin 30 | 1.1-SNAPSHOT 31 | jicoco-test-kotlin 32 | Jitsi Common Components (Kotlin Test Utilities) 33 | 34 | 35 | 36 | ${project.groupId} 37 | jicoco-config 38 | ${project.version} 39 | 40 | 41 | com.typesafe 42 | config 43 | 1.4.2 44 | 45 | 46 | io.mockk 47 | mockk 48 | ${mockk.version} 49 | 50 | 51 | org.jetbrains.kotlin 52 | kotlin-stdlib-jdk8 53 | 54 | 55 | io.kotest 56 | kotest-runner-junit5-jvm 57 | ${kotest.version} 58 | test 59 | 60 | 61 | io.kotest 62 | kotest-assertions-core-jvm 63 | ${kotest.version} 64 | test 65 | 66 | 67 | 68 | 69 | src/main/kotlin 70 | src/test/kotlin 71 | 72 | 73 | org.jetbrains.kotlin 74 | kotlin-maven-plugin 75 | ${kotlin.version} 76 | 77 | 78 | compile 79 | compile 80 | 81 | compile 82 | 83 | 84 | 85 | test-compile 86 | test-compile 87 | 88 | test-compile 89 | 90 | 91 | 92 | 93 | 11 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /jicoco-test-kotlin/src/main/kotlin/org/jitsi/config/ConfigTestHelpers.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.config 18 | 19 | import com.typesafe.config.ConfigFactory 20 | import java.io.StringReader 21 | import java.util.Properties 22 | 23 | /** 24 | * Execute the given [block] using the props defined by [props] as a legacy 25 | * [org.jitsi.metaconfig.ConfigSource] with name [name]. Resets the legacy 26 | * config to empty after [block] is executed. 27 | */ 28 | inline fun withLegacyConfig(props: String, name: String = "legacy", block: () -> Unit) { 29 | setLegacyConfig(props = props, name = name) 30 | block() 31 | setLegacyConfig("") 32 | } 33 | 34 | /** 35 | * Execute the given [block] using the config defined by [config] as a new 36 | * [org.jitsi.metaconfig.ConfigSource], falling back to the defaults if 37 | * [loadDefaults] is true, with name [name]. Resets the new config to empty 38 | * after [block] is executed. 39 | */ 40 | inline fun withNewConfig(config: String, name: String = "new", loadDefaults: Boolean = true, block: () -> Unit) { 41 | setNewConfig(config, loadDefaults, name) 42 | block() 43 | setNewConfig("", true) 44 | } 45 | 46 | /** 47 | * Creates a [TypesafeConfigSource] using the parsed value of [config] and 48 | * defaults in reference.conf if [loadDefaults] is set with name [name] and 49 | * sets it as the underlying source of [JitsiConfig.newConfig] 50 | */ 51 | fun setNewConfig(config: String, loadDefaults: Boolean, name: String = "new") { 52 | JitsiConfig.useDebugNewConfig( 53 | TypesafeConfigSource( 54 | name, 55 | ConfigFactory.parseString(config).run { if (loadDefaults) withFallback(ConfigFactory.load()) else this } 56 | ) 57 | ) 58 | } 59 | 60 | /** 61 | * Creates a [ReadOnlyConfigurationService] using the parsed value of [props] 62 | * with name [name] and sets it as the underlying source of [JitsiConfig.legacyConfig] 63 | */ 64 | fun setLegacyConfig(props: String, name: String = "legacy") { 65 | JitsiConfig.useDebugLegacyConfig( 66 | ConfigurationServiceConfigSource( 67 | name, 68 | TestReadOnlyConfigurationService(Properties().apply { load(StringReader(props)) }) 69 | ) 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /jicoco-test-kotlin/src/main/kotlin/org/jitsi/config/TestReadOnlyConfigurationService.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.config 18 | 19 | import java.util.Properties 20 | 21 | class TestReadOnlyConfigurationService( 22 | override var properties: Properties = Properties() 23 | ) : AbstractReadOnlyConfigurationService() { 24 | 25 | val props: Properties 26 | get() = properties 27 | 28 | override fun reloadConfiguration() {} 29 | } 30 | --------------------------------------------------------------------------------