Http Server / Client communication #7

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

View file

@ -1,7 +1,9 @@
package com.faraphel.tasks_valider.connectivity.task package com.faraphel.tasks_valider.connectivity.task
import com.faraphel.tasks_valider.connectivity.task.api.TaskSessionManagerApi
import com.faraphel.tasks_valider.connectivity.task.session.TaskSessionManager import com.faraphel.tasks_valider.connectivity.task.session.TaskSessionManager
import com.faraphel.tasks_valider.database.TaskDatabase import com.faraphel.tasks_valider.database.TaskDatabase
import com.faraphel.tasks_valider.database.api.TaskDatabaseApi
import fi.iki.elonen.NanoHTTPD import fi.iki.elonen.NanoHTTPD
@ -16,50 +18,41 @@ class TaskServer(
) : NanoHTTPD(port) { ) : NanoHTTPD(port) {
private val sessionManager = TaskSessionManager() ///< the session manager private val sessionManager = TaskSessionManager() ///< the session manager
private val sessionManagerApi = TaskSessionManagerApi(this.sessionManager) ///< the api of the session manager
private val databaseApi = TaskDatabaseApi(this.database) ///< the api of the database
/**
* Handle an API request
* @param httpSession the http session
*/
override fun serve(httpSession: IHTTPSession): Response { override fun serve(httpSession: IHTTPSession): Response {
// get the session data of the client // get the session data of the client
val taskSession = this.sessionManager.getOrCreateSessionData(httpSession) val taskSession = this.sessionManager.getOrCreateSessionData(httpSession)
// get the method used
val method: Method = httpSession.method
// parse the url // parse the url
val uri: String = httpSession.uri.substring(1) // remove the first slash val uri: String = httpSession.uri.substring(1) // remove the first slash
val uriComponents = uri.split("/") val path = uri.split("/").toMutableList()
// get the type of the request from the uri // get the type of the request from the uri
val requestType = uriComponents.getOrNull(0) val requestType = path.removeFirstOrNull()
?: return newFixedLengthResponse( ?: return newFixedLengthResponse(
NanoHTTPD.Response.Status.BAD_REQUEST, NanoHTTPD.Response.Status.BAD_REQUEST,
"text/plain", "text/plain",
"Invalid request type" "Missing request type"
) )
// get the response from the correct part of the application // get the response from the correct part of the application
val response = when (requestType) { val response = when (requestType) {
// session requests // session requests
"sessions" -> "sessions" -> this.sessionManagerApi.handleRequest(taskSession, httpSession, path)
this.sessionManager.handleApiRequest(taskSession, httpSession, method)
// entities requests // entities requests
"entities" -> { "entities" -> return this.databaseApi.handleRequest(taskSession, httpSession, path)
val entityName = uriComponents.getOrNull(1)
?: return newFixedLengthResponse(
NanoHTTPD.Response.Status.BAD_REQUEST,
"text/plain",
"Invalid entity name"
)
// TODO(Faraphel): the uri arguments should be handled in the handleApiRequest
return this.database.handleApiRequest(taskSession, httpSession, method, entityName)
}
// invalid requests // invalid requests
else -> else ->
newFixedLengthResponse( newFixedLengthResponse(
NanoHTTPD.Response.Status.BAD_REQUEST, NanoHTTPD.Response.Status.BAD_REQUEST,
"text/plain", "text/plain",
"Invalid request type" "Unknown request type"
) )
} }

View file

@ -0,0 +1,139 @@
package com.faraphel.tasks_valider.connectivity.task.api
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.connectivity.task.session.TaskSessionManager
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import fi.iki.elonen.NanoHTTPD
/**
* the HTTP API for the session manager
*/
class TaskSessionManagerApi(private val sessionManager: TaskSessionManager) {
private val jsonParser = Gson() ///< the json parser
/**
* Handle a HTTP Api request
* @param taskSession the data of the client session
* @param httpSession the data of the http session
* @param path the path of the request
*/
fun handleRequest(
taskSession: TaskSession,
httpSession: NanoHTTPD.IHTTPSession,
path: MutableList<String>,
): NanoHTTPD.Response {
// get the target session id
val targetSessionId = path.removeFirstOrNull()
return if (targetSessionId == null) {
// no specific session targeted
this.handleRequestGeneric(taskSession, httpSession)
} else {
// a specific session is targeted
this.handleRequestSpecific(taskSession, httpSession, targetSessionId)
}
}
/**
* Handle a request with no specific session targeted
*/
private fun handleRequestGeneric(
taskSession: TaskSession,
httpSession: NanoHTTPD.IHTTPSession,
): NanoHTTPD.Response {
when (httpSession.method) {
// get all the session data
NanoHTTPD.Method.GET -> {
// check the permission of the session
if (taskSession.role.permissions.contains(TaskPermission.READ))
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.FORBIDDEN,
"text/plain",
"Forbidden"
)
// return the session data
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.OK,
"application/json",
jsonParser.toJson(taskSession)
)
}
// other action are limited
else -> {
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.METHOD_NOT_ALLOWED,
"text/plain",
"Unknown method"
)
}
}
}
private fun handleRequestSpecific(
taskSession: TaskSession,
httpSession: NanoHTTPD.IHTTPSession,
targetSessionId: String,
): NanoHTTPD.Response {
when (httpSession.method) {
// change a specific 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",
"You are not allowed to update a session"
)
// parse the content of the request
val targetSession = jsonParser.fromJson(
httpSession.inputStream.bufferedReader().readText(),
TaskSession::class.java
)
// update the session
this.sessionManager.setSessionData(targetSessionId, targetSession)
// success message
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.OK,
"text/plain",
"Session updated"
)
}
// delete the session
NanoHTTPD.Method.DELETE -> {
// check the permission of the session
if (taskSession.role.permissions.contains(TaskPermission.ADMIN))
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.FORBIDDEN,
"text/plain",
"You are not allowed to delete a session"
)
// delete the target session
this.sessionManager.deleteSessionData(targetSessionId)
// success message
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.OK,
"text/plain",
"Session deleted"
)
}
// ignore other methods
else -> {
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.METHOD_NOT_ALLOWED,
"text/plain",
"Invalid method"
)
}
}
}
}

