add some more security: return sim specifc sms based on api key
This commit is contained in:
@@ -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) {}
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ class TextpipeServer(
|
|||||||
webUIRoutes(context)
|
webUIRoutes(context)
|
||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
statusRoutes(simManager, { startTime }, { currentPort })
|
statusRoutes(simManager)
|
||||||
smsRoutes(smsRepository)
|
smsRoutes(smsRepository)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user