123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435 |
- #include "HTTPServer.h"
- bell::HTTPServer::HTTPServer(int serverPort) { this->serverPort = serverPort; }
- unsigned char bell::HTTPServer::h2int(char c) {
- if (c >= '0' && c <= '9') {
- return ((unsigned char)c - '0');
- }
- if (c >= 'a' && c <= 'f') {
- return ((unsigned char)c - 'a' + 10);
- }
- if (c >= 'A' && c <= 'F') {
- return ((unsigned char)c - 'A' + 10);
- }
- return (0);
- }
- std::string bell::HTTPServer::urlDecode(std::string str) {
- std::string encodedString = "";
- char c;
- char code0;
- char code1;
- for (int i = 0; i < str.length(); i++) {
- c = str[i];
- if (c == '+') {
- encodedString += ' ';
- } else if (c == '%') {
- i++;
- code0 = str[i];
- i++;
- code1 = str[i];
- c = (h2int(code0) << 4) | h2int(code1);
- encodedString += c;
- } else {
- encodedString += c;
- }
- }
- return encodedString;
- }
- std::vector<std::string> bell::HTTPServer::splitUrl(const std::string &url,
- char delimiter) {
- std::stringstream ssb(url);
- std::string segment;
- std::vector<std::string> seglist;
- while (std::getline(ssb, segment, delimiter)) {
- seglist.push_back(segment);
- }
- return seglist;
- }
- void bell::HTTPServer::registerHandler(RequestType requestType,
- const std::string &routeUrl,
- httpHandler handler) {
- if (routes.find(routeUrl) == routes.end()) {
- routes.insert({routeUrl, std::vector<HTTPRoute>()});
- }
- this->routes[routeUrl].push_back(HTTPRoute{
- .requestType = requestType,
- .handler = handler,
- });
- }
- void bell::HTTPServer::listen() {
- BELL_LOG(info, "http", "Starting configuration server at port %d",
- this->serverPort);
- // setup address
- struct addrinfo hints, *server;
- memset(&hints, 0, sizeof hints);
- hints.ai_family = AF_INET;
- hints.ai_socktype = SOCK_STREAM;
- hints.ai_flags = AI_PASSIVE;
- getaddrinfo(NULL, std::to_string(serverPort).c_str(), &hints, &server);
- int sockfd =
- socket(server->ai_family, server->ai_socktype, server->ai_protocol);
- struct sockaddr_in clientname;
- socklen_t incomingSockSize;
- int i;
- int yes = true;
- setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int));
- bind(sockfd, server->ai_addr, server->ai_addrlen);
- ::listen(sockfd, 10);
- FD_ZERO(&activeFdSet);
- FD_SET(sockfd, &activeFdSet);
- for (;;) {
- /* Block until input arrives on one or more active sockets. */
- readFdSet = activeFdSet;
- struct timeval tv = {0, 100000};
- if (select(FD_SETSIZE, &readFdSet, NULL, NULL, &tv) < 0) {
- BELL_LOG(error, "http", "Error in select");
- perror("select");
- // exit(EXIT_FAILURE);
- }
- /* Service all the sockets with input pending. */
- for (i = 0; i < FD_SETSIZE; ++i)
- if (FD_ISSET(i, &readFdSet)) {
- if (i == sockfd) {
- /* Connection request on original socket. */
- int newFd;
- incomingSockSize = sizeof(clientname);
- newFd = accept(sockfd, (struct sockaddr *)&clientname,
- &incomingSockSize);
- if (newFd < 0) {
- perror("accept");
- exit(EXIT_FAILURE);
- }
- FD_SET(newFd, &activeFdSet);
- HTTPConnection conn = {.buffer = std::vector<uint8_t>(128),
- .httpMethod = ""};
- this->connections.insert({newFd, conn});
- } else {
- /* Data arriving on an already-connected socket. */
- readFromClient(i);
- }
- }
- for (auto it = this->connections.cbegin();
- it != this->connections.cend() /* not hoisted */;
- /* no increment */) {
- if ((*it).second.toBeClosed) {
- close((*it).first);
- FD_CLR((*it).first, &activeFdSet);
- this->connections.erase(
- it++); // or "it = m.erase(it)" since C++11
- } else {
- ++it;
- }
- }
- }
- }
- void bell::HTTPServer::readFromClient(int clientFd) {
- HTTPConnection &conn = this->connections[clientFd];
- int nbytes = recv(clientFd, &conn.buffer[0], conn.buffer.size(), 0);
- if (nbytes < 0) {
- BELL_LOG(error, "http", "Error reading from client");
- perror("recv");
- this->closeConnection(clientFd);
- } else if (nbytes == 0) {
- this->closeConnection(clientFd);
- } else {
- conn.currentLine +=
- std::string(conn.buffer.data(), conn.buffer.data() + nbytes);
- READBODY:
- if (!conn.isReadingBody) {
- while (conn.currentLine.find("\r\n") != std::string::npos) {
- auto line =
- conn.currentLine.substr(0, conn.currentLine.find("\r\n"));
- conn.currentLine = conn.currentLine.substr(
- conn.currentLine.find("\r\n") + 2, conn.currentLine.size());
- if (line.find("GET ") != std::string::npos ||
- line.find("POST ") != std::string::npos ||
- line.find("OPTIONS ") != std::string::npos) {
- conn.httpMethod = line;
- }
- if (line.find("Content-Length: ") != std::string::npos) {
- conn.contentLength =
- std::stoi(line.substr(16, line.size() - 1));
- BELL_LOG(info, "http", "Content-Length: %d",
- conn.contentLength);
- }
- if (line.size() == 0) {
- if (conn.contentLength != 0) {
- conn.isReadingBody = true;
- goto READBODY;
- } else {
- findAndHandleRoute(conn.httpMethod, conn.currentLine,
- clientFd);
- }
- }
- }
- } else {
- if (conn.currentLine.size() >= conn.contentLength) {
- findAndHandleRoute(conn.httpMethod, conn.currentLine, clientFd);
- }
- }
- }
- }
- void bell::HTTPServer::closeConnection(int connection) {
- this->connections[connection].toBeClosed = true;
- }
- void bell::HTTPServer::writeResponseEvents(int connFd) {
- std::lock_guard lock(this->responseMutex);
- std::stringstream stream;
- stream << "HTTP/1.1 200 OK\r\n";
- stream << "Server: EUPHONIUM\r\n";
- stream << "Connection: keep-alive\r\n";
- stream << "Content-type: text/event-stream\r\n";
- stream << "Cache-Control: no-cache\r\n";
- stream << "Access-Control-Allow-Origin: *\r\n";
- stream << "Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE, "
- "OPTIONS\r\n";
- stream << "Access-Control-Allow-Headers: Origin, Content-Type, "
- "X-Auth-Token\r\n";
- stream << "\r\n";
- auto responseStr = stream.str();
- write(connFd, responseStr.c_str(), responseStr.size());
- this->connections[connFd].isEventConnection = true;
- }
- void bell::HTTPServer::writeResponse(const HTTPResponse &response) {
- std::lock_guard lock(this->responseMutex);
- auto fileSize = response.body.size();
- if (response.responseReader != nullptr) {
- fileSize = response.responseReader->getTotalSize();
- }
- std::stringstream stream;
- stream << "HTTP/1.1 " << response.status << " OK\r\n";
- stream << "Server: EUPHONIUM\r\n";
- stream << "Connection: close\r\n";
- stream << "Content-type: " << response.contentType << "\r\n";
- if (response.useGzip) {
- stream << "Content-encoding: gzip"
- << "\r\n";
- }
- stream << "Access-Control-Allow-Origin: *\r\n";
- stream << "Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS\r\n";
- stream << "Access-Control-Expose-Headers: Location\r\n";
- stream << "Access-Control-Allow-Headers: Origin, Content-Type, "
- "X-Auth-Token\r\n";
- // go over every item in request->extraHeaders
- for (auto &header : response.extraHeaders) {
- stream << header << "\r\n";
- }
- stream << "\r\n";
- if (response.body.size() > 0) {
- stream << response.body;
- }
- auto responseStr = stream.str();
- write(response.connectionFd, responseStr.c_str(), responseStr.size());
- if (response.responseReader != nullptr) {
- size_t read;
- do {
- read = response.responseReader->read(responseBuffer.data(),
- responseBuffer.size());
- if (read > 0) {
- write(response.connectionFd, responseBuffer.data(), read);
- }
- } while (read > 0);
- }
- BELL_LOG(info, "HTTP", "Closing connection");
- this->closeConnection(response.connectionFd);
- }
- void bell::HTTPServer::respond(const HTTPResponse &response) {
- writeResponse(response);
- }
- void bell::HTTPServer::redirectTo(const std::string & url, int connectionFd) {
- std::lock_guard lock(this->responseMutex);
- std::stringstream stream;
- stream << "HTTP/1.1 301 Moved Permanently\r\n";
- stream << "Server: EUPHONIUM\r\n";
- stream << "Connection: close\r\n";
- stream << "Location: " << url << "\r\n\r\n";
- auto responseStr = stream.str();
- write(connectionFd, responseStr.c_str(), responseStr.size());
- this->closeConnection(connectionFd);
- }
- void bell::HTTPServer::publishEvent(std::string eventName,
- std::string eventData) {
- std::lock_guard lock(this->responseMutex);
- BELL_LOG(info, "http", "Publishing event");
- std::stringstream stream;
- stream << "event: " << eventName << "\n";
- stream << "data: " << eventData << "\n\n";
- auto responseStr = stream.str();
- // Reply to all event-connections
- for (auto it = this->connections.cbegin(); it != this->connections.cend();
- ++it) {
- if ((*it).second.isEventConnection) {
- write(it->first, responseStr.c_str(), responseStr.size());
- }
- }
- }
- std::map<std::string, std::string>
- bell::HTTPServer::parseQueryString(const std::string &queryString) {
- std::map<std::string, std::string> query;
- auto prefixedString = "&" + queryString;
- while (prefixedString.find("&") != std::string::npos) {
- auto keyStart = prefixedString.find("&");
- auto keyEnd = prefixedString.find("=");
- // Find second occurence of "&" in prefixedString
- auto valueEnd = prefixedString.find("&", keyStart + 1);
- if (valueEnd == std::string::npos) {
- valueEnd = prefixedString.size();
- }
- auto key = prefixedString.substr(keyStart + 1, keyEnd - 1);
- auto value = prefixedString.substr(keyEnd + 1, valueEnd - keyEnd - 1);
- query[key] = urlDecode(value);
- prefixedString = prefixedString.substr(valueEnd);
- }
- return query;
- }
- void bell::HTTPServer::findAndHandleRoute(std::string &url, std::string &body,
- int connectionFd) {
- std::map<std::string, std::string> pathParams;
- std::map<std::string, std::string> queryParams;
- BELL_LOG(info, "http", "URL %s", url.c_str());
- if (url.find("OPTIONS /") != std::string::npos) {
- std::stringstream stream;
- stream << "HTTP/1.1 200 OK\r\n";
- stream << "Server: EUPHONIUM\r\n";
- stream << "Allow: OPTIONS, GET, HEAD, POST\r\n";
- stream << "Connection: close\r\n";
- stream << "Access-Control-Allow-Origin: *\r\n";
- stream << "Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n";
- stream << "Access-Control-Allow-Headers: Origin, Content-Type, "
- "X-Auth-Token\r\n";
- stream << "\r\n";
- auto responseStr = stream.str();
- write(connectionFd, responseStr.c_str(), responseStr.size());
- closeConnection(connectionFd);
- return;
- }
- if (url.find("GET /events") != std::string::npos) {
- // Handle SSE endpoint here
- writeResponseEvents(connectionFd);
- return;
- }
- for (const auto &routeSet : this->routes) {
- for (const auto &route : routeSet.second) {
- std::string path = url;
- if (url.find("GET ") != std::string::npos &&
- route.requestType == RequestType::GET) {
- path = path.substr(4);
- } else if (url.find("POST ") != std::string::npos &&
- route.requestType == RequestType::POST) {
- path = path.substr(5);
- } else {
- continue;
- }
- path = path.substr(0, path.find(" "));
- if (path.find("?") != std::string::npos) {
- auto urlEncodedSplit = splitUrl(path, '?');
- path = urlEncodedSplit[0];
- queryParams = this->parseQueryString(urlEncodedSplit[1]);
- }
- auto routeSplit = splitUrl(routeSet.first, '/');
- auto urlSplit = splitUrl(path, '/');
- bool matches = true;
- pathParams.clear();
- if (routeSplit.size() == urlSplit.size()) {
- for (int x = 0; x < routeSplit.size(); x++) {
- if (routeSplit[x] != urlSplit[x]) {
- if (routeSplit[x][0] == ':') {
- pathParams.insert(
- {routeSplit[x].substr(1), urlSplit[x]});
- } else {
- matches = false;
- }
- }
- }
- } else {
- matches = false;
- }
- if (routeSplit.back().find("*") != std::string::npos &&
- urlSplit[1] == routeSplit[1]) {
- matches = true;
- }
- if (matches) {
- if (body.find("&") != std::string::npos) {
- queryParams = this->parseQueryString(body);
- }
- HTTPRequest req = {.urlParams = pathParams,
- .queryParams = queryParams,
- .body = body,
- .handlerId = 0,
- .connection = connectionFd};
- route.handler(req);
- return;
- }
- }
- }
- writeResponse(HTTPResponse{
- .connectionFd = connectionFd,
- .status = 404,
- .body = "Not found",
- });
- }
|