View file

@ -10,15 +10,13 @@ import java.util.*
* The manager for the session system * The manager for the session system
*/ */
class TaskSessionManager { 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 private val sessions = mutableMapOf<String, TaskSession>() ///< sessions specific data
/** /**
* Create a new session * Create a new session
* @param session the data for the session (optional) * @param session the data for the session (optional)
* @param sessionId the session id to use (optional) * @param sessionId the session id to use (optional)
* @return a new session identifiant * @return a new session identifier
*/ */
fun newSessionData( fun newSessionData(
session: TaskSession = TaskSession(), session: TaskSession = TaskSession(),
@ -40,14 +38,31 @@ class TaskSessionManager {
} }
/** /**
* Get data from a session identifiant * Get data from a session identifier
* @param sessionId the identifiant of the session * @param sessionId the identifier of the session
* @return the session data * @return the session data
*/ */
fun getSessionData(sessionId: String): TaskSession? { fun getSessionData(sessionId: String): TaskSession? {
return this.sessions[sessionId] return this.sessions[sessionId]
} }
/**
* Set the data of a session
* @param sessionId the identifier of the session
* @param session the session data
*/
fun setSessionData(sessionId: String, session: TaskSession) {
this.sessions[sessionId] = session
}
/**
* Delete a session
* @param sessionId the identifier of the session
*/
fun deleteSessionData(sessionId: String): TaskSession? {
return this.sessions.remove(sessionId)
}
/** /**
* Get data from a http session. If it does not exist, create it. * Get data from a http session. If it does not exist, create it.
* @param httpSession the HTTP session * @param httpSession the HTTP session
@ -75,86 +90,11 @@ class TaskSessionManager {
response: NanoHTTPD.Response, response: NanoHTTPD.Response,
cookies: NanoHTTPD.CookieHandler cookies: NanoHTTPD.CookieHandler
): NanoHTTPD.Response { ): NanoHTTPD.Response {
// update the cookie of the user
cookies.set(NanoHTTPD.Cookie("sessionId", this.newSessionData())) cookies.set(NanoHTTPD.Cookie("sessionId", this.newSessionData()))
// load them in the response
cookies.unloadQueue(response) cookies.unloadQueue(response)
return 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 -> {
// check the permission of the session
if (taskSession.role.permissions.contains(TaskPermission.READ))
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.FORBIDDEN,
"text/plain",
"Forbidden"
)
// return the session data
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.OK,
"application/json",
jsonParser.toJson(taskSession)
)
}
// 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,10 +4,9 @@ import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverters import androidx.room.TypeConverters
import com.faraphel.tasks_valider.connectivity.task.session.TaskPermission 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.connectivity.task.session.TaskSession
import com.faraphel.tasks_valider.database.api.* import com.faraphel.tasks_valider.database.api.entities.base.BaseApi
import com.faraphel.tasks_valider.database.api.base.BaseApi import com.faraphel.tasks_valider.database.api.entities.*
import com.faraphel.tasks_valider.database.converters.InstantConverter import com.faraphel.tasks_valider.database.converters.InstantConverter
import com.faraphel.tasks_valider.database.dao.GroupDao import com.faraphel.tasks_valider.database.dao.GroupDao
import com.faraphel.tasks_valider.database.dao.GroupStudentDao import com.faraphel.tasks_valider.database.dao.GroupStudentDao
@ -44,8 +43,6 @@ import fi.iki.elonen.NanoHTTPD
InstantConverter::class InstantConverter::class
) )
abstract class TaskDatabase : RoomDatabase() { abstract class TaskDatabase : RoomDatabase() {
// entities
abstract fun groupDao(): GroupDao abstract fun groupDao(): GroupDao
abstract fun studentDao(): StudentDao abstract fun studentDao(): StudentDao
abstract fun teacherDao(): TeacherDao abstract fun teacherDao(): TeacherDao
@ -53,93 +50,4 @@ abstract class TaskDatabase : RoomDatabase() {
abstract fun groupStudentDao(): GroupStudentDao abstract fun groupStudentDao(): GroupStudentDao
abstract fun taskGroupDao(): TaskGroupDao abstract fun taskGroupDao(): TaskGroupDao
// api
private val api: Map<String, BaseApi> = mapOf(
"group" to GroupApi(this.groupDao()),
"student" to StudentApi(this.studentDao()),
"teacher" to TeacherApi(this.teacherDao()),
"task" to TaskApi(this.taskDao()),
"group_student" to GroupStudentApi(this.groupStudentDao()),
"task_group" to TaskGroupApi(this.taskGroupDao()),
)
/**
* dispatch an API request
*/
fun handleApiRequest(
taskSession: TaskSession,
httpSession: NanoHTTPD.IHTTPSession,
method: NanoHTTPD.Method,
entityName: String
): NanoHTTPD.Response {
// get the correspond Api object for this entity
val entityApi = this.api[entityName]
?: return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.NOT_FOUND,
"text/plain",
"Invalid entity name"
)
// dispatch the request to the correct entity API
when (method) {
// check if the data is in the database
NanoHTTPD.Method.HEAD -> {
// check the permission of the session
if (taskSession.role.permissions.contains(TaskPermission.READ))
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.FORBIDDEN,
"text/plain",
"Forbidden"
)
return entityApi.head(httpSession)
}
// get the data from the database
NanoHTTPD.Method.GET -> {
// check the permission of the session
if (taskSession.role.permissions.contains(TaskPermission.READ))
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.FORBIDDEN,
"text/plain",
"Forbidden"
)
return entityApi.get(httpSession)
}
// insert the data into the database
NanoHTTPD.Method.POST -> {
// check the permission of the session
if (taskSession.role.permissions.contains(TaskPermission.WRITE))
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.FORBIDDEN,
"text/plain",
"Forbidden"
)
return entityApi.post(httpSession)
}
// delete the data from the database
NanoHTTPD.Method.DELETE -> {
// check the permission of the session
if (taskSession.role.permissions.contains(TaskPermission.WRITE))
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.FORBIDDEN,
"text/plain",
"Forbidden"
)
return entityApi.delete(httpSession)
}
// other methods are not allowed
else ->
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.METHOD_NOT_ALLOWED,
"text/plain",
"Method not allowed"
)
}
}
} }

