mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-17 19:31:53 +00:00
Merge branch 'master' into f/public-groups
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
useWorker = typeof window.Worker !== "undefined";
|
||||
isDesktop = true;
|
||||
// Create WebSocket connection.
|
||||
const socket = new WebSocket(`ws://${location.host}`);
|
||||
const socket = new WebSocket(`ws://${location.host}${location.search}`);
|
||||
socket.addEventListener("open", (_event) => {
|
||||
console.log("Opened socket");
|
||||
sendMessageToNative = (msg) => {
|
||||
@@ -192,4 +192,4 @@ function updateCallInfoView(state, description) {
|
||||
document.getElementById("state").innerText = state;
|
||||
document.getElementById("description").innerText = description;
|
||||
}
|
||||
//# sourceMappingURL=ui.js.map
|
||||
//# sourceMappingURL=ui.js.map
|
||||
|
||||
+36
-7
@@ -18,10 +18,12 @@ import org.nanohttpd.protocols.http.response.Status
|
||||
import org.nanohttpd.protocols.websockets.*
|
||||
import java.io.IOException
|
||||
import java.net.BindException
|
||||
import java.net.URI
|
||||
import java.security.SecureRandom
|
||||
import java.util.Base64
|
||||
|
||||
private const val SERVER_HOST = "localhost"
|
||||
private const val SERVER_PORT = 50395
|
||||
private const val CALL_SERVER_TOKEN_BYTES = 32
|
||||
val connections = ArrayList<WebSocket>()
|
||||
|
||||
// Spec: spec/services/calls.md#ActiveCallView
|
||||
@@ -153,14 +155,15 @@ private fun SendStateUpdates() {
|
||||
@Composable
|
||||
fun WebRTCController(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIMessage) -> Unit) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val token = remember { newCallServerToken() }
|
||||
val endCall = {
|
||||
val call = chatModel.activeCall.value
|
||||
if (call != null) withBGApi { chatModel.callManager.endCall(call) }
|
||||
}
|
||||
val server = remember {
|
||||
startServer(onResponse).apply {
|
||||
startServer(onResponse, token = token).apply {
|
||||
try {
|
||||
uriHandler.openUri("http://${SERVER_HOST}:${listeningPort}/simplex/call/")
|
||||
uriHandler.openUri("http://${SERVER_HOST}:${listeningPort}/simplex/call/?token=$token")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to open browser: ${e.stackTraceToString()}")
|
||||
AlertManager.shared.showAlertMsg(
|
||||
@@ -208,7 +211,11 @@ fun WebRTCController(callCommand: SnapshotStateList<WCallCommand>, onResponse: (
|
||||
}
|
||||
}
|
||||
|
||||
fun startServer(onResponse: (WVAPIMessage) -> Unit, port: Int = SERVER_PORT): NanoWSD {
|
||||
fun startServer(
|
||||
onResponse: (WVAPIMessage) -> Unit,
|
||||
port: Int = SERVER_PORT,
|
||||
token: String = newCallServerToken(),
|
||||
): NanoWSD {
|
||||
val server = object: NanoWSD(SERVER_HOST, port) {
|
||||
override fun openWebSocket(session: IHTTPSession): WebSocket = MyWebSocket(onResponse, session)
|
||||
|
||||
@@ -227,8 +234,18 @@ fun startServer(onResponse: (WVAPIMessage) -> Unit, port: Int = SERVER_PORT): Na
|
||||
|
||||
override fun handle(session: IHTTPSession): Response {
|
||||
return when {
|
||||
session.headers["upgrade"] == "websocket" -> super.handle(session)
|
||||
session.uri.contains("/simplex/call/") -> resourcesToResponse("/desktop/call.html")
|
||||
session.headers["upgrade"] == "websocket" ->
|
||||
if (hasValidCallServerToken(session.parameters, token)) {
|
||||
super.handle(session)
|
||||
} else {
|
||||
unauthorizedResponse()
|
||||
}
|
||||
session.uri.contains("/simplex/call/") ->
|
||||
if (hasValidCallServerToken(session.parameters, token)) {
|
||||
resourcesToResponse("/desktop/call.html")
|
||||
} else {
|
||||
unauthorizedResponse()
|
||||
}
|
||||
else -> resourcesToResponse(uriCreateOrNull(session.uri)?.path ?: return newFixedLengthResponse("Error parsing URL"))
|
||||
}
|
||||
}
|
||||
@@ -239,11 +256,23 @@ fun startServer(onResponse: (WVAPIMessage) -> Unit, port: Int = SERVER_PORT): Na
|
||||
if (port == 0) throw e
|
||||
Log.w(TAG, "Call server port $port is busy, using a random port: ${e.message}")
|
||||
server.stop()
|
||||
return startServer(onResponse, port = 0)
|
||||
return startServer(onResponse, port = 0, token = token)
|
||||
}
|
||||
return server
|
||||
}
|
||||
|
||||
internal fun newCallServerToken(): String {
|
||||
val bytes = ByteArray(CALL_SERVER_TOKEN_BYTES)
|
||||
SecureRandom().nextBytes(bytes)
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes)
|
||||
}
|
||||
|
||||
internal fun hasValidCallServerToken(parameters: Map<String, List<String>>, token: String): Boolean =
|
||||
token.isNotEmpty() && parameters["token"]?.any { it == token } == true
|
||||
|
||||
private fun unauthorizedResponse(): Response =
|
||||
newFixedLengthResponse(Status.UNAUTHORIZED, "text/plain", "Unauthorized")
|
||||
|
||||
class MyWebSocket(val onResponse: (WVAPIMessage) -> Unit, handshakeRequest: IHTTPSession) : WebSocket(handshakeRequest) {
|
||||
override fun onOpen() {
|
||||
connections.add(this)
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package chat.simplex.app
|
||||
|
||||
import chat.simplex.common.views.call.startServer
|
||||
import java.net.Socket
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotEquals
|
||||
|
||||
// Integration test for the desktop call server's token gate (the handle() enforcement),
|
||||
// which the unit-level CallServerTokenTest does not exercise.
|
||||
class CallServerAuthTest {
|
||||
private val token = "integration-test-token"
|
||||
// port = 0 binds a random free port, avoiding a clash with a real call server on SERVER_PORT
|
||||
private val server = startServer(onResponse = {}, port = 0, token = token)
|
||||
private val port get() = server.listeningPort
|
||||
|
||||
@AfterTest
|
||||
fun tearDown() = server.stop()
|
||||
|
||||
@Test
|
||||
fun testWebSocketUpgradeRejectedWithoutToken() {
|
||||
assertEquals(401, requestStatus(webSocketUpgrade(path = "/")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWebSocketUpgradeRejectedWithWrongToken() {
|
||||
assertEquals(401, requestStatus(webSocketUpgrade(path = "/?token=wrong")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWebSocketUpgradeAcceptedWithToken() {
|
||||
assertEquals(101, requestStatus(webSocketUpgrade(path = "/?token=$token")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCallPageRejectedWithoutToken() {
|
||||
assertEquals(401, requestStatus(get(path = "/simplex/call/")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCallPagePassesAuthGateWithToken() {
|
||||
// Resource serving may differ in the test classpath, so assert only that the auth gate was passed (not 401)
|
||||
assertNotEquals(401, requestStatus(get(path = "/simplex/call/?token=$token")))
|
||||
}
|
||||
|
||||
private fun get(path: String): List<String> = listOf("GET $path HTTP/1.1", "Host: localhost:$port")
|
||||
|
||||
private fun webSocketUpgrade(path: String): List<String> =
|
||||
listOf(
|
||||
"GET $path HTTP/1.1",
|
||||
"Host: localhost:$port",
|
||||
"Upgrade: websocket",
|
||||
"Connection: Upgrade",
|
||||
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==",
|
||||
"Sec-WebSocket-Version: 13",
|
||||
)
|
||||
|
||||
// Sends a raw HTTP request and returns the response status code from the status line.
|
||||
private fun requestStatus(requestLines: List<String>): Int =
|
||||
Socket("localhost", port).use { socket ->
|
||||
socket.soTimeout = 5000
|
||||
socket.getOutputStream().apply {
|
||||
write((requestLines.joinToString("\r\n") + "\r\n\r\n").toByteArray())
|
||||
flush()
|
||||
}
|
||||
val statusLine = socket.getInputStream().bufferedReader().readLine() ?: error("no response from call server")
|
||||
statusLine.split(" ")[1].toInt()
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package chat.simplex.app
|
||||
|
||||
import chat.simplex.common.views.call.hasValidCallServerToken
|
||||
import chat.simplex.common.views.call.newCallServerToken
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class CallServerTokenTest {
|
||||
@Test
|
||||
fun testCallServerTokenRequiresExactTokenParameter() {
|
||||
val token = "secret"
|
||||
|
||||
assertTrue(hasValidCallServerToken(mapOf("token" to listOf(token)), token))
|
||||
assertFalse(hasValidCallServerToken(mapOf("token" to listOf("wrong")), token))
|
||||
assertFalse(hasValidCallServerToken(mapOf("x-token" to listOf(token)), token))
|
||||
assertFalse(hasValidCallServerToken(mapOf("token" to listOf(token)), ""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCallServerTokenIsUrlSafe() {
|
||||
val token = newCallServerToken()
|
||||
|
||||
assertTrue(token.length >= 40)
|
||||
assertFalse(token.contains("+"))
|
||||
assertFalse(token.contains("/"))
|
||||
assertFalse(token.contains("="))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user