Server endpoint matching custom paths

This commit is contained in:
Adam Gastineau 2025-08-31 07:59:55 -07:00
parent e951602aac
commit ca5dcd4e7b
7 changed files with 74 additions and 11 deletions

View File

@ -1,5 +1,6 @@
package com.penumbraos.bridge_settings
import android.os.IBinder
import android.util.Log
import com.penumbraos.bridge.ISettingsProvider
import com.penumbraos.bridge.callback.IHttpEndpointCallback
@ -343,6 +344,12 @@ class SettingsProvider(private val settingsRegistry: SettingsRegistry) : ISettin
Log.e(TAG, "Cannot register HTTP endpoint - web server not initialized")
false
} else {
callback.asBinder().linkToDeath(object : IBinder.DeathRecipient {
override fun binderDied() {
server.unregisterEndpoint(providerId, path, method)
}
}, 0)
val success = server.registerEndpoint(
providerId,
path,

View File

@ -280,13 +280,24 @@ class SettingsWebServer(
}
intercept(ApplicationCallPipeline.Call) {
val fullPath =
call.request.uri.substringBefore('?') // Remove query params from path
val fullPath = call.request.uri.substringBefore('?')
val method = call.request.httpMethod.value
val endpointKey = "${method}:$fullPath"
var matchedEndpoint: RegisteredEndpoint? = null
var pathParams: Map<String, String>? = null
for (endpoint in registeredEndpoints.values) {
if (endpoint.method.equals(method, ignoreCase = true)) {
val match = endpoint.matchesPath(fullPath)
if (match != null) {
matchedEndpoint = endpoint
pathParams = match
break
}
}
}
val endpoint = registeredEndpoints[endpointKey]
if (endpoint != null) {
if (matchedEndpoint != null && pathParams != null) {
try {
val headers = call.request.headers.toMap()
.mapValues { it.value.firstOrNull() ?: "" }
@ -303,10 +314,11 @@ class SettingsWebServer(
method = method,
headers = headers,
queryParams = queryParams,
pathParams = pathParams,
body = body
)
val response = endpoint.callback.handle(request)
val response = matchedEndpoint.callback.handle(request)
response.headers.forEach { (key, value) ->
call.response.headers.append(key, value)
@ -320,7 +332,7 @@ class SettingsWebServer(
)
return@intercept finish()
} catch (e: Exception) {
Log.e(TAG, "Error handling dynamic endpoint $endpointKey", e)
Log.e(TAG, "Error handling dynamic endpoint ${matchedEndpoint.path}", e)
call.respond(
HttpStatusCode.InternalServerError,
mapOf("error" to "Internal server error")
@ -335,7 +347,7 @@ class SettingsWebServer(
webSocket("/ws/settings") {
handleWebSocketConnection(this)
}
// REST API endpoints
get("/api/settings") {
try {

View File

@ -0,0 +1,28 @@
package com.penumbraos.bridge_settings.server
object PathParser {
fun matchPath(pattern: String, actualPath: String): Map<String, String>? {
val patternSegments = pattern.split('/').filter { it.isNotEmpty() }
val actualSegments = actualPath.split('/').filter { it.isNotEmpty() }
if (patternSegments.size != actualSegments.size) {
return null
}
val pathParams = mutableMapOf<String, String>()
for (i in patternSegments.indices) {
val patternSegment = patternSegments[i]
val actualSegment = actualSegments[i]
if (patternSegment.startsWith('{') && patternSegment.endsWith('}')) {
val variableName = patternSegment.substring(1, patternSegment.length - 1)
pathParams[variableName] = actualSegment
} else if (patternSegment != actualSegment) {
return null
}
}
return pathParams
}
}

View File

@ -11,6 +11,7 @@ data class EndpointRequest(
val method: String,
val headers: Map<String, String>,
val queryParams: Map<String, String>,
val pathParams: Map<String, String>,
val body: String?
)
@ -35,7 +36,11 @@ data class RegisteredEndpoint(
val method: String,
val callback: EndpointCallback,
val providerId: String
)
) {
fun matchesPath(requestPath: String): Map<String, String>? {
return PathParser.matchPath(this.path, requestPath)
}
}
class AidlEndpointCallback(
private val aidlCallback: IHttpEndpointCallback
@ -64,6 +69,7 @@ class AidlEndpointCallback(
aidlCallback.onHttpRequest(
request.path,
request.method,
request.pathParams,
request.headers,
request.queryParams,
request.body,

View File

@ -3,5 +3,5 @@ package com.penumbraos.bridge.callback;
import com.penumbraos.bridge.callback.IHttpResponseCallback;
interface IHttpEndpointCallback {
void onHttpRequest(String path, String method, in Map headers, in Map queryParams, String body, IHttpResponseCallback responseCallback);
void onHttpRequest(String path, String method, in Map pathParams, in Map headers, in Map queryParams, String body, IHttpResponseCallback responseCallback);
}

View File

@ -182,6 +182,7 @@ class SettingsClient(private val settingsProvider: ISettingsProvider) {
override fun onHttpRequest(
path: String,
method: String,
pathParams: MutableMap<Any?, Any?>,
headers: MutableMap<Any?, Any?>?,
queryParams: MutableMap<Any?, Any?>?,
body: String?,
@ -193,7 +194,15 @@ class SettingsClient(private val settingsProvider: ISettingsProvider) {
val queryMap = queryParams?.mapKeys { it.key.toString() }
?.mapValues { it.value.toString() } ?: emptyMap()
val request = HttpRequest(path, method, headerMap, queryMap, body)
val request =
HttpRequest(
path,
method,
pathParams as Map<String, String>,
headerMap,
queryMap,
body
)
scope.launch {
try {

View File

@ -3,6 +3,7 @@ package com.penumbraos.sdk.api.types
data class HttpRequest(
val path: String,
val method: String,
val pathParams: Map<String, String>,
val headers: Map<String, String>,
val queryParams: Map<String, String>,
val body: String?