View file

@ -0,0 +1,108 @@
package com.faraphel.tasks_valider.database.api
import com.faraphel.tasks_valider.connectivity.task.session.TaskPermission
import com.faraphel.tasks_valider.connectivity.task.session.TaskSession
import com.faraphel.tasks_valider.database.TaskDatabase
import com.faraphel.tasks_valider.database.api.entities.*
import com.faraphel.tasks_valider.database.api.entities.base.BaseApi
import fi.iki.elonen.NanoHTTPD
class TaskDatabaseApi(private val database: TaskDatabase) {
private val api: Map<String, BaseApi> = mapOf(
"group" to GroupApi(this.database.groupDao()),
"student" to StudentApi(this.database.studentDao()),
"teacher" to TeacherApi(this.database.teacherDao()),
"task" to TaskApi(this.database.taskDao()),
"group_student" to GroupStudentApi(this.database.groupStudentDao()),
"task_group" to TaskGroupApi(this.database.taskGroupDao()),
)
/**
* handle an API request
* @param taskSession the current user session
* @param httpSession the http session
* @param path the path of the request
*/
fun handleRequest(
taskSession: TaskSession,
httpSession: NanoHTTPD.IHTTPSession,
path: MutableList<String>
): NanoHTTPD.Response {
// get the entity name
val entityName = path.removeFirstOrNull()
?: return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.BAD_REQUEST,
"text/plain",
"Missing entity name"
)
// get the correspond Api object for this entity
val entityApi = this.api[entityName]
?: return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.NOT_FOUND,
"text/plain",
"Unknown entity name"
)
// dispatch the request to the correct entity API
when (httpSession.method) {
// check if the data is in the database
// TODO(Faraphel): should only be allowed to read data concerning the current class session
NanoHTTPD.Method.HEAD -> {
// check the permission of the session
if (taskSession.role.permissions.contains(TaskPermission.READ))
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.FORBIDDEN,
"text/plain",
"Forbidden"
)
return entityApi.head(httpSession)
}
// get the data from the database
NanoHTTPD.Method.GET -> {
// check the permission of the session
if (taskSession.role.permissions.contains(TaskPermission.READ))
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.FORBIDDEN,
"text/plain",
"Forbidden"
)
return entityApi.get(httpSession)
}
// insert the data into the database
NanoHTTPD.Method.POST -> {
// check the permission of the session
if (taskSession.role.permissions.contains(TaskPermission.WRITE))
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.FORBIDDEN,
"text/plain",
"Forbidden"
)
return entityApi.post(httpSession)
}
// delete the data from the database
NanoHTTPD.Method.DELETE -> {
// check the permission of the session
if (taskSession.role.permissions.contains(TaskPermission.WRITE))
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.FORBIDDEN,
"text/plain",
"Forbidden"
)
return entityApi.delete(httpSession)
}
// other methods are not allowed
else ->
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.METHOD_NOT_ALLOWED,
"text/plain",
"Method not allowed"
)
}
}
}

