Skip to content

Commit

Permalink
Add Cloudflare turn servers
Browse files Browse the repository at this point in the history
  • Loading branch information
Brutus5000 committed Nov 27, 2024
1 parent c9e238a commit d0e3e65
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.faforever.icebreaker.service.cloudflare

import jakarta.annotation.Priority
import jakarta.ws.rs.Priorities
import jakarta.ws.rs.client.ClientRequestContext
import jakarta.ws.rs.client.ClientRequestFilter
import jakarta.ws.rs.core.HttpHeaders

@Priority(Priorities.AUTHENTICATION)
class ApiKeyAuthenticationRequestFilter(turnApiKey: String) : ClientRequestFilter {
override fun filter(requestContext: ClientRequestContext) {
requestContext.headers.add(HttpHeaders.AUTHORIZATION, accessToken)
}

private val accessToken: String = "Bearer $turnApiKey"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.faforever.icebreaker.service.cloudflare

import jakarta.inject.Singleton
import org.eclipse.microprofile.faulttolerance.Retry
import org.eclipse.microprofile.rest.client.RestClientBuilder
import java.net.URI

@Singleton
class CloudflareApiAdapter(
private val cloudflareProperties: CloudflareProperties,
) {
private val cloudflareApiClient = RestClientBuilder
.newBuilder()
.baseUri(URI.create("https://rtc.live.cloudflare.com"))
.register(ApiKeyAuthenticationRequestFilter(turnApiKey = cloudflareProperties.turnKeyApiToken()))
.build(CloudflareApiClient::class.java)

@Retry
fun requestIceServers(credentialRequest: CloudflareApiClient.CredentialRequest) =
cloudflareApiClient.requestCredentials(
turnKeyId = cloudflareProperties.turnKeyId(),
credentialRequest = credentialRequest,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.faforever.icebreaker.service.cloudflare

import jakarta.enterprise.context.ApplicationScoped
import jakarta.ws.rs.Consumes
import jakarta.ws.rs.POST
import jakarta.ws.rs.Path
import jakarta.ws.rs.PathParam
import jakarta.ws.rs.core.MediaType
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient

@ApplicationScoped
@RegisterRestClient
@Consumes(MediaType.APPLICATION_JSON)
interface CloudflareApiClient {

data class CredentialRequest(val ttl: Long)
data class CredentialResponse(val iceServers: IceServers) {
data class IceServers(val urls: List<String>, val username: String, val credential: String)
}

@POST
@Path("/v1/turn/keys/{turnKeyId}/credentials/generate")
fun requestCredentials(
@PathParam("turnKeyId") turnKeyId: String,
credentialRequest: CredentialRequest,
): CredentialResponse
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.faforever.icebreaker.service.cloudflare

import io.smallrye.config.ConfigMapping

@ConfigMapping(prefix = "cloudflare")
interface CloudflareProperties {
fun enabled(): Boolean

fun turnEnabled(): Boolean

fun turnKeyId(): String

fun turnKeyApiToken(): String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.faforever.icebreaker.service.cloudflare

import com.faforever.icebreaker.config.FafProperties
import com.faforever.icebreaker.service.Server
import com.faforever.icebreaker.service.Session
import com.faforever.icebreaker.service.SessionHandler
import jakarta.inject.Singleton

@Singleton
class CloudflareSessionHandler(
cloudflareProperties: CloudflareProperties,
val fafProperties: FafProperties,
private val cloudflareApiAdapter: CloudflareApiAdapter,
) : SessionHandler {
companion object {
const val SERVER_NAME = "cloudflare.com"
}

override val active = cloudflareProperties.enabled()
private val turnEnabled = cloudflareProperties.turnEnabled()

override fun createSession(id: String) {
// Cloudflare has no session handling, we use global access
}

override fun deleteSession(id: String) {
// Cloudflare has no session handling, we use global access
}

override fun getIceServers() = listOf(Server(id = SERVER_NAME, region = "Global"))

override fun getIceServersSession(sessionId: String): List<Session.Server> =
cloudflareApiAdapter.requestIceServers(
credentialRequest = CloudflareApiClient.CredentialRequest(ttl = fafProperties.tokenLifetimeSeconds()),
).let {
listOf(
Session.Server(
id = SERVER_NAME,
username = it.iceServers.username,
credential = it.iceServers.credential,
urls = it.iceServers.urls.map { url ->
// A sample response looks like "stun:fr-turn1.xirsys.com"
// The java URI class fails to read host and port due to the missing // after the :
// Thus we "normalize" the uri, even though it is technically valid
url.replaceFirst(":", "://")
}.filter { url -> turnEnabled || !url.startsWith("turn") },
),
)
}
}
7 changes: 7 additions & 0 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,20 @@ xirsys:
channel-namespace: "faf"
turn-enabled: ${XIRSYS_TURN_ENABLED:true}
geo-ip-path: ${GEO_IP_DATABASE_PATH:/geoip/GeoLite2-City.mmdb}
cloudflare:
enabled: ${CLOUDFLARE_ENABLED:true}
turn-enabled: ${CLOUDFLARE_TURN_ENABLED:true}
turn-key-id: ${CLOUDFLARE_TURN_KEY_ID:undefined}
turn-key-api-token: ${CLOUDFLARE_TURN_KEY_API_KEY:undefined}
smallrye:
jwt:
sign:
key: ${JWT_PRIVATE_KEY_PATH}
"%dev":
xirsys:
enabled: ${XIRSYS_ENABLED:false}
cloudflare:
enabled: ${CLOUDFLARE_ENABLED:false}
smallrye:
jwt:
sign:
Expand Down

0 comments on commit d0e3e65

Please sign in to comment.