-
Notifications
You must be signed in to change notification settings - Fork 0
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. missing val? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is true for cloudflare as well? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") }, | ||
), | ||
) | ||
} | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not just you!