View file

@ -1,6 +1,6 @@
package com.faraphel.tasks_valider.database.api package com.faraphel.tasks_valider.database.api.entities
import com.faraphel.tasks_valider.database.api.base.BaseJsonApi import com.faraphel.tasks_valider.database.api.entities.base.BaseJsonApi
import com.faraphel.tasks_valider.database.dao.base.BaseDao import com.faraphel.tasks_valider.database.dao.base.BaseDao
import com.faraphel.tasks_valider.database.entities.GroupEntity import com.faraphel.tasks_valider.database.entities.GroupEntity

View file

@ -1,6 +1,6 @@
package com.faraphel.tasks_valider.database.api package com.faraphel.tasks_valider.database.api.entities
import com.faraphel.tasks_valider.database.api.base.BaseJsonApi import com.faraphel.tasks_valider.database.api.entities.base.BaseJsonApi
import com.faraphel.tasks_valider.database.dao.base.BaseDao import com.faraphel.tasks_valider.database.dao.base.BaseDao
import com.faraphel.tasks_valider.database.entities.GroupStudentEntity import com.faraphel.tasks_valider.database.entities.GroupStudentEntity

View file

@ -1,6 +1,6 @@
package com.faraphel.tasks_valider.database.api package com.faraphel.tasks_valider.database.api.entities
import com.faraphel.tasks_valider.database.api.base.BaseJsonApi import com.faraphel.tasks_valider.database.api.entities.base.BaseJsonApi
import com.faraphel.tasks_valider.database.dao.base.BaseDao import com.faraphel.tasks_valider.database.dao.base.BaseDao
import com.faraphel.tasks_valider.database.entities.StudentEntity import com.faraphel.tasks_valider.database.entities.StudentEntity

