Http Server / Client communication #7

Merged
faraphel merged 24 commits from test-http into main 2024-05-17 17:22:56 +02:00
6 changed files with 204 additions and 98 deletions
Showing only changes of commit 6b4b420a22 - Show all commits

View file

@ -1,11 +1,8 @@
package com.faraphel.tasks_valider.connectivity.task
import com.faraphel.tasks_valider.connectivity.task.session.TaskPermission
import com.faraphel.tasks_valider.connectivity.task.session.TaskSession
import com.faraphel.tasks_valider.connectivity.task.session.TaskSessionManager
import com.faraphel.tasks_valider.database.TaskDatabase
import com.google.gson.Gson
import fi.iki.elonen.NanoHTTPD
import java.util.*
/**
@ -17,78 +14,11 @@ class TaskServer(
private val port: Int,
private val database: TaskDatabase
) : NanoHTTPD(port) {
// Session
// TODO(Faraphel): theses functions should be moved somewhere else for specific session management
private val sessions = mutableMapOf<String, TaskSession>() ///< sessions specific data
/**
* Create a new session
* @param session the data for the session (optional)
* @return a new session identifiant
*/
private fun newSessionData(session: TaskSession = TaskSession()): String {
val sessionId = UUID.randomUUID().toString()
this.sessions[sessionId] = session
return sessionId
}
/**
* Get data from a http session
* @param httpSession the HTTP session
* @return the session data
*/
private fun getSessionData(httpSession: IHTTPSession): TaskSession? {
val sessionId = httpSession.cookies.read("sessionId") ?: return null
val sessionData = this.getSessionData(sessionId)
return sessionData
}
/**
* Get data from a session identifiant
* @param sessionId the identifiant of the session
* @return the session data
*/
private fun getSessionData(sessionId: String): TaskSession? {
return this.sessions[sessionId]
}
/**
* Get data from a http session. If it does not exist, create it.
* @param httpSession the HTTP session
*/
private fun getOrCreateSessionData(httpSession: IHTTPSession): TaskSession {
// try to get the session directly
var session = this.getSessionData(httpSession)
// if the session does not exist, create it
if (session == null) {
val sessionId = this.newSessionData()
session = this.getSessionData(sessionId)!!
}
// return the session
return session
}
/**
* Insert new cookies for a session in a response
* @param response the response to inject cookies into
* @param cookies the cookie handler
*/
private fun responseSetSessionData(response: Response, cookies: CookieHandler): Response {
cookies.set(Cookie("sessionId", this.newSessionData()))
cookies.unloadQueue(response)
return response
}
// HTTP
private val sessionManager = TaskSessionManager() ///< the session manager
override fun serve(httpSession: IHTTPSession): Response {
// get the session data of the client
val taskSession = this.getOrCreateSessionData(httpSession)
val taskSession = this.sessionManager.getOrCreateSessionData(httpSession)
// get the method used
val method: Method = httpSession.method
@ -108,9 +38,8 @@ class TaskServer(
// get the response from the correct part of the application
val response = when (requestType) {
// session requests
"sessions" -> {
TODO("Faraphel: handle admin changing permission")
}
"sessions" ->
this.sessionManager.handleApiRequest(taskSession, httpSession, method)
// entities requests
"entities" -> {
@ -121,26 +50,25 @@ class TaskServer(
"Invalid entity name"
)
this.database.handleApiRequest(taskSession, httpSession, method, entityName)
// TODO(Faraphel): the uri arguments should be handled in the handleApiRequest
return this.database.handleApiRequest(taskSession, httpSession, method, entityName)
}
// invalid requests
else -> {
else ->
newFixedLengthResponse(
NanoHTTPD.Response.Status.BAD_REQUEST,
"text/plain",
"Invalid request type"
)
}
}
// wrap additional information in the response
return this.responseSetSessionData(response, httpSession.cookies)
return this.sessionManager.responseSetSessionData(response, httpSession.cookies)
}
/**
* Start the server with the default configuration
*/
override fun start() {
super.start(SOCKET_READ_TIMEOUT, false)
}
override fun start() = super.start(SOCKET_READ_TIMEOUT, false)
}

View file

@ -1,12 +1,7 @@
package com.faraphel.tasks_valider.connectivity.task.session
/**
* A permission level that can be used for in the task system
*/
enum class TaskPermission {
NONE,
STUDENT,
TEACHER,
ADMIN,
READ,
WRITE,
ADMIN
}

View file

@ -0,0 +1,31 @@
package com.faraphel.tasks_valider.connectivity.task.session
/**
* A role system that can be used for in the task system
*/
enum class TaskRole(val value: String) {
NONE("none") {
override var permissions: List<TaskPermission> = listOf()
},
STUDENT("student") {
override var permissions = listOf(
TaskPermission.READ
)
},
TEACHER("teacher") {
override var permissions: List<TaskPermission> = listOf(
TaskPermission.READ,
TaskPermission.WRITE
)
},
ADMIN("admin") {
override var permissions: List<TaskPermission> = listOf(
TaskPermission.READ,
TaskPermission.WRITE,
TaskPermission.ADMIN
)
};
abstract var permissions: List<TaskPermission>
}

View file

@ -3,8 +3,8 @@ package com.faraphel.tasks_valider.connectivity.task.session
/**
* store the data of a session in the task system
* @param permission the permission accorded to the session
* @param role the role accorded to the session
*/
data class TaskSession(
var permission: TaskPermission = TaskPermission.STUDENT
var role: TaskRole = TaskRole.STUDENT
)

