[Android] Remote Access: Enforce HTTPS for API and authentication endpoints

Nicolas Pomepuy git at videolan.org
Fri Oct 2 07:29:56 UTC 2026


vlc-android | branch: master | Nicolas Pomepuy <nicolas at videolabs.io> | Tue Sep 29 13:06:35 2026 +0200| [14269cd03fc41d8e5b3d7f5f92ec6599916b3b6f] | committer: Nicolas Pomepuy

Remote Access: Enforce HTTPS for API and authentication endpoints

Restrict API routes and OTP authentication (/code, /verify-code) to the
HTTPS connector only, returning 403 Forbidden for cleartext HTTP requests.

The unencrypted HTTP port remains open solely to serve static UI assets
and redirect GET / requests to /index.html#/ssl, guiding the user to
accept the self-signed HTTPS certificate before proceeding.

> https://code.videolan.org/videolan/vlc-android/commit/14269cd03fc41d8e5b3d7f5f92ec6599916b3b6f
---

 .../vlc/remoteaccessserver/RemoteAccessServer.kt         | 13 +++++++++++++
 .../remoteaccessserver/routing/RemoteAccessRouting.kt    | 16 +++++++++++++++-
 .../routing/RemoteAccessRoutingAuth.kt                   |  9 +++++++++
 .../routing/RemoteAccessRoutingCommon.kt                 |  7 ++++++-
 4 files changed, 43 insertions(+), 2 deletions(-)

diff --git a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/RemoteAccessServer.kt b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/RemoteAccessServer.kt
index 99046b801b..802a979680 100644
--- a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/RemoteAccessServer.kt
+++ b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/RemoteAccessServer.kt
@@ -829,6 +829,19 @@ class RemoteAccessServer(private val context: Context) : PlaybackService.Callbac
         }
     }
 
+    /**
+     * Checks if a given port is the HTTPS port
+     *
+     * @param port the port to test
+     * @return true if the port is the HTTPS port
+     */
+    fun isHttpsPort(port: Int): Boolean {
+        if (::engine.isInitialized) {
+            return engine.environment.connectors.firstOrNull { it.type.name == "HTTPS" }?.port == port
+        }
+        return false
+    }
+
     /**
      * Returns the server addresses as a list
      *
diff --git a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/routing/RemoteAccessRouting.kt b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/routing/RemoteAccessRouting.kt
index 5e80cad35d..15ca0e3449 100644
--- a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/routing/RemoteAccessRouting.kt
+++ b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/routing/RemoteAccessRouting.kt
@@ -32,6 +32,10 @@ import kotlinx.coroutines.CoroutineScope
 import org.videolan.tools.Settings
 import org.videolan.vlc.remoteaccessserver.RemoteAccessServer
 import org.videolan.vlc.remoteaccessserver.RemoteAccessServer.Companion.getServerFiles
+import io.ktor.http.HttpStatusCode
+import io.ktor.server.application.ApplicationCallPipeline
+import io.ktor.server.application.call
+import io.ktor.server.response.respond
 import java.io.File
 
 /**
@@ -40,12 +44,22 @@ import java.io.File
  */
 fun Route.setupRouting(appContext: Context, scope: CoroutineScope) {
     val settings = Settings.getInstance(appContext)
+    
+    // Unprotected static files and common public routes (served on both HTTP and HTTPS)
     staticFiles("", File(getServerFiles(appContext)))
+    publicCommonRouting(appContext)
 
     publicAuthRouting(appContext, scope, settings)
-    publicCommonRouting(appContext)
 
     authenticate("user_session", optional = RemoteAccessServer.byPassAuth) {
+        intercept(ApplicationCallPipeline.Call) {
+            val server = RemoteAccessServer.getInstance(appContext)
+            if (!server.isHttpsPort(call.request.local.serverPort)) {
+                call.respond(HttpStatusCode.Forbidden)
+                finish()
+            }
+        }
+        
         authenticatedAuthRouting()
         mediaRouting(appContext, scope, settings)
         authenticatedFileRouting(appContext, scope, settings)
diff --git a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/routing/RemoteAccessRoutingAuth.kt b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/routing/RemoteAccessRoutingAuth.kt
index ef473cf655..a5f77b6267 100644
--- a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/routing/RemoteAccessRoutingAuth.kt
+++ b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/routing/RemoteAccessRoutingAuth.kt
@@ -40,6 +40,7 @@ import io.ktor.server.routing.post
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 import org.videolan.vlc.remoteaccessserver.RemoteAccessOTP
+import org.videolan.vlc.remoteaccessserver.RemoteAccessServer
 import org.videolan.vlc.remoteaccessserver.RemoteAccessSession
 import org.videolan.vlc.remoteaccessserver.websockets.RemoteAccessWebSockets
 import org.videolan.vlc.util.RemoteAccessUtils
@@ -50,6 +51,10 @@ fun Route.publicAuthRouting(appContext: Context, scope: CoroutineScope, settings
     //the client is requesting a new code.
     // if the formparameters "challenge" is sent. Remove the corresponding code
     post("/code") {
+        if (!RemoteAccessServer.getInstance(appContext).isHttpsPort(call.request.local.serverPort)) {
+            call.respond(HttpStatusCode.Forbidden)
+            return at post
+        }
         val formParameters = try {
             call.receiveParameters()
         } catch (_: Exception) {
@@ -67,6 +72,10 @@ fun Route.publicAuthRouting(appContext: Context, scope: CoroutineScope, settings
     }
     //Verify the code and inject the cookie if valid
     post("/verify-code") {
+        if (!RemoteAccessServer.getInstance(appContext).isHttpsPort(call.request.local.serverPort)) {
+            call.respond(HttpStatusCode.Forbidden)
+            return at post
+        }
         val formParameters = try {
             call.receiveParameters()
         } catch (e: Exception) {
diff --git a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/routing/RemoteAccessRoutingCommon.kt b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/routing/RemoteAccessRoutingCommon.kt
index e959837a2e..278fc9f02f 100644
--- a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/routing/RemoteAccessRoutingCommon.kt
+++ b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/routing/RemoteAccessRoutingCommon.kt
@@ -64,7 +64,12 @@ import java.io.File
 fun Route.publicCommonRouting(appContext: Context) {
     // Main end point redirect to index.html
     get("/") {
-        call.respondRedirect("index.html", permanent = true)
+        val isHttps = RemoteAccessServer.getInstance(appContext).isHttpsPort(call.request.local.serverPort)
+        if (isHttps) {
+            call.respondRedirect("index.html", permanent = true)
+        } else {
+            call.respondRedirect("/index.html#/ssl", permanent = false)
+        }
     }
     get("/index.html") {
         try {



More information about the Android mailing list