add some more security: return sim specifc sms based on api key

This commit is contained in:
2026-02-06 02:23:41 +05:00
parent 649de49c75
commit 6e42941b0f
8 changed files with 63 additions and 27 deletions

View File

@@ -386,9 +386,8 @@
async function loadStatus() { async function loadStatus() {
try { try {
const r = await fetch('/api/status', { headers: { 'X-API-Key': apiKey } }); const r = await fetch('/api/status', { headers: { 'X-API-Key': apiKey } });
const d = await r.json(); const sim = await r.json();
const sim = d.sims && d.sims[0]; document.getElementById('sim-info').textContent = sim.slot !== undefined
document.getElementById('sim-info').textContent = sim
? `SIM ${sim.slot + 1} - ${sim.number || sim.carrier || 'Unknown'}` ? `SIM ${sim.slot + 1} - ${sim.number || sim.carrier || 'Unknown'}`
: 'No SIM'; : 'No SIM';
} catch (e) {} } catch (e) {}

View File

@@ -33,4 +33,10 @@ interface SmsMessageDao {
@Query("SELECT COUNT(*) FROM sms_messages WHERE type = :type") @Query("SELECT COUNT(*) FROM sms_messages WHERE type = :type")
suspend fun getCountByType(type: String): Int suspend fun getCountByType(type: String): Int
@Query("SELECT * FROM sms_messages WHERE simSlot = :simSlot ORDER BY timestamp DESC")
suspend fun getBySimSlot(simSlot: Int): List<SmsMessageEntity>
@Query("SELECT * FROM sms_messages WHERE simSlot = :simSlot AND type = :type ORDER BY timestamp DESC")
suspend fun getBySimSlotAndType(simSlot: Int, type: String): List<SmsMessageEntity>
} }

View File

@@ -27,17 +27,20 @@ class SmsRepository(
) )
} }
suspend fun getAllMessages(type: String? = null): List<SmsMessageResponse> { suspend fun getMessagesBySimSlot(simSlot: Int, type: String? = null): List<SmsMessageResponse> {
val messages = if (type != null) { val messages = if (type != null) {
dao.getByType(type) dao.getBySimSlotAndType(simSlot, type)
} else { } else {
dao.getAll() dao.getBySimSlot(simSlot)
} }
return messages.map { it.toResponse() } return messages.map { it.toResponse() }
} }
suspend fun getMessageById(id: Long): SmsMessageResponse? { suspend fun getMessageById(id: Long, simSlot: Int): SmsMessageResponse? {
return dao.getById(id)?.toResponse() val message = dao.getById(id) ?: return null
// Only return if message belongs to this SIM slot
if (message.simSlot != simSlot) return null
return message.toResponse()
} }
suspend fun insertReceivedSms(from: String, text: String, simSlot: Int): Long { suspend fun insertReceivedSms(from: String, text: String, simSlot: Int): Long {

View File

@@ -153,7 +153,7 @@ class TextpipeServer(
webUIRoutes(context) webUIRoutes(context)
// API routes // API routes
statusRoutes(simManager, { startTime }, { currentPort }) statusRoutes(simManager)
smsRoutes(smsRepository) smsRoutes(smsRepository)
} }
} }

View File