View file

@ -1,6 +1,6 @@
package com.faraphel.tasks_valider.database.api package com.faraphel.tasks_valider.database.api.entities
import com.faraphel.tasks_valider.database.api.base.BaseJsonApi import com.faraphel.tasks_valider.database.api.entities.base.BaseJsonApi
import com.faraphel.tasks_valider.database.dao.base.BaseDao import com.faraphel.tasks_valider.database.dao.base.BaseDao
import com.faraphel.tasks_valider.database.entities.TaskEntity import com.faraphel.tasks_valider.database.entities.TaskEntity

View file

@ -1,6 +1,6 @@
package com.faraphel.tasks_valider.database.api package com.faraphel.tasks_valider.database.api.entities
import com.faraphel.tasks_valider.database.api.base.BaseJsonApi import com.faraphel.tasks_valider.database.api.entities.base.BaseJsonApi
import com.faraphel.tasks_valider.database.dao.base.BaseDao import com.faraphel.tasks_valider.database.dao.base.BaseDao
import com.faraphel.tasks_valider.database.entities.TaskGroupEntity import com.faraphel.tasks_valider.database.entities.TaskGroupEntity

View file

@ -1,6 +1,6 @@
package com.faraphel.tasks_valider.database.api package com.faraphel.tasks_valider.database.api.entities
import com.faraphel.tasks_valider.database.api.base.BaseJsonApi import com.faraphel.tasks_valider.database.api.entities.base.BaseJsonApi
import com.faraphel.tasks_valider.database.dao.base.BaseDao import com.faraphel.tasks_valider.database.dao.base.BaseDao
import com.faraphel.tasks_valider.database.entities.TeacherEntity import com.faraphel.tasks_valider.database.entities.TeacherEntity

View file

@ -1,4 +1,4 @@
package com.faraphel.tasks_valider.database.api.base package com.faraphel.tasks_valider.database.api.entities.base
import fi.iki.elonen.NanoHTTPD import fi.iki.elonen.NanoHTTPD

View file

@ -1,4 +1,4 @@
package com.faraphel.tasks_valider.database.api.base package com.faraphel.tasks_valider.database.api.entities.base
import com.faraphel.tasks_valider.database.dao.base.BaseDao import com.faraphel.tasks_valider.database.dao.base.BaseDao
import com.faraphel.tasks_valider.database.entities.base.BaseEntity import com.faraphel.tasks_valider.database.entities.base.BaseEntity