View file

@ -0,0 +1,147 @@
package com.faraphel.tasks_valider.connectivity.task.session
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import fi.iki.elonen.NanoHTTPD
import java.util.*
/**
* The manager for the session system
*/
class TaskSessionManager {
private val jsonParser = Gson() ///< the json parser
private val jsonTypeToken = object : TypeToken<Map<String, String>>(){} ///< the json type
private val sessions = mutableMapOf<String, TaskSession>() ///< sessions specific data
/**
* Create a new session
* @param session the data for the session (optional)
* @param sessionId the session id to use (optional)
* @return a new session identifiant
*/
fun newSessionData(
session: TaskSession = TaskSession(),
sessionId: String = UUID.randomUUID().toString()
): String {
this.sessions[sessionId] = session
return sessionId
}
/**
* Get data from a http session
* @param httpSession the HTTP session
* @return the session data
*/
fun getSessionData(httpSession: NanoHTTPD.IHTTPSession): TaskSession? {
val sessionId = httpSession.cookies.read("sessionId") ?: return null
val sessionData = this.getSessionData(sessionId)
return sessionData
}
/**
* Get data from a session identifiant
* @param sessionId the identifiant of the session
* @return the session data
*/
fun getSessionData(sessionId: String): TaskSession? {
return this.sessions[sessionId]
}
/**
* Get data from a http session. If it does not exist, create it.
* @param httpSession the HTTP session
*/
fun getOrCreateSessionData(httpSession: NanoHTTPD.IHTTPSession): TaskSession {
// try to get the session directly
var session = this.getSessionData(httpSession)
// if the session does not exist, create it
if (session == null) {
val sessionId = this.newSessionData()
session = this.getSessionData(sessionId)!!
}
// return the session
return session
}
/**
* Insert new cookies for a session in a response
* @param response the response to inject cookies into
* @param cookies the cookie handler
*/
fun responseSetSessionData(
response: NanoHTTPD.Response,
cookies: NanoHTTPD.CookieHandler
): NanoHTTPD.Response {
cookies.set(NanoHTTPD.Cookie("sessionId", this.newSessionData()))
cookies.unloadQueue(response)
return response
}
/**
* Handle a HTTP Api request
* @param taskSession the data of the client session
* @param httpSession the data of the http session
* @param method the method used
*/
fun handleApiRequest(
taskSession: TaskSession,
httpSession: NanoHTTPD.IHTTPSession,
method: NanoHTTPD.Method
): NanoHTTPD.Response {
when (method) {
// get a client session data
NanoHTTPD.Method.GET -> {
TODO("Faraphel: implement get")
}
// change a client session data
NanoHTTPD.Method.POST -> {
// check the permission of the session
if (taskSession.role.permissions.contains(TaskPermission.ADMIN))
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.FORBIDDEN,
"text/plain",
"Forbidden"
)
// parse the content of the request
val data: Map<String, String> = jsonParser.fromJson(
httpSession.inputStream.bufferedReader().readText(),
jsonTypeToken.type
)
// update the role
data["role"]?.let { role ->
try {
taskSession.role = TaskRole.valueOf(role)
} catch (exception: IllegalArgumentException) {
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.BAD_REQUEST,
"text/plain",
"Invalid role"
)
}
// NOTE(Faraphel): does a modification on the object require updating the array ?
}
// success message
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.OK,
"text/plain",
"Session updated"
)
}
// other action are limited
else -> {
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.METHOD_NOT_ALLOWED,
"text/plain",
"Method not allowed"
)
}
}
}
}

View file

@ -4,6 +4,7 @@ import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.faraphel.tasks_valider.connectivity.task.session.TaskPermission
import com.faraphel.tasks_valider.connectivity.task.session.TaskRole
import com.faraphel.tasks_valider.connectivity.task.session.TaskSession
import com.faraphel.tasks_valider.database.api.*
import com.faraphel.tasks_valider.database.api.base.BaseApi
@ -83,10 +84,11 @@ abstract class TaskDatabase : RoomDatabase() {
)
// dispatch the request to the correct entity API
return when (method) {
when (method) {
// check if the data is in the database
NanoHTTPD.Method.HEAD -> {
if (taskSession.permission < TaskPermission.STUDENT)
// check the permission of the session
if (taskSession.role.permissions.contains(TaskPermission.READ))
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.FORBIDDEN,
"text/plain",
@ -97,7 +99,8 @@ abstract class TaskDatabase : RoomDatabase() {
}
// get the data from the database
NanoHTTPD.Method.GET -> {
if (taskSession.permission < TaskPermission.STUDENT)
// check the permission of the session
if (taskSession.role.permissions.contains(TaskPermission.READ))
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.FORBIDDEN,
"text/plain",
@ -108,7 +111,8 @@ abstract class TaskDatabase : RoomDatabase() {
}
// insert the data into the database
NanoHTTPD.Method.POST -> {
if (taskSession.permission < TaskPermission.TEACHER)
// check the permission of the session
if (taskSession.role.permissions.contains(TaskPermission.WRITE))
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.FORBIDDEN,
"text/plain",
@ -119,7 +123,8 @@ abstract class TaskDatabase : RoomDatabase() {
}
// delete the data from the database
NanoHTTPD.Method.DELETE -> {
if (taskSession.permission < TaskPermission.TEACHER)
// check the permission of the session
if (taskSession.role.permissions.contains(TaskPermission.WRITE))
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.FORBIDDEN,
"text/plain",