@@ -15,15 +15,12 @@ fun Application.configureAuth(simManager: SimManager) {
intercept(ApplicationCallPipeline.Plugins) { intercept(ApplicationCallPipeline.Plugins) {
val path = call.request.local.uri val path = call.request.local.uri
// Skip auth for status endpoint // Skip auth for web UI
if (path == "/api/status" || path == "/") { if (path == "/" || path.startsWith("/assets") || !path.startsWith("/api/")) {
return@intercept return@intercept
} }
// Only require auth for /api/sms/* endpoints // All /api/* endpoints require auth
if (!path.startsWith("/api/sms")) {
return@intercept
}
val apiKey = extractApiKey(call) val apiKey = extractApiKey(call)

View File

@@ -9,6 +9,7 @@ import sh.sar.textpipe.data.model.MessagesResponse
import sh.sar.textpipe.data.model.SendSmsRequest import sh.sar.textpipe.data.model.SendSmsRequest
import sh.sar.textpipe.data.repository.SmsRepository import sh.sar.textpipe.data.repository.SmsRepository
import sh.sar.textpipe.server.auth.ApiKeyAttributeKey import sh.sar.textpipe.server.auth.ApiKeyAttributeKey
import sh.sar.textpipe.server.auth.SimSlotAttributeKey
fun Route.smsRoutes(smsRepository: SmsRepository) { fun Route.smsRoutes(smsRepository: SmsRepository) {
post("/api/sms/send") { post("/api/sms/send") {
@@ -45,6 +46,12 @@ fun Route.smsRoutes(smsRepository: SmsRepository) {
} }
get("/api/sms/messages") { get("/api/sms/messages") {
val simSlot = call.attributes.getOrNull(SimSlotAttributeKey)
if (simSlot == null) {
call.respond(HttpStatusCode.Unauthorized, ErrorResponse("Missing API key"))
return@get
}
val type = call.request.queryParameters["type"] val type = call.request.queryParameters["type"]
// Validate type if provided // Validate type if provided
@@ -53,7 +60,7 @@ fun Route.smsRoutes(smsRepository: SmsRepository) {
return@get return@get
} }
val messages = smsRepository.getAllMessages(type) val messages = smsRepository.getMessagesBySimSlot(simSlot, type)
val response = MessagesResponse( val response = MessagesResponse(
messages = messages, messages = messages,
total = messages.size total = messages.size
@@ -62,6 +69,12 @@ fun Route.smsRoutes(smsRepository: SmsRepository) {
} }
get("/api/sms/status/{id}") { get("/api/sms/status/{id}") {
val simSlot = call.attributes.getOrNull(SimSlotAttributeKey)
if (simSlot == null) {
call.respond(HttpStatusCode.Unauthorized, ErrorResponse("Missing API key"))
return@get
}
val idParam = call.parameters["id"] val idParam = call.parameters["id"]
val id = idParam?.toLongOrNull() val id = idParam?.toLongOrNull()
@@ -70,7 +83,7 @@ fun Route.smsRoutes(smsRepository: SmsRepository) {
return@get return@get
} }
val message = smsRepository.getMessageById(id) val message = smsRepository.getMessageById(id, simSlot)
if (message == null) { if (message == null) {
call.respond(HttpStatusCode.NotFound, ErrorResponse("Message not found")) call.respond(HttpStatusCode.NotFound, ErrorResponse("Message not found"))
return@get return@get

View File

@@ -1,19 +1,27 @@
package sh.sar.textpipe.server.routes package sh.sar.textpipe.server.routes
import io.ktor.http.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
import sh.sar.textpipe.data.model.StatusResponse import sh.sar.textpipe.data.model.ErrorResponse
import sh.sar.textpipe.data.model.SimInfo
import sh.sar.textpipe.server.auth.ApiKeyAttributeKey
import sh.sar.textpipe.sim.SimManager import sh.sar.textpipe.sim.SimManager
fun Route.statusRoutes(simManager: SimManager, getStartTime: () -> Long, getPort: () -> Int) { fun Route.statusRoutes(simManager: SimManager) {
get("/api/status") { get("/api/status") {
val uptime = System.currentTimeMillis() - getStartTime() val apiKey = call.attributes.getOrNull(ApiKeyAttributeKey)
val response = StatusResponse( if (apiKey == null) {
running = true, call.respond(HttpStatusCode.Unauthorized, ErrorResponse("Missing API key"))
port = getPort(), return@get
uptime = uptime, }
sims = simManager.getSimInfoList()
) val simInfo = simManager.getSimInfoForApiKey(apiKey)
call.respond(response) if (simInfo == null) {
call.respond(HttpStatusCode.NotFound, ErrorResponse("SIM not found"))
return@get
}
call.respond(simInfo)
} }
} }

View File

@@ -112,6 +112,16 @@ class SimManager(private val context: Context) {
} }
} }
fun getSimInfoForApiKey(apiKey: String): SimInfo? {
val sim = cachedSims.find { it.apiKey == apiKey } ?: return null
return SimInfo(
slot = sim.slotIndex,
carrier = sim.carrierName,
number = sim.phoneNumber,
hasApiKey = true
)
}
fun getCachedSims(): List<SimSlotInfo> = cachedSims fun getCachedSims(): List<SimSlotInfo> = cachedSims
fun regenerateApiKey(slotIndex: Int): String { fun regenerateApiKey(slotIndex: Int): String {