Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Cloudflare turn servers #45

Merged
merged 1 commit into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a little surprised we have to write our own filter for this, but seems like that is the way to do it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not just you!

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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing val?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

val is only required if I access the variable after initialization. This is not the case here, this it is not required

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
Comment on lines +42 to +44
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is true for cloudflare as well?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Surprisingly, they also have the exact same response structure for credential and username. Maybe it is part of the RFC

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
Loading