diff --git a/jicofo-common/pom.xml b/jicofo-common/pom.xml index eee964aeb8..9331feef72 100644 --- a/jicofo-common/pom.xml +++ b/jicofo-common/pom.xml @@ -24,10 +24,6 @@ - - org.eclipse.jetty - jetty-server - org.igniterealtime.smack smack-tcp @@ -75,22 +71,6 @@ org.slf4j slf4j-api - - org.glassfish.jersey.containers - jersey-container-jetty-http - - - org.glassfish.jersey.containers - jersey-container-servlet - - - org.glassfish.jersey.inject - jersey-hk2 - - - org.glassfish.jersey.media - jersey-media-json-jackson - com.fasterxml.jackson.module jackson-module-kotlin @@ -123,39 +103,6 @@ mockito-core test - - org.glassfish.jersey.test-framework - jersey-test-framework-core - test - - - junit - junit - - - - - org.glassfish.jersey.test-framework.providers - jersey-test-framework-provider-jetty - test - - - junit - junit - - - - - org.glassfish.jersey.test-framework.providers - jersey-test-framework-provider-grizzly2 - test - - - junit - junit - - - io.kotest kotest-runner-junit5-jvm diff --git a/jicofo-selector/pom.xml b/jicofo-selector/pom.xml index eaa53d8920..140e9f9b01 100644 --- a/jicofo-selector/pom.xml +++ b/jicofo-selector/pom.xml @@ -29,10 +29,6 @@ - - org.eclipse.jetty - jetty-server - org.igniterealtime.smack smack-tcp @@ -80,22 +76,6 @@ org.slf4j slf4j-api - - org.glassfish.jersey.containers - jersey-container-jetty-http - - - org.glassfish.jersey.containers - jersey-container-servlet - - - org.glassfish.jersey.inject - jersey-hk2 - - - org.glassfish.jersey.media - jersey-media-json-jackson - com.github.spotbugs @@ -123,39 +103,6 @@ mockito-core test - - org.glassfish.jersey.test-framework - jersey-test-framework-core - test - - - junit - junit - - - - - org.glassfish.jersey.test-framework.providers - jersey-test-framework-provider-jetty - test - - - junit - junit - - - - - org.glassfish.jersey.test-framework.providers - jersey-test-framework-provider-grizzly2 - test - - - junit - junit - - - io.kotest kotest-runner-junit5-jvm diff --git a/jicofo-selector/src/main/resources/reference.conf b/jicofo-selector/src/main/resources/reference.conf index fcba5ec292..de8532a959 100644 --- a/jicofo-selector/src/main/resources/reference.conf +++ b/jicofo-selector/src/main/resources/reference.conf @@ -330,8 +330,9 @@ jicofo { } rest { + enabled = true port = 8888 - tls-port = 8843 + host = "0.0.0.0" prometheus { // Enable the prometheus /metrics endpoint. @@ -345,6 +346,12 @@ jicofo { // Enable the move-endpoint API. enabled = true } + debug { + enabled = true + } + pin { + enabled = true + } } sctp { diff --git a/jicofo/pom.xml b/jicofo/pom.xml index d5e80586c3..12697e7adb 100644 --- a/jicofo/pom.xml +++ b/jicofo/pom.xml @@ -39,8 +39,29 @@ commons-lang3 - org.eclipse.jetty - jetty-server + io.ktor + ktor-server-core-jvm + ${ktor.version} + + + io.ktor + ktor-server-netty-jvm + ${ktor.version} + + + io.ktor + ktor-server-content-negotiation-jvm + ${ktor.version} + + + io.ktor + ktor-serialization-jackson-jvm + ${ktor.version} + + + io.ktor + ktor-server-status-pages-jvm + ${ktor.version} org.igniterealtime.smack @@ -93,22 +114,6 @@ org.slf4j slf4j-api - - org.glassfish.jersey.containers - jersey-container-jetty-http - - - org.glassfish.jersey.containers - jersey-container-servlet - - - org.glassfish.jersey.inject - jersey-hk2 - - - org.glassfish.jersey.media - jersey-media-json-jackson - com.fasterxml.jackson.module jackson-module-kotlin @@ -146,39 +151,6 @@ mockk-jvm test - - org.glassfish.jersey.test-framework - jersey-test-framework-core - test - - - junit - junit - - - - - org.glassfish.jersey.test-framework.providers - jersey-test-framework-provider-jetty - test - - - junit - junit - - - - - org.glassfish.jersey.test-framework.providers - jersey-test-framework-provider-grizzly2 - test - - - junit - junit - - - io.kotest kotest-runner-junit5-jvm diff --git a/jicofo/src/main/java/org/jitsi/jicofo/rest/Application.java b/jicofo/src/main/java/org/jitsi/jicofo/rest/Application.java deleted file mode 100644 index cd340c747c..0000000000 --- a/jicofo/src/main/java/org/jitsi/jicofo/rest/Application.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright @ 2018 - present 8x8, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.jitsi.jicofo.rest; - -import org.glassfish.hk2.utilities.binding.*; -import org.glassfish.jersey.server.*; - -import java.time.*; -import java.util.*; - -/** - * Adds the configuration for the REST web endpoints. - */ -public class Application - extends ResourceConfig -{ - protected final Clock clock = Clock.systemUTC(); - - public Application(List components) - { - register(new AbstractBinder() - { - @Override - protected void configure() - { - bind(clock).to(Clock.class); - } - }); - packages("org.jitsi.jicofo.rest"); - - components.forEach(this::register); - } -} diff --git a/jicofo/src/main/java/org/jitsi/jicofo/rest/Debug.java b/jicofo/src/main/java/org/jitsi/jicofo/rest/Debug.java deleted file mode 100644 index af8e4dcdcc..0000000000 --- a/jicofo/src/main/java/org/jitsi/jicofo/rest/Debug.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright @ 2018 - present 8x8, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.jitsi.jicofo.rest; - -import org.jetbrains.annotations.*; -import org.jitsi.jicofo.*; -import org.jitsi.jicofo.conference.*; -import org.jitsi.jicofo.xmpp.*; -import org.jitsi.utils.*; -import org.json.simple.*; - -import jakarta.ws.rs.*; -import jakarta.ws.rs.core.MediaType; -import java.util.*; - -/** - * An interface which exposes detailed internal state for the purpose of debugging. - */ -@Path("/debug") -public class Debug -{ - @NotNull - private final JicofoServices jicofoServices - = Objects.requireNonNull(JicofoServices.getJicofoServicesSingleton(), "jicofoServices"); - - /** - * Returns json string with statistics. - * @return json string with statistics. - */ - @GET - @Produces(MediaType.APPLICATION_JSON) - @NotNull - public String getDebug(@DefaultValue("false") @QueryParam("full") boolean full) - { - return jicofoServices.getDebugState(full).toJSONString(); - } - - @GET - @Path("conference/{confId}") - @Produces(MediaType.APPLICATION_JSON) - @NotNull - public String confDebug(@PathParam("confId") String confId) - { - OrderedJsonObject confJson = jicofoServices.getConferenceDebugState(confId); - return confJson.toJSONString(); - } - - @GET - @Path("xmpp-caps") - @Produces(MediaType.APPLICATION_JSON) - @NotNull - public String xmppCaps() - { - return XmppCapsStats.getStats().toJSONString(); - } - - @GET - @Path("/conferences") - @Produces(MediaType.APPLICATION_JSON) - @NotNull - @SuppressWarnings("unchecked") - public String conferences() - { - JSONArray conferencesJson = new JSONArray(); - for (JitsiMeetConference c : jicofoServices.getFocusManager().getAllConferences()) - { - conferencesJson.add(c.getRoomName().toString()); - } - return conferencesJson.toJSONString(); - } - - @GET - @Path("/conferences-full") - @Produces(MediaType.APPLICATION_JSON) - @NotNull - @SuppressWarnings("unchecked") - public String conferencesFull() - { - JSONObject conferencesJson = new JSONObject(); - for (JitsiMeetConference c : jicofoServices.getFocusManager().getAllConferences()) - { - conferencesJson.put(c.getRoomName().toString(), c.getDebugState()); - } - return conferencesJson.toJSONString(); - } -} diff --git a/jicofo/src/main/java/org/jitsi/jicofo/rest/Pin.java b/jicofo/src/main/java/org/jitsi/jicofo/rest/Pin.java deleted file mode 100644 index 1c922b347d..0000000000 --- a/jicofo/src/main/java/org/jitsi/jicofo/rest/Pin.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright @ 2018 - present 8x8, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.jitsi.jicofo.rest; - -import com.fasterxml.jackson.annotation.*; -import org.eclipse.jetty.http.*; -import org.jetbrains.annotations.*; -import org.jitsi.jicofo.*; -import org.jxmpp.jid.*; -import org.jxmpp.jid.impl.*; -import org.jxmpp.stringprep.*; - -import jakarta.servlet.http.*; -import jakarta.ws.rs.*; -import jakarta.ws.rs.core.*; -import java.time.*; -import java.util.*; - -/** - * A resource for pinning conferences to a specific bridge version. - */ -@Path("/pin") -public class Pin -{ - @NotNull - private final JicofoServices jicofoServices - = Objects.requireNonNull(JicofoServices.getJicofoServicesSingleton(), "jicofoServices"); - - @GET - @Produces(MediaType.APPLICATION_JSON) - public String getPins() - { - return jicofoServices.getFocusManager().getPinnedConferencesJson().toJSONString(); - } - - @POST - @Consumes(MediaType.APPLICATION_JSON) - public Response pin(PinJson pinJson, @Context HttpServletRequest request) - { - try - { - EntityBareJid conferenceJid = JidCreate.entityBareFrom(pinJson.conferenceId); - jicofoServices.getFocusManager().pinConference(conferenceJid, pinJson.jvbVersion, - Duration.ofMinutes(pinJson.minutes)); - return Response.ok().build(); - } - catch (XmppStringprepException x) - { - return Response.status(HttpStatus.BAD_REQUEST_400).build(); - } - catch (Throwable t) - { - return Response.status(HttpStatus.INTERNAL_SERVER_ERROR_500).build(); - } - } - - @Path("/remove") - @POST - @Consumes(MediaType.APPLICATION_JSON) - public Response unpin(UnpinJson unpinJson, @Context HttpServletRequest request) - { - try - { - EntityBareJid conferenceJid = JidCreate.entityBareFrom(unpinJson.conferenceId); - jicofoServices.getFocusManager().unpinConference(conferenceJid); - return Response.ok().build(); - } - catch (XmppStringprepException x) - { - return Response.status(HttpStatus.BAD_REQUEST_400).build(); - } - catch (Throwable t) - { - return Response.status(HttpStatus.INTERNAL_SERVER_ERROR_500).build(); - } - } - - /** - * Holds the JSON for the pin POST request - */ - public static class PinJson - { - @JsonProperty(value = "conference-id", required = true) - private String conferenceId; - - @JsonProperty(value = "jvb-version", required = true) - private String jvbVersion; - - @JsonProperty(value = "duration-minutes", required = true) - private Integer minutes; - - @JsonCreator - public PinJson(@JsonProperty(value = "conference-id", required = true) String conferenceId, - @JsonProperty(value = "jvb-version", required = true) String jvbVersion, - @JsonProperty(value = "duration-minutes", required = true) Integer minutes) - { - this.conferenceId = conferenceId; - this.jvbVersion = jvbVersion; - this.minutes = minutes; - } - } - - /** - * Holds the JSON for the unpin POST request - */ - public static class UnpinJson - { - @JsonProperty(value = "conference-id", required = true) - private String conferenceId; - - @JsonCreator - public UnpinJson(@JsonProperty(value = "conference-id", required = true) String conferenceId) - { - this.conferenceId = conferenceId; - } - } -} diff --git a/jicofo/src/main/java/org/jitsi/jicofo/rest/Statistics.java b/jicofo/src/main/java/org/jitsi/jicofo/rest/Statistics.java deleted file mode 100644 index d37801ce0f..0000000000 --- a/jicofo/src/main/java/org/jitsi/jicofo/rest/Statistics.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright @ 2018 - present 8x8, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.jitsi.jicofo.rest; - -import org.jitsi.jicofo.*; - -import jakarta.ws.rs.*; -import jakarta.ws.rs.core.*; -import org.jitsi.jicofo.metrics.*; - -import java.util.*; - -/** - * Adds statistics REST endpoint exposes some internal Jicofo stats. - */ -@Path("/stats") -public class Statistics -{ - /** - * Returns json string with statistics. - * @return json string with statistics. - */ - @GET - @Produces(MediaType.APPLICATION_JSON) - @SuppressWarnings("unchecked") - public String getStats() - { - JicofoServices jicofoServices - = Objects.requireNonNull(JicofoServices.getJicofoServicesSingleton(), "jicofoServices"); - // Update the metrics that are usually updated periodically so we read the current values. - JicofoMetricsContainer.getInstance().getMetricsUpdater().updateMetrics(); - return jicofoServices.getStats().toJSONString(); - } -} diff --git a/jicofo/src/main/kotlin/org/jitsi/jicofo/ConferenceStore.kt b/jicofo/src/main/kotlin/org/jitsi/jicofo/ConferenceStore.kt index 33ee266875..bd82f0df7f 100644 --- a/jicofo/src/main/kotlin/org/jitsi/jicofo/ConferenceStore.kt +++ b/jicofo/src/main/kotlin/org/jitsi/jicofo/ConferenceStore.kt @@ -19,6 +19,7 @@ package org.jitsi.jicofo import org.jitsi.jicofo.conference.JitsiMeetConference import org.jxmpp.jid.EntityBareJid +import java.time.Duration interface ConferenceStore { /** Get a list of all conferences. */ @@ -27,6 +28,10 @@ interface ConferenceStore { /** Get a conference for a specific [Jid] (i.e. name). */ fun getConference(jid: EntityBareJid): JitsiMeetConference? + fun getPinnedConferences(): List + fun pinConference(roomName: EntityBareJid, jvbVersion: String, duration: Duration) + fun unpinConference(roomName: EntityBareJid) + fun addListener(listener: Listener) {} fun removeListener(listener: Listener) {} @@ -35,7 +40,8 @@ interface ConferenceStore { } } -class EmptyConferenceStore : ConferenceStore { - override fun getAllConferences() = emptyList() - override fun getConference(jid: EntityBareJid): JitsiMeetConference? = null -} +data class PinnedConference( + val conferenceId: String, + val jvbVersion: String, + val expiresAt: String +) diff --git a/jicofo/src/main/kotlin/org/jitsi/jicofo/FocusManager.kt b/jicofo/src/main/kotlin/org/jitsi/jicofo/FocusManager.kt index 0384a3511f..050cd0a78e 100644 --- a/jicofo/src/main/kotlin/org/jitsi/jicofo/FocusManager.kt +++ b/jicofo/src/main/kotlin/org/jitsi/jicofo/FocusManager.kt @@ -28,13 +28,11 @@ import org.jitsi.utils.OrderedJsonObject import org.jitsi.utils.logging2.createLogger import org.jitsi.utils.queue.QueueStatistics.Companion.getStatistics import org.jitsi.utils.stats.ConferenceSizeBuckets -import org.json.simple.JSONArray import org.json.simple.JSONObject import org.jxmpp.jid.EntityBareJid import java.time.Clock import java.time.Duration import java.time.Instant -import java.time.ZoneId import java.time.temporal.ChronoUnit import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList @@ -71,7 +69,7 @@ class FocusManager( private val listeners: MutableList = ArrayList() /** Holds the conferences that are currently pinned to a specific bridge version. */ - private val pinnedConferences: MutableMap = HashMap() + private val pinnedConferences: MutableMap = HashMap() fun start() { metricsContainer.metricsUpdater.addUpdateTask { updateMetrics() } @@ -302,8 +300,8 @@ class FocusManager( } /** Create or update the pinning for the specified conference. */ - fun pinConference(roomName: EntityBareJid, jvbVersion: String, duration: Duration) { - val pc = PinnedConference(jvbVersion, duration) + override fun pinConference(roomName: EntityBareJid, jvbVersion: String, duration: Duration) { + val pc = PinnedConferenceState(jvbVersion, duration) synchronized(conferencesSyncRoot) { val prev = pinnedConferences.remove(roomName) if (prev != null) { @@ -317,7 +315,7 @@ class FocusManager( /** * Remove any existing pinning for the specified conference. */ - fun unpinConference(roomName: EntityBareJid) = synchronized(conferencesSyncRoot) { + override fun unpinConference(roomName: EntityBareJid) = synchronized(conferencesSyncRoot) { val prev = pinnedConferences.remove(roomName) logger.info(if (prev != null) "Removing pin for $roomName" else "Unpin failed: $roomName") } @@ -340,25 +338,17 @@ class FocusManager( } /** Get the set of current pinned conferences. */ - fun getPinnedConferencesJson(): JSONObject = JSONObject().apply { - val pins = JSONArray() + override fun getPinnedConferences(): List = buildList { synchronized(conferencesSyncRoot) { expirePins(clock.instant()) - pinnedConferences.forEach { (conferenceId, pinnedConference) -> - pins.add( - JSONObject().apply { - this["conference-id"] = conferenceId.toString() - this["jvb-version"] = pinnedConference.jvbVersion - this["expires-at"] = pinnedConference.expiresAt.atZone(ZoneId.systemDefault()).toString() - } - ) + pinnedConferences.forEach { (conferenceId, p) -> + add(PinnedConference(conferenceId.toString(), p.jvbVersion, p.expiresAt.toString())) } } - this["pins"] = pins } /** Holds pinning information for one conference. */ - private inner class PinnedConference( + private inner class PinnedConferenceState( /** The version of the bridge that this conference must use. */ val jvbVersion: String, duration: Duration diff --git a/jicofo/src/main/kotlin/org/jitsi/jicofo/JicofoServices.kt b/jicofo/src/main/kotlin/org/jitsi/jicofo/JicofoServices.kt index 1721010fbc..c161106bc5 100644 --- a/jicofo/src/main/kotlin/org/jitsi/jicofo/JicofoServices.kt +++ b/jicofo/src/main/kotlin/org/jitsi/jicofo/JicofoServices.kt @@ -18,9 +18,6 @@ package org.jitsi.jicofo import edu.umd.cs.findbugs.annotations.SuppressFBWarnings -import org.eclipse.jetty.server.Server -import org.eclipse.jetty.servlet.ServletHolder -import org.glassfish.jersey.servlet.ServletContainer import org.jitsi.jicofo.auth.AbstractAuthAuthority import org.jitsi.jicofo.auth.AuthConfig import org.jitsi.jicofo.auth.ExternalJWTAuthority @@ -34,25 +31,18 @@ import org.jitsi.jicofo.health.JicofoHealthChecker import org.jitsi.jicofo.jibri.JibriConfig import org.jitsi.jicofo.jibri.JibriDetector import org.jitsi.jicofo.jibri.JibriDetectorMetrics +import org.jitsi.jicofo.ktor.RestConfig import org.jitsi.jicofo.metrics.GlobalMetrics import org.jitsi.jicofo.metrics.JicofoMetricsContainer -import org.jitsi.jicofo.rest.Application -import org.jitsi.jicofo.rest.ConferenceRequest -import org.jitsi.jicofo.rest.RestConfig -import org.jitsi.jicofo.rest.move.MoveEndpoints -import org.jitsi.jicofo.rest.move.MoveEndpointsConfig import org.jitsi.jicofo.util.SynchronizedDelegate import org.jitsi.jicofo.version.CurrentVersionImpl import org.jitsi.jicofo.xmpp.XmppServices import org.jitsi.jicofo.xmpp.initializeSmack import org.jitsi.jicofo.xmpp.jingle.JingleStats -import org.jitsi.rest.Version -import org.jitsi.rest.createServer -import org.jitsi.rest.prometheus.Prometheus -import org.jitsi.rest.servletContextHandler import org.jitsi.utils.OrderedJsonObject import org.jitsi.utils.logging2.createLogger import org.json.simple.JSONObject +import org.jxmpp.jid.EntityBareJid import org.jxmpp.jid.impl.JidCreate import org.jitsi.jicofo.auth.AuthConfig.Companion.config as authConfig @@ -143,38 +133,23 @@ class JicofoServices { null } - private val jettyServer: Server? - - init { - jettyServer = if (RestConfig.config.enabled) { - logger.info("Starting HTTP server with config: ${RestConfig.config.httpServerConfig}.") - val restApp = Application( - buildList { - healthChecker?.let { - add(org.jitsi.rest.Health(it)) - } - add(Version(CurrentVersionImpl.VERSION)) - if (RestConfig.config.enableConferenceRequest) { - add(ConferenceRequest(xmppServices.conferenceIqHandler)) - } - if (RestConfig.config.enablePrometheus) { - add(Prometheus(JicofoMetricsContainer.instance)) - } - if (MoveEndpointsConfig.enabled) { - add(MoveEndpoints(focusManager, bridgeSelector)) - } - } - ) - createServer(RestConfig.config.httpServerConfig).also { - it.servletContextHandler.addServlet( - ServletHolder(ServletContainer(restApp)), - "/*" - ) - it.start() + private val ktor = if (RestConfig.config.enabled) { + org.jitsi.jicofo.ktor.Application( + healthChecker, + xmppServices.conferenceIqHandler, + focusManager, + bridgeSelector, + { getStats() } + ) { full, confId -> + if (confId == null) { + getDebugState(full) + } else { + getConferenceDebugState(confId) } - } else { - null } + } else { + logger.info("Rest interface disabled.") + null } init { @@ -189,7 +164,7 @@ class JicofoServices { } healthChecker?.shutdown() JicofoMetricsContainer.instance.metricsUpdater.stop() - jettyServer?.stop() + ktor?.stop() jvbDoctor?.let { bridgeSelector.removeHandler(it) it.shutdown() @@ -223,7 +198,10 @@ class JicofoServices { } } - fun getStats(): OrderedJsonObject = OrderedJsonObject().apply { + /** Gets statistics for the /stats HTTP interface. */ + private fun getStats(): OrderedJsonObject = OrderedJsonObject().apply { + // Update the metrics that are usually updated periodically so we read the current values. + JicofoMetricsContainer.instance.metricsUpdater.updateMetrics() // We want to avoid exposing unnecessary hierarchy levels in the stats, // so we merge the FocusManager and ColibriConference stats in the root object. putAll(focusManager.stats) @@ -252,7 +230,7 @@ class JicofoServices { } } - fun getDebugState(full: Boolean) = OrderedJsonObject().apply { + private fun getDebugState(full: Boolean) = OrderedJsonObject().apply { put("focus_manager", focusManager.getDebugState(full)) put("bridge_selector", bridgeSelector.debugState) put("jibri_detector", jibriDetector?.debugState ?: "null") @@ -262,7 +240,7 @@ class JicofoServices { put("conference_iq_handler", xmppServices.conferenceIqHandler.debugState) } - fun getConferenceDebugState(conferenceId: String) = OrderedJsonObject().apply { + private fun getConferenceDebugState(conferenceId: EntityBareJid) = OrderedJsonObject().apply { val conference = focusManager.getConference(JidCreate.entityBareFrom(conferenceId)) return conference?.debugState ?: OrderedJsonObject() } diff --git a/jicofo/src/main/kotlin/org/jitsi/jicofo/ktor/Application.kt b/jicofo/src/main/kotlin/org/jitsi/jicofo/ktor/Application.kt new file mode 100644 index 0000000000..9537366dc6 --- /dev/null +++ b/jicofo/src/main/kotlin/org/jitsi/jicofo/ktor/Application.kt @@ -0,0 +1,327 @@ +/* + * Jicofo, the Jitsi Conference Focus. + * + * Copyright @ 2024 - present 8x8, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jitsi.jicofo.ktor + +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.http.parseHeaderValue +import io.ktor.serialization.jackson.jackson +import io.ktor.server.application.install +import io.ktor.server.engine.EmbeddedServer +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.ktor.server.netty.NettyApplicationEngine +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation +import io.ktor.server.plugins.statuspages.StatusPages +import io.ktor.server.request.receive +import io.ktor.server.response.respond +import io.ktor.server.response.respondText +import io.ktor.server.routing.Route +import io.ktor.server.routing.RoutingCall +import io.ktor.server.routing.get +import io.ktor.server.routing.post +import io.ktor.server.routing.route +import io.ktor.server.routing.routing +import org.jitsi.health.HealthCheckService +import org.jitsi.jicofo.ConferenceRequest +import org.jitsi.jicofo.ConferenceStore +import org.jitsi.jicofo.bridge.BridgeSelector +import org.jitsi.jicofo.ktor.exception.BadRequest +import org.jitsi.jicofo.ktor.exception.ExceptionHandler +import org.jitsi.jicofo.ktor.exception.Forbidden +import org.jitsi.jicofo.ktor.exception.MissingParameter +import org.jitsi.jicofo.metrics.JicofoMetricsContainer +import org.jitsi.jicofo.version.CurrentVersionImpl +import org.jitsi.jicofo.xmpp.ConferenceIqHandler +import org.jitsi.jicofo.xmpp.XmppCapsStats +import org.jitsi.utils.OrderedJsonObject +import org.jitsi.utils.logging2.createLogger +import org.jitsi.xmpp.extensions.jitsimeet.ConferenceIq +import org.jivesoftware.smack.packet.ErrorIQ +import org.jivesoftware.smack.packet.IQ +import org.jivesoftware.smack.packet.StanzaError +import org.json.simple.JSONArray +import org.json.simple.JSONObject +import org.jxmpp.jid.EntityBareJid +import org.jxmpp.jid.impl.JidCreate +import org.jxmpp.stringprep.XmppStringprepException +import java.time.Duration +import org.jitsi.jicofo.ktor.RestConfig.Companion.config as config + +class Application( + private val healthChecker: HealthCheckService?, + private val conferenceIqHandler: ConferenceIqHandler, + private val conferenceStore: ConferenceStore, + bridgeSelector: BridgeSelector, + private val getStatsJson: () -> OrderedJsonObject, + private val getDebugState: (full: Boolean, confId: EntityBareJid?) -> OrderedJsonObject +) { + private val logger = createLogger() + private val server = start() + private val moveEndpointsHandler = MoveEndpoints(conferenceStore, bridgeSelector) + + private fun start(): EmbeddedServer { + logger.info("Starting ktor on port ${config.port}, host ${config.host}") + return embeddedServer(Netty, port = config.port, host = config.host) { + install(ContentNegotiation) { + jackson {} + } + install(StatusPages) { + exception { call, cause -> + ExceptionHandler.handle(call, cause) + } + } + + routing { + about() + conferenceRequest() + debug() + metrics() + moveEndpoints() + pin() + rtcstats() + stats() + } + }.start(wait = false) + } + + fun stop() = server.stop() + + private fun Route.metrics() { + if (config.enablePrometheus) { + get("/metrics") { + val accepts = + parseHeaderValue(call.request.headers["Accept"]).sortedByDescending { it.quality }.map { it.value } + val (metrics, contentType) = JicofoMetricsContainer.instance.getMetrics(accepts) + call.respondText(metrics, contentType = ContentType.parse(contentType)) + } + } + } + + private fun Route.about() { + data class VersionInfo(val name: String? = null, val version: String? = null, val os: String? = null) + val versionInfo = VersionInfo( + CurrentVersionImpl.VERSION.applicationName, + CurrentVersionImpl.VERSION.toString(), + System.getProperty("os.name") + ) + + route("/about") { + get("version") { + call.respond(versionInfo) + } + healthChecker?.let { + get("health") { + healthChecker.result.let { + if (it.success) { + call.respond("OK") + } else { + val status = it.responseCode ?: if (it.hardFailure) 500 else 503 + call.respondText(ContentType.Text.Plain, HttpStatusCode.fromValue(status)) { + it.message ?: "Unknown error." + } + } + } + } + } + } + } + + private fun Route.conferenceRequest() { + if (config.enableConferenceRequest) { + post("/conference-request/v1") { + val request = try { + call.receive() + } catch (e: Exception) { + throw BadRequest(e.message) + } + + val response: IQ = try { + conferenceIqHandler.handleConferenceIq(request.toConferenceIq()) + } catch (e: XmppStringprepException) { + throw BadRequest("Invalid room name: ${e.message}") + } catch (e: Exception) { + logger.error(e.message, e) + throw BadRequest(e.message) + } + + if (response !is ConferenceIq) { + if (response is ErrorIQ) { + throw when (response.error.condition) { + StanzaError.Condition.not_authorized -> Forbidden() + StanzaError.Condition.not_acceptable -> BadRequest("invalid-session") + else -> BadRequest(response.error.toString()) + } + } else { + throw InternalError() + } + } + call.respond(ConferenceRequest.fromConferenceIq(response)) + } + } + } + + private fun Route.rtcstats() { + get("/rtcstats") { + val rtcstats = JSONObject() + conferenceStore.getAllConferences().forEach { conference -> + if (conference.includeInStatistics() && conference.isRtcStatsEnabled) { + conference.meetingId?.let { meetingId -> + rtcstats.put(meetingId, conference.rtcstatsState) + } + } + } + + call.respondJson(rtcstats) + } + } + + private fun Route.moveEndpoints() { + if (config.enableMoveEndpoints) { + route("/move-endpoints") { + get("move-endpoint") { + call.respond( + moveEndpointsHandler.moveEndpoint( + call.request.queryParameters["conference"], + call.request.queryParameters["endpoint"], + call.request.queryParameters["bridge"], + ) + ) + } + get("move-endpoints") { + call.respond( + moveEndpointsHandler.moveEndpoints( + call.request.queryParameters["bridge"], + call.request.queryParameters["conference"], + call.request.queryParameters["numEndpoints"]?.toInt() ?: 1 + ) + ) + } + get("move-fraction") { + call.respond( + moveEndpointsHandler.moveFraction( + call.request.queryParameters["bridge"], + call.request.queryParameters["fraction"]?.toDouble() ?: 0.1 + ) + ) + } + } + } + } + + private fun Route.debug() { + if (config.enableDebug) { + route("/debug") { + get("") { + call.respondJson( + getDebugState(call.request.queryParameters["full"] == "true", null) + ) + } + get("conferences") { + val conferencesJson = JSONArray().apply { + conferenceStore.getAllConferences().forEach { + add(it.roomName.toString()) + } + } + call.respondJson(conferencesJson) + } + get("conferences-full") { + val conferencesJson = JSONObject().apply { + conferenceStore.getAllConferences().forEach { + put(it.roomName.toString(), it.debugState) + } + } + call.respondJson(conferencesJson) + } + get("/conference/{conference}") { + val conference = call.parameters["conference"] ?: throw MissingParameter("conference") + val conferenceJid = try { + JidCreate.entityBareFrom(conference) + } catch (e: Exception) { + throw BadRequest("Invalid conference ID") + } + call.respondJson( + getDebugState(true, conferenceJid) + ) + } + get("xmpp-caps") { + call.respondJson(XmppCapsStats.stats) + } + } + } + } + + private fun Route.pin() { + data class PinJson(val conferenceId: String, val jvbVersion: String, val durationMinutes: Int) + data class UnpinJson(val conferenceId: String) + + if (config.pinEnabled) { + route("/pin") { + get("") { + call.respond(conferenceStore.getPinnedConferences()) + } + post("") { + val pin = try { + call.receive() + } catch (e: Exception) { + throw BadRequest(e.message) + } + val conferenceJid = try { + JidCreate.entityBareFrom(pin.conferenceId) + } catch (e: Exception) { + throw BadRequest("Invalid conference ID") + } + + conferenceStore.pinConference( + conferenceJid, + pin.jvbVersion, + Duration.ofMinutes(pin.durationMinutes.toLong()) + ) + call.respond(HttpStatusCode.OK) + } + post("/remove") { + val unpin = call.receive() + val conferenceJid = try { + JidCreate.entityBareFrom(unpin.conferenceId) + } catch (e: Exception) { + throw BadRequest("Invalid conference ID") + } + + conferenceStore.unpinConference(conferenceJid) + call.respond(HttpStatusCode.OK) + } + } + } + } + + private fun Route.stats() { + get("/stats") { + call.respondJson(getStatsJson()) + } + } +} + +private suspend fun RoutingCall.respondJson(json: JSONArray) { + respondText(ContentType.Application.Json, HttpStatusCode.OK) { json.toJSONString() } +} +private suspend fun RoutingCall.respondJson(json: JSONObject) { + respondText(ContentType.Application.Json, HttpStatusCode.OK) { json.toJSONString() } +} +private suspend fun RoutingCall.respondJson(json: OrderedJsonObject) { + respondText(ContentType.Application.Json, HttpStatusCode.OK) { json.toJSONString() } +} diff --git a/jicofo/src/main/kotlin/org/jitsi/jicofo/rest/move/MoveEndpoints.kt b/jicofo/src/main/kotlin/org/jitsi/jicofo/ktor/MoveEndpoints.kt similarity index 65% rename from jicofo/src/main/kotlin/org/jitsi/jicofo/rest/move/MoveEndpoints.kt rename to jicofo/src/main/kotlin/org/jitsi/jicofo/ktor/MoveEndpoints.kt index 8842a995ed..4551162fe6 100644 --- a/jicofo/src/main/kotlin/org/jitsi/jicofo/rest/move/MoveEndpoints.kt +++ b/jicofo/src/main/kotlin/org/jitsi/jicofo/ktor/MoveEndpoints.kt @@ -15,25 +15,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.jitsi.jicofo.rest.move +package org.jitsi.jicofo.ktor -import jakarta.servlet.http.HttpServletResponse -import jakarta.ws.rs.DefaultValue -import jakarta.ws.rs.GET -import jakarta.ws.rs.NotFoundException -import jakarta.ws.rs.Path -import jakarta.ws.rs.Produces -import jakarta.ws.rs.QueryParam -import jakarta.ws.rs.core.MediaType -import jakarta.ws.rs.core.Response -import org.jitsi.config.JitsiConfig import org.jitsi.jicofo.ConferenceStore import org.jitsi.jicofo.bridge.Bridge import org.jitsi.jicofo.bridge.BridgeConfig import org.jitsi.jicofo.bridge.BridgeSelector import org.jitsi.jicofo.conference.JitsiMeetConference -import org.jitsi.jicofo.rest.BadRequestExceptionWithMessage -import org.jitsi.metaconfig.config +import org.jitsi.jicofo.ktor.exception.BadRequest +import org.jitsi.jicofo.ktor.exception.MissingParameter +import org.jitsi.jicofo.ktor.exception.NotFound import org.jitsi.utils.logging2.createLogger import org.jxmpp.jid.impl.JidCreate import kotlin.math.min @@ -44,7 +35,6 @@ import kotlin.math.roundToInt * that when re-inviting the normal bridge selection logic is used again, so it's possible that the same bridge is * selected (unless it's unhealthy/draining or overloaded and there are less loaded bridges). */ -@Path("/move-endpoints") class MoveEndpoints( val conferenceStore: ConferenceStore, val bridgeSelector: BridgeSelector @@ -54,28 +44,19 @@ class MoveEndpoints( /** * Move a specific endpoint in a specific conference. */ - @Path("move-endpoint") - @GET - @Produces(MediaType.APPLICATION_JSON) fun moveEndpoint( - /** - * Conference JID, e.g room@conference.example.com. This is a required parameter, but without a @DefaultValue - * jetty returns a 500 and prints a stack trace. - */ - @QueryParam("conference") @DefaultValue("") conferenceId: String, - /** - * Endpoint ID, e.g. abcdefgh. This is a required parameter, but without a @DefaultValue jetty returns a 500 - * and prints a stack trace. - */ - @QueryParam("endpoint") @DefaultValue("") endpointId: String, + /** Conference JID, e.g room@conference.example.com. */ + conferenceId: String?, + /** Endpoint ID, e.g. abcdefgh. */ + endpointId: String?, /** * Optional bridge JID. If specified, the endpoint will only be moved it if is indeed connected to this bridge. */ - @QueryParam("bridge") @DefaultValue("") bridgeId: String + bridgeId: String? ): Result { - if (conferenceId.isEmpty()) throw BadRequestExceptionWithMessage("Conference ID is missing") - if (endpointId.isEmpty()) throw BadRequestExceptionWithMessage("Endpoint ID is missing") - val bridge = if (bridgeId.isEmpty()) null else getBridge(bridgeId) + if (conferenceId.isNullOrBlank()) throw MissingParameter("conference") + if (endpointId.isNullOrBlank()) throw MissingParameter("endpoint") + val bridge = if (bridgeId.isNullOrBlank()) null else getBridge(bridgeId) val conference = getConference(conferenceId) logger.info("Moving conference=$conferenceId endpoint=$endpointId bridge=$bridgeId") @@ -98,26 +79,20 @@ class MoveEndpoints( * greedily from the list until we've selected the desired count. Note that this may need to be adjusted if it leads * to thundering horde issues (though the recentlyAddedEndpointCount correction should prevent them). */ - @Path("move-endpoints") - @GET - @Produces(MediaType.APPLICATION_JSON) fun moveEndpoints( - /** - * Bridge JID, e.g. jvbbrewery@muc.jvb.example.com/jvb1. This is a required parameter, but without a - * @DefaultValue jetty returns a 500 and prints a stack trace. - */ - @QueryParam("bridge") @DefaultValue("") bridgeId: String, + /** Bridge JID, e.g. jvbbrewery@muc.jvb.example.com/jvb1. */ + bridgeId: String?, /** * Optional conference JID, e.g room@conference.example.com. If specified only endpoints from this conference * will be moved. */ - @QueryParam("conference") @DefaultValue("") conferenceId: String, + conferenceId: String?, /** Number of endpoints to move. */ - @QueryParam("endpoints") @DefaultValue("1") numEndpoints: Int + numEndpoints: Int ): Result { - if (bridgeId.isEmpty()) throw BadRequestExceptionWithMessage("Bridge JID is missing") + if (bridgeId.isNullOrBlank()) throw MissingParameter("bridge") val bridge = getBridge(bridgeId) - val conference = if (conferenceId.isEmpty()) null else getConference(conferenceId) + val conference = if (conferenceId.isNullOrBlank()) null else getConference(conferenceId) val bridgeConferences = if (conference == null) { bridge.getConferences() } else { @@ -136,19 +111,13 @@ class MoveEndpoints( * that this may need to be adjusted if it leads to thundering horde issues (though the recentlyAddedEndpointCount * correction should prevent them). */ - @Path("move-fraction") - @GET - @Produces(MediaType.APPLICATION_JSON) fun moveFraction( - /** - * Bridge JID, e.g. jvbbrewery@muc.jvb.example.com/jvb1. This is a required parameter, but without a - * @DefaultValue jetty returns a 500 and prints a stack trace. - */ - @QueryParam("bridge") @DefaultValue("") bridgeId: String, - /** The fraction of endpoints to move. Defaults to 10% */ - @QueryParam("fraction") @DefaultValue("0.1") fraction: Double + /** Bridge JID, e.g. jvbbrewery@muc.jvb.example.com/jvb1. */ + bridgeId: String?, + /** The fraction of endpoints to move. */ + fraction: Double ): Result { - if (bridgeId.isEmpty()) throw BadRequestExceptionWithMessage("Bridge JID is missing") + if (bridgeId.isNullOrBlank()) throw MissingParameter("bridge") val bridge = getBridge(bridgeId) val bridgeConferences = bridge.getConferences() val totalEndpoints = bridgeConferences.sumOf { it.second } @@ -175,7 +144,7 @@ class MoveEndpoints( val bridgeJid = try { JidCreate.from(bridge) } catch (e: Exception) { - throw BadRequestExceptionWithMessage("Invalid bridge ID") + throw BadRequest("Invalid bridge ID") } bridgeSelector.get(bridgeJid)?.let { return it } @@ -183,19 +152,18 @@ class MoveEndpoints( val bridgeFullJid = try { JidCreate.from("${BridgeConfig.config.breweryJid}/$bridge") } catch (e: Exception) { - throw BadRequestExceptionWithMessage("Invalid bridge ID") + throw BadRequest("Invalid bridge ID") } - return bridgeSelector.get(bridgeFullJid) ?: throw NotFoundExceptionWithMessage("Bridge not found") + return bridgeSelector.get(bridgeFullJid) ?: throw NotFound("Bridge not found") } private fun getConference(conferenceId: String): JitsiMeetConference { val conferenceJid = try { JidCreate.entityBareFrom(conferenceId) } catch (e: Exception) { - throw BadRequestExceptionWithMessage("Invalid conference ID") + throw BadRequest("Invalid conference ID") } - return conferenceStore.getConference(conferenceJid) - ?: throw NotFoundExceptionWithMessage("Conference not found") + return conferenceStore.getConference(conferenceJid) ?: throw NotFound("Conference not found") } private fun Bridge.getConferences() = conferenceStore.getAllConferences().mapNotNull { conference -> @@ -208,14 +176,6 @@ data class Result( val conferences: Int ) -class MoveEndpointsConfig { - companion object { - val enabled: Boolean by config { - "jicofo.rest.move-endpoints.enabled".from(JitsiConfig.newConfig) - } - } -} - /** * Select endpoints to move, e.g. with a map m={a: 1, b: 3, c: 3}: * select(m, 1) should return {a: 1} @@ -239,10 +199,3 @@ private fun List>.select(n: Int): Map { } } } - -/** - * The [NotFoundException(String message)] constructor doesn't actually include the message in the response. - */ -class NotFoundExceptionWithMessage(message: String?) : NotFoundException( - Response.status(HttpServletResponse.SC_NOT_FOUND, message).build() -) diff --git a/jicofo/src/main/kotlin/org/jitsi/jicofo/rest/RestConfig.kt b/jicofo/src/main/kotlin/org/jitsi/jicofo/ktor/RestConfig.kt similarity index 62% rename from jicofo/src/main/kotlin/org/jitsi/jicofo/rest/RestConfig.kt rename to jicofo/src/main/kotlin/org/jitsi/jicofo/ktor/RestConfig.kt index b7377f15a6..2f59db29b6 100644 --- a/jicofo/src/main/kotlin/org/jitsi/jicofo/rest/RestConfig.kt +++ b/jicofo/src/main/kotlin/org/jitsi/jicofo/ktor/RestConfig.kt @@ -15,17 +15,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.jitsi.jicofo.rest +package org.jitsi.jicofo.ktor import org.jitsi.config.JitsiConfig import org.jitsi.metaconfig.config -import org.jitsi.rest.JettyBundleActivatorConfig -import org.jitsi.rest.isEnabled class RestConfig private constructor() { - val httpServerConfig = JettyBundleActivatorConfig("org.jitsi.jicofo.auth", "jicofo.rest") + val port: Int by config { + "jicofo.rest.port".from(JitsiConfig.newConfig) + } - val enabled = httpServerConfig.isEnabled() + val host: String by config { + "jicofo.rest.host".from(JitsiConfig.newConfig) + } + + val enabled: Boolean by config { + "jicofo.rest.enabled".from(JitsiConfig.newConfig) + } val enablePrometheus: Boolean by config { "jicofo.rest.prometheus.enabled".from(JitsiConfig.newConfig) @@ -35,6 +41,18 @@ class RestConfig private constructor() { "jicofo.rest.conference-request.enabled".from(JitsiConfig.newConfig) } + val enableMoveEndpoints: Boolean by config { + "jicofo.rest.move-endpoints.enabled".from(JitsiConfig.newConfig) + } + + val enableDebug: Boolean by config { + "jicofo.rest.debug.enabled".from(JitsiConfig.newConfig) + } + + val pinEnabled: Boolean by config { + "jicofo.rest.pin.enabled".from(JitsiConfig.newConfig) + } + companion object { @JvmField val config = RestConfig() diff --git a/jicofo/src/main/kotlin/org/jitsi/jicofo/ktor/exception/Exceptions.kt b/jicofo/src/main/kotlin/org/jitsi/jicofo/ktor/exception/Exceptions.kt new file mode 100644 index 0000000000..559574f451 --- /dev/null +++ b/jicofo/src/main/kotlin/org/jitsi/jicofo/ktor/exception/Exceptions.kt @@ -0,0 +1,48 @@ +/* + * Jicofo, the Jitsi Conference Focus. + * + * Copyright @ 2024 - present 8x8, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jitsi.jicofo.ktor.exception + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.ApplicationCall +import io.ktor.server.response.respondText + +sealed class JicofoKtorException(message: String?) : RuntimeException(message) +open class BadRequest(message: String? = null) : JicofoKtorException("Bad request: ${message ?: ""}") +class NotFound(message: String?) : JicofoKtorException("Not found: ${message ?: ""}") +class Forbidden(message: String? = null) : JicofoKtorException("Forbidden: ${message ?: ""}") +class InternalError(message: String? = null) : JicofoKtorException("Internal error: ${message ?: ""}") + +@SuppressFBWarnings(value = ["NP_METHOD_PARAMETER_TIGHTENS_ANNOTATION"], justification = "False positive") +class MissingParameter(parameter: String) : BadRequest("Missing parameter: $parameter") + +object ExceptionHandler { + suspend fun handle(call: ApplicationCall, cause: Throwable) { + when (cause) { + is BadRequest -> call.respond(HttpStatusCode.BadRequest, cause.message) + is Forbidden -> call.respond(HttpStatusCode.Forbidden) + is InternalError -> call.respond(HttpStatusCode.InternalServerError, cause.message) + else -> call.respond(HttpStatusCode.InternalServerError, cause.message) + } + } + + private suspend fun ApplicationCall.respond(status: HttpStatusCode, message: String? = null) { + respondText(ContentType.Text.Plain, status) { message ?: "Error" } + } +} diff --git a/jicofo/src/main/kotlin/org/jitsi/jicofo/rest/ConferenceRequest.kt b/jicofo/src/main/kotlin/org/jitsi/jicofo/rest/ConferenceRequest.kt deleted file mode 100644 index 6f0f497052..0000000000 --- a/jicofo/src/main/kotlin/org/jitsi/jicofo/rest/ConferenceRequest.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Jicofo, the Jitsi Conference Focus. - * - * Copyright @ 2022 - present 8x8, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.jitsi.jicofo.rest - -import jakarta.servlet.http.HttpServletResponse -import jakarta.ws.rs.BadRequestException -import jakarta.ws.rs.Consumes -import jakarta.ws.rs.ForbiddenException -import jakarta.ws.rs.InternalServerErrorException -import jakarta.ws.rs.POST -import jakarta.ws.rs.Path -import jakarta.ws.rs.Produces -import jakarta.ws.rs.core.MediaType -import jakarta.ws.rs.core.Response -import org.jitsi.jicofo.xmpp.ConferenceIqHandler -import org.jitsi.utils.logging2.createLogger -import org.jitsi.xmpp.extensions.jitsimeet.ConferenceIq -import org.jivesoftware.smack.packet.ErrorIQ -import org.jivesoftware.smack.packet.IQ -import org.jivesoftware.smack.packet.StanzaError -import org.jxmpp.stringprep.XmppStringprepException - -@Path("/conference-request/v1") -class ConferenceRequest( - val conferenceIqHandler: ConferenceIqHandler -) { - private val logger = createLogger() - - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - fun conferenceRequest(conferenceRequest: org.jitsi.jicofo.ConferenceRequest?): String { - val response: IQ - if (conferenceRequest == null) throw BadRequestExceptionWithMessage("Missing body.") - try { - response = conferenceIqHandler.handleConferenceIq(conferenceRequest.toConferenceIq()) - } catch (e: XmppStringprepException) { - throw BadRequestExceptionWithMessage("Invalid room name: ${e.message}") - } catch (e: Exception) { - logger.error(e.message, e) - throw BadRequestExceptionWithMessage(e.message) - } - - if (response !is ConferenceIq) { - if (response is ErrorIQ) { - throw when (response.error.condition) { - StanzaError.Condition.not_authorized -> { - ForbiddenException() - } - StanzaError.Condition.not_acceptable -> { - BadRequestExceptionWithMessage("invalid-session") - } - else -> BadRequestExceptionWithMessage(response.error.toString()) - } - } else { - throw InternalServerErrorException() - } - } - - return org.jitsi.jicofo.ConferenceRequest.fromConferenceIq(response).toJson() - } -} - -/** - * The ctor for {@link BadRequestException} which takes in a String doesn't - * actually include that String in the response. A much longer syntax (seen - * in the constructor below) is necessary. This class exists in order to expose - * that behavior in a more concise way - */ -class BadRequestExceptionWithMessage(message: String?) : BadRequestException( - Response.status(HttpServletResponse.SC_BAD_REQUEST, message).build() -) diff --git a/jicofo/src/main/kotlin/org/jitsi/jicofo/rest/RtcStats.kt b/jicofo/src/main/kotlin/org/jitsi/jicofo/rest/RtcStats.kt deleted file mode 100644 index 45ecbd52aa..0000000000 --- a/jicofo/src/main/kotlin/org/jitsi/jicofo/rest/RtcStats.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Jicofo, the Jitsi Conference Focus. - * - * Copyright @ 2023-Present 8x8, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.jitsi.jicofo.rest - -import jakarta.ws.rs.GET -import jakarta.ws.rs.InternalServerErrorException -import jakarta.ws.rs.Path -import jakarta.ws.rs.Produces -import jakarta.ws.rs.core.MediaType -import org.jitsi.jicofo.FocusManager -import org.jitsi.jicofo.JicofoServices.Companion.jicofoServicesSingleton -import org.jitsi.utils.logging2.createLogger -import org.json.simple.JSONObject -import java.util.* - -/** - * Get stats intended to be included in rtcstats reports. - * Excludes health check conferences, excludes conferences that have explicitly disabled rtcstats. - * - * Returns a map of meetingId to conference state. - */ -@Path("/rtcstats") -class RtcStats { - private val logger = createLogger() - - /** - * Returns json string with statistics. - * @return json string with statistics. - */ - @GET - @Produces(MediaType.APPLICATION_JSON) - fun rtcstats(): String { - val conferenceStore: FocusManager = jicofoServicesSingleton?.focusManager - ?: throw InternalServerErrorException("No conference store") - - val rtcstats = JSONObject() - conferenceStore.getConferences().forEach { conference -> - if (conference.includeInStatistics() && conference.isRtcStatsEnabled) { - conference.meetingId?.let { meetingId -> - rtcstats.put(meetingId, conference.rtcstatsState) - } - } - } - - return rtcstats.toJSONString() - } -} diff --git a/jicofo/src/test/kotlin/org/jitsi/jicofo/bridge/BridgePinTest.kt b/jicofo/src/test/kotlin/org/jitsi/jicofo/bridge/BridgePinTest.kt index 4b7b4623b6..ead23f29a1 100644 --- a/jicofo/src/test/kotlin/org/jitsi/jicofo/bridge/BridgePinTest.kt +++ b/jicofo/src/test/kotlin/org/jitsi/jicofo/bridge/BridgePinTest.kt @@ -22,8 +22,6 @@ import io.kotest.matchers.shouldBe import io.mockk.mockk import org.jitsi.jicofo.FocusManager import org.jitsi.utils.time.FakeClock -import org.json.simple.JSONArray -import org.json.simple.JSONObject import org.jxmpp.jid.impl.JidCreate import java.time.Duration @@ -49,7 +47,7 @@ class BridgePinTest : ShouldSpec() { focusManager.pinConference(conf3, v3, Duration.ofMinutes(14)) should("pin correctly") { - getNumPins(focusManager.getPinnedConferencesJson()) shouldBe 3 + focusManager.getPinnedConferences().size shouldBe 3 focusManager.getBridgeVersionForConference(conf1) shouldBe v1 focusManager.getBridgeVersionForConference(conf2) shouldBe v2 focusManager.getBridgeVersionForConference(conf3) shouldBe v3 @@ -57,19 +55,19 @@ class BridgePinTest : ShouldSpec() { should("expire") { clock.elapse(Duration.ofMinutes(11)) - getNumPins(focusManager.getPinnedConferencesJson()) shouldBe 2 + focusManager.getPinnedConferences().size shouldBe 2 focusManager.getBridgeVersionForConference(conf1) shouldBe null focusManager.getBridgeVersionForConference(conf2) shouldBe v2 focusManager.getBridgeVersionForConference(conf3) shouldBe v3 clock.elapse(Duration.ofMinutes(2)) - getNumPins(focusManager.getPinnedConferencesJson()) shouldBe 1 + focusManager.getPinnedConferences().size shouldBe 1 focusManager.getBridgeVersionForConference(conf1) shouldBe null focusManager.getBridgeVersionForConference(conf2) shouldBe null focusManager.getBridgeVersionForConference(conf3) shouldBe v3 clock.elapse(Duration.ofMinutes(2)) - getNumPins(focusManager.getPinnedConferencesJson()) shouldBe 0 + focusManager.getPinnedConferences().size shouldBe 0 focusManager.getBridgeVersionForConference(conf1) shouldBe null focusManager.getBridgeVersionForConference(conf2) shouldBe null focusManager.getBridgeVersionForConference(conf3) shouldBe null @@ -85,7 +83,7 @@ class BridgePinTest : ShouldSpec() { should("unpin") { focusManager.unpinConference(conf3) - getNumPins(focusManager.getPinnedConferencesJson()) shouldBe 2 + focusManager.getPinnedConferences().size shouldBe 2 focusManager.getBridgeVersionForConference(conf1) shouldBe v1 focusManager.getBridgeVersionForConference(conf2) shouldBe v2 focusManager.getBridgeVersionForConference(conf3) shouldBe null @@ -94,19 +92,19 @@ class BridgePinTest : ShouldSpec() { should("modify version and timeout") { clock.elapse(Duration.ofMinutes(4)) focusManager.pinConference(conf1, v3, Duration.ofMinutes(10)) - getNumPins(focusManager.getPinnedConferencesJson()) shouldBe 2 + focusManager.getPinnedConferences().size shouldBe 2 focusManager.getBridgeVersionForConference(conf1) shouldBe v3 focusManager.getBridgeVersionForConference(conf2) shouldBe v2 focusManager.getBridgeVersionForConference(conf3) shouldBe null clock.elapse(Duration.ofMinutes(9)) - getNumPins(focusManager.getPinnedConferencesJson()) shouldBe 1 + focusManager.getPinnedConferences().size shouldBe 1 focusManager.getBridgeVersionForConference(conf1) shouldBe v3 focusManager.getBridgeVersionForConference(conf2) shouldBe null focusManager.getBridgeVersionForConference(conf3) shouldBe null clock.elapse(Duration.ofMinutes(2)) - getNumPins(focusManager.getPinnedConferencesJson()) shouldBe 0 + focusManager.getPinnedConferences().size shouldBe 0 focusManager.getBridgeVersionForConference(conf1) shouldBe null focusManager.getBridgeVersionForConference(conf2) shouldBe null focusManager.getBridgeVersionForConference(conf3) shouldBe null @@ -114,12 +112,3 @@ class BridgePinTest : ShouldSpec() { } } } - -fun getNumPins(obj: JSONObject): Int { - val pins = obj["pins"] - if (pins is JSONArray) { - return pins.size - } else { - return -1 - } -} diff --git a/jicofo/src/test/kotlin/org/jitsi/jicofo/util/ListConferenceStore.kt b/jicofo/src/test/kotlin/org/jitsi/jicofo/util/ListConferenceStore.kt index 71d9e4602c..0b302bf69d 100644 --- a/jicofo/src/test/kotlin/org/jitsi/jicofo/util/ListConferenceStore.kt +++ b/jicofo/src/test/kotlin/org/jitsi/jicofo/util/ListConferenceStore.kt @@ -18,10 +18,15 @@ package org.jitsi.jicofo.util import org.jitsi.jicofo.ConferenceStore +import org.jitsi.jicofo.PinnedConference import org.jitsi.jicofo.conference.JitsiMeetConference import org.jxmpp.jid.EntityBareJid +import java.time.Duration class ListConferenceStore : ConferenceStore, MutableList by ArrayList() { override fun getAllConferences() = this override fun getConference(jid: EntityBareJid) = find { it.roomName == jid } + override fun getPinnedConferences(): List = listOf() + override fun pinConference(roomName: EntityBareJid, jvbVersion: String, duration: Duration) { } + override fun unpinConference(roomName: EntityBareJid) {} } diff --git a/pom.xml b/pom.xml index bf6a67b610..2443a04d1d 100644 --- a/pom.xml +++ b/pom.xml @@ -34,17 +34,16 @@ false - 11.0.20 UTF-8 1.0.3 4.4.8 1.9.10 + 3.0.0 5.7.2 1.7.32 - 3.0.10 1.0-127-g6c65524 - 1.1-140-g8f45a9f + 1.1-143-g175c44b 3.0.0 4.6.0 5.10.0 @@ -99,11 +98,6 @@ commons-lang3 3.12.0 - - org.eclipse.jetty - jetty-server - ${jetty.version} - org.igniterealtime.smack smack-core @@ -187,26 +181,6 @@ slf4j-api ${slf4j.version} - - org.glassfish.jersey.containers - jersey-container-jetty-http - ${jersey.version} - - - org.glassfish.jersey.containers - jersey-container-servlet - ${jersey.version} - - - org.glassfish.jersey.inject - jersey-hk2 - ${jersey.version} - - - org.glassfish.jersey.media - jersey-media-json-jackson - ${jersey.version} - com.fasterxml.jackson.module jackson-module-kotlin @@ -244,42 +218,6 @@ 4.0.0 test - - org.glassfish.jersey.test-framework - jersey-test-framework-core - ${jersey.version} - test - - - junit - junit - - - - - org.glassfish.jersey.test-framework.providers - jersey-test-framework-provider-jetty - ${jersey.version} - test - - - junit - junit - - - - - org.glassfish.jersey.test-framework.providers - jersey-test-framework-provider-grizzly2 - ${jersey.version} - test - - - junit - junit - - - io.kotest kotest-runner-junit5-jvm