HTTPServer/ultraOverEngenieeredHTTPserver.cxx
2023-10-20 17:25:17 +02:00

446 lines
14 KiB
C++

#include <arpa/inet.h>
#include <fcntl.h>
#include <ios>
#include <iostream>
#include <map>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <sys/socket.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#define BACKLOG 128
#define NB_CLIENTS 1000
#define TAILLE_BUFFER 512
#define MAXKEEPALIVE 2
#define DEBUG true
#define debugPrint(X) if (DEBUG) cout << X << endl
using namespace std;
class SocketReader {
private:
/*!
* @brief socket a lire
*/
int socketfd;
/*!
* @brief nombre d'octect lu a chaque tour de boucle
*/
char msgRead[TAILLE_BUFFER];
/*!
* @brief la position du premier élément de l'intervalle de recherche de "\n"
*/
int start;
/*!
* @brief la taille de l'intervalle de recherhce de \n
*/
int count;
public:
SocketReader(int socketfd) {
count = 0;
this->socketfd = socketfd;
}
/*!
* @brief La fonction readline prends le fd d'un socket et une string en
* paramettre, et y met au max TAILLE_BUFFER caracteres, si les
* données lues conntiennt le caractere "\n", les données sont trunc
* jusqua le "\n" et les données suivantes seront disponible lors de
* la prochaine lecture.
*
* NOTE : J'ai legerement modififé la classe pour qu'elle utilise le
* socket donné au constructeur, et pour qu'elle ne retourne
* pas le "\r" avant chaque "\n".
*
* @param[out] line : le string a remplir avec les données du socket,
* qui contiendra les donnée de la position actuelle
* jusqu'au prochain "\n"
*
*/
int readLine(string &line) {
line = "";
int i;
while (true) {
if (count == 0) {
count = read(this->socketfd, msgRead, sizeof(msgRead));
if (count == -1)
return -1; // cas d'erreur
if (count == 0)
return 0; // cas de deconnexion
start = 0;
}
for (i = start; i < count; i++)
if (msgRead[i] == '\n')
break;
if (i < count) { // on a trouvé /n
line = line + string(msgRead, start, (i - 1) - start);
if (i == count - 1)
count = 0;
else
start = i + 1;
break;
} else {
// on n'a pas trouvé le \n
line = line + string(msgRead, start, (i - 1) - start + 1);
count = 0;
}
}
return line.size();
}
};// SocketReader
void exitErreur(const char *msg) {
perror(msg);
exit(EXIT_FAILURE);
}
/*!
* @brief : la fonction parseRequest permet de recuperer les elements de la
* premiere ligne d'une requete HTTP, c'est a dire :
* - le type de la requete (GET,POST,PUT,...)
* - la ressource demandée par le client
* - et la version du protocol HTTP du client
*
* @param[in,out] line : le string contenant la premiere ligne de la requete
* @param[out] requestType : le string qui va contenir le type de la requete
* @param[out] requestedRessource : le string qui va contenir la ressource
* demandée par la requete
* @param[out] protocolVersion : le string qui va contenir la version du
* proctol HTTP demandée par le client
*
*/
void parseRequest(string line, string &requestType, string &requestedRessource,
string &protocolVersion) {
string delimiter = " ";
size_t pos = 0;
pos = line.find(delimiter);
requestType = line.substr(0, pos);
debugPrint("[DEBUG] : parsed request type : " << requestType);
line.erase(0, pos + delimiter.length());
pos = line.find(delimiter);
requestedRessource = line.substr(0, pos);
debugPrint("[DEBUG] : parsed requested ressource : " << requestedRessource);
line.erase(0, pos + delimiter.length());
protocolVersion = line;
debugPrint("[DEBUG] : parsed protocol version : " << protocolVersion);
}// parseRequest()
/*!
* @brief : la fonction parseHeaders intervient apres que la premiere ligne de
* la requete, elle lit et stoque les en-tetes de la requette client
* dans une hashmap
*
* @param[in] sr : SocketReader initialisé avec le socket du client
* @return : une hashmap contenant les en-tete client ainsi que leur valeurs
*
*/
map<string, string> parseHeaders(SocketReader &sr) {
map<string, string> headers;
string delimiter = ": ";
string ligne;
sr.readLine(ligne);
while ("" != ligne) {
size_t pos = ligne.find(delimiter);
string key = ligne.substr(0, pos);
ligne.erase(0, pos + delimiter.length());
string value = ligne;
debugPrint("[DEBUG] : parsed header : " << key << " : " << value);
headers[key] = value;
sr.readLine(ligne);
}
return headers;
}// parseHeaders()
/*!
* @brief : la fonction sendError sert a envoyer une reponse erreur au client
* @param[in] socketfd : file descriptor du socket client a qui envoyer
* l'erreur
* @param[in] errorCode : code erreur de la reponse a envoyer au client
*/
void sendError(const int socketfd, const int errorCode) {
string headers;
string message;
debugPrint("[DEBUG] : sending error : " << errorCode);
switch (errorCode) {
// erreur 404 : fichier pas trouvé
case 404:
message = "<html><body><h1>The requested URL was Not "
"Found</h1></body></html>\r\n\r\n";
headers = "HTTP/1.1 404 Not Found\nServer: Djalim's Server (it uses arch "
"btw)\nContent-Length: " +
to_string(message.size()) + "\r\n\r\n";
write(socketfd, headers.c_str(), headers.size());
write(socketfd, message.c_str(), message.size());
break;
// erreur 501 : fonctionnalité pas implementée
case 501:
message = "<html><body><h1>Not Implemented</h1></body></html>\r\n\r\n";
headers = "HTTP/1.1 501 Not Implemented\nServer: Djalim's Server (it uses "
"arch btw)\nContent-Length: " +
to_string(message.size()) + "\r\n\r\n";
write(socketfd, headers.c_str(), headers.size());
write(socketfd, message.c_str(), message.size());
break;
// erreur 505 : version HTTP incompatible/ pas supportée
case 505:
message =
"<html><body><h1>unsupported HTTP Version</h1></body></html>\r\n\r\n";
headers = "HTTP/1.1 505 Version Not Supported\nServer: Djalim's Server (it "
"uses arch btw)\nContent-Length: " +
to_string(message.size()) + "\r\n\r\n";
write(socketfd, headers.c_str(), headers.size());
write(socketfd, message.c_str(), message.size());
break;
// TODO : ajouter les autres erreurs existantes
};
}// sendError()
/*!
* @brief : la fonction sendFile sert a envoyer un fichier au client, cette
fonction part du principe que le fichier a envoyer existe deja, il
faut donc verifier l'existance du fichier avant l'appel de cette
fonction.
* @param[in] socketfd : file descriptor du socket client a qui envoyer
* le fichier
* @param[in] filePath : chemin du fichier a envoyer
* @param[in] fileStat : structure stat associé au fichier a envoyer
* @param[in] sendContent : Le booleen sendContent permet d'indiquer a cette
* fonction si elle doit envoyer uniquement l'en-tete
* ou non
*
*/
void sendFile(const int socketfd, const string filePath,
const struct stat &fileStat, const bool sendContent) {
debugPrint("[DEBUG] : sending file : " << filePath);
int file = open(filePath.c_str(), O_RDONLY);
char *fileContent;
string responseHeaders;
responseHeaders = "HTTP/1.1 200 OK\nServer: Djalim's Server (it uses arch "
"btw)\nContent-Length: " +
to_string(fileStat.st_size) + "\r\n\r\n";
write(socketfd, responseHeaders.c_str(), responseHeaders.size());
if (sendContent) {
read(file, &fileContent, fileStat.st_size);
write(socketfd, &fileContent, fileStat.st_size);
write(socketfd, "\r\n\r\n", 5);
}
close(file);
}// sendFile()
/*!
* @brief : La fonction getHandler gere les requetes GET recue par le serveur
* elle verifie l'existance de la ressource demandée et renvoie
* la reponse appropriée.
* @param[in] socketfd : socket du client a qui envoyer la reponse
* @param[in,out] requestedRessource : ressource demandée par le client, elle
* peut etre affecté a "/index" si la
* ressource demandée et "/"
* @param[in] requestHeaders : en-tetes de la requette du client
*
*/
void getHandler(const int socketfd, string requestedRessource,
const map<string, string> requestHeaders) {
debugPrint("[DEBUG] : handling a GET request");
if ("/" == requestedRessource)
requestedRessource = "/index.html";
string filePath = "." + requestedRessource;
struct stat fileStat;
debugPrint("[DEBUG] : Searching for file " << filePath);
if (-1 == stat(filePath.c_str(), &fileStat)) {
debugPrint("[ERROR] : could not stat file");
sendError(socketfd, 404);
return;
}
sendFile(socketfd, filePath, fileStat, true);
} // getHandler()
/*!
* @brief : La fonction getHandler gere les requetes HEAD recue par le serveur
* elle verifie l'existance de la ressource demandée et renvoie
* la reponse appropriée.
* @param[in] socketfd : socket du client a qui envoyer la reponse
* @param[in,out] requestedRessource : ressource demandée par le client, elle
* peut etre affecté a "/index" si la
* ressource demandée et "/"
* @param[in] requestHeaders : en-tetes de la requette du client
*
*/
void headHandler(const int socketfd, string requestedRessource,
const map<string, string> requestHeaders) {
debugPrint("[DEBUG] : handling a HEAD request");
if ("/" == requestedRessource)
requestedRessource = "/index.html";
string filePath = "." + requestedRessource;
struct stat fileStat;
debugPrint("[DEBUG] : Searching for file " << filePath);
if (-1 == stat(filePath.c_str(), &fileStat)) {
debugPrint("[ERROR] : could not stat file");
sendError(socketfd, 404);
return;
}
sendFile(socketfd, filePath, fileStat, false);
}// headHandler()
/*!
* @brief : La fonction getHandler gere les requetes POST recue par le serveur,
* actuellement pas implémenté, cette fonction retourne une erreur
* 501 au client
* @param[in] socketfd : socket du client a qui envoyer la reponse
*
*/
void postHandler(const int socketfd) {
debugPrint("[DEBUG] : handling a POST request");
sendError(socketfd, 501);
}
/*!
* @brief : La fonction getHandler gere les requetes PUT recue par le serveur,
* actuellement pas implémenté, cette fonction retourne une erreur
* 501 au client
* @param[in] socketfd : socket du client a qui envoyer la reponse
*
*/
void putHandler(const int socketfd) {
debugPrint("[DEBUG] : handling a PUT request");
sendError(socketfd, 501);
}
/*!
* @brief : La fonction getHandler gere les requetes DELETE recue par le
* serveur, actuellement pas implémenté, cette fonction retourne une
* erreur 501 au client
* @param[in] socketfd : socket du client a qui envoyer la reponse
*
*/
void deleteHandler(const int socketfd) {
debugPrint("[DEBUG] : handling a DELETE request");
sendError(socketfd, 501);
}
int main(int argc, char *argv[]) {
if (argc != 2)
exitErreur("[FATAL ERROR] : missing port");
int sock_serveur = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in sockaddr_serveur;
sockaddr_serveur.sin_family = AF_INET;
sockaddr_serveur.sin_port = htons(atoi(argv[1]));
//TODO faire en sorte que l'ip peut etre choisi en paramettre du serveur
inet_aton("127.0.0.1", &sockaddr_serveur.sin_addr);
int yes = 1;
if (setsockopt(sock_serveur, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) ==
-1)
exitErreur("[FATAL ERROR] : setsockopt");
if (bind(sock_serveur, (struct sockaddr *)&sockaddr_serveur,
sizeof(sockaddr_in)) == -1)
exitErreur("[FATAL ERROR] : bind");
if (listen(sock_serveur, BACKLOG) == -1)
exitErreur("[FATAL ERROR] : listen");
int sock_client;
string msgRecu("");
debugPrint("[DEBUG] : Started server");
char* ip;
debugPrint("[DEBUG] : listening on "<< inet_ntoa(sockaddr_serveur.sin_addr));
const map<string, int> types;
for (int i = 1; i <= NB_CLIENTS; i++) {
// Le serveur attend un client
struct sockaddr_in sockaddr_client;
socklen_t a;
sock_client = accept(sock_serveur, (struct sockaddr*)&sockaddr_client, &a);
if (sock_client == -1)
exitErreur("[FATAL ERROR] : accept");
int p = fork();
if (!p) { // processus fils
debugPrint("[DEBUG] : "<< inet_ntoa(sockaddr_client.sin_addr) << " connected" );
close(sock_serveur);
SocketReader sr(sock_client);
for (int i = 1; i <= MAXKEEPALIVE; i++) {
string ligne;
int n = sr.readLine(ligne);
// client s'est déconnecté
if (!n) {
debugPrint("[DEBUG] : Client deconnected");
break;
}
// cas d'erreur
if (n == -1){
debugPrint("[FATAL ERROR] : Could not read line");
break;
}
string requestType, requestedRessource, protocolVersion;
if ("" != ligne)
parseRequest(ligne, requestType, requestedRessource, protocolVersion);
map<string, string> headers = parseHeaders(sr);
//TODO faire un truc avec les en-tete parsées
if ("HTTP/1.1" != protocolVersion) {
sendError(sock_client, 505);
}
// pas tres elegant mais fonctionnel :)
if ("GET" == requestType)
getHandler(sock_client, requestedRessource, headers);
else if ("HEAD" == requestType)
headHandler(sock_client, requestedRessource, headers);
else if ("POST" == requestType)
postHandler(sock_client);
else if ("PUT" == requestType)
putHandler(sock_client);
else if ("DELETE" == requestType)
deleteHandler(sock_client);
if (MAXKEEPALIVE == i)
debugPrint("[DEBUG] : reached max keep alive requests");
}
debugPrint("[DEBUG] : closing connection with client");
close(sock_client);
exit(EXIT_SUCCESS);
}
close(sock_client);
}
debugPrint("[DEBUG] : Max client reached, terminating server");
close(sock_serveur);
return EXIT_SUCCESS;
}// main()