Skip to content

Instantly share code, notes, and snippets.

@LinuxRocks2000
Last active January 14, 2022 16:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save LinuxRocks2000/7722d1cd917e79a2f1ae6485a0a5e6cd to your computer and use it in GitHub Desktop.
Save LinuxRocks2000/7722d1cd917e79a2f1ae6485a0a5e6cd to your computer and use it in GitHub Desktop.
One-file threaded HTTP server in C++ with support for callbacks. Updated as I need it, so eventually will be state aware and etc.
// Socket includes
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include <fcntl.h>
// STD includes
#include <thread>
#include <string>
#include <vector>
#include <atomic>
// Otherwise
#include <signal.h>
// Only for testing
#include <unistd.h>
#include <iostream>
// Processing positions for requests.
#define DETERMINING_HTTP_METHOD 0
#define DETERMINING_HTTP_PATH 1
#define DETERMINING_HTTP_VERSION 2
#define READING_HTTP_HEADERS_P1 3 // Reading the name
#define READING_HTTP_HEADERS_P2 4 // Reading the value
#define READ_CONTENT 5
// HTTP methods
#define HTTP_METHOD_GET 0
#define HTTP_METHOD_POST 1
#define HTTP_METHOD_DELETE 2
#define HTTP_METHOD_INVALID 255
// HTTP versions, for now only these are supported.
#define HTTP_VERSION_1_1 0
#define HTTP_VERSION_INVALID 255
// Note that error handling should be done by the code running the server! This program will assign HTTP_*_INVALID to all fields that permit it,
// if it catches a problem.
struct HTTPHeader{
std::string name; // Std::strings are fairly light and efficient and are easier to do many operations on.
std::string value; // We'll use them until somebody complains, then we'll ignore the complainers so viciously they won't know what didn't notice them.
};
struct HTTPRequest{
uint8_t procpos = DETERMINING_HTTP_METHOD; // Processing position.
uint8_t method; // These are all set during processing.
uint8_t HTTPVersion;
std::string url;
std::string content;
unsigned int contentLength = 0; // The header.
std::vector<HTTPHeader*> headers;
HTTPHeader *_currentHeader; // Header buffer.
std::string _currentData; // This is a multi-purpose buffer.
bool _ignoringSpaces = true; // This is for when it ignores whitespace between the name and value of a header.
};
struct HTTPResponse{
HTTPRequest *origReq;
std::vector<HTTPHeader*> headers;
std::string content;
uint16_t status;
// Automatically available headers:
bool keepAlive = true;
std::string contentType = "text/plain";
};
// Client, is it really that hard to understand? Badadadummmmmm.
struct Client{
HTTPRequest *currentRequest;
bool hasRequest = false;
int sock;
};
// HTTP server structure, I always use structs because classes are eww. It has all the necessary functions to do things.
struct HTTPServer{
int serverSock = -1; // We don't want this to accidentally write to stdin/out if it is not assigned.
// Yet.
struct sockaddr_in address;
socklen_t addrsize = sizeof(address);
std::vector<Client*> clients;
std::function<void(HTTPRequest*, HTTPResponse*, Client*)> requestCallback;//void (*requestCallback)(HTTPRequest*, HTTPResponse*, Client*);
static void accepter(HTTPServer* self){ // The self thing is a workaround; because thread functions can't be members, you have to pass in `this` the hard way.
while (true){
int cliSock = accept(self -> serverSock, (struct sockaddr*)&self -> address, &self -> addrsize);
Client *client = new Client;
client -> sock = cliSock;
fcntl(cliSock, F_SETFL, O_NONBLOCK);
self -> clients.push_back(client);
}
}
void sendResponse(HTTPResponse* response, Client *client){
if (response -> origReq -> HTTPVersion == HTTP_VERSION_1_1){
send(client -> sock, "HTTP/1.1 ", 9, 0);
}
else {
send(client -> sock, "Invalid protocol version. Please go away.", 41, 0);
return;
}
char code[5];
snprintf(code, 5, "%d\r\n", response -> status);
send(client -> sock, code, 5, 0); // 3 for response code + 2 for CRLF
for (unsigned int x = 0; x < response -> headers.size(); x ++){
HTTPHeader *curHeader = response -> headers[x];
send(client -> sock, curHeader -> name.c_str(), curHeader -> name.size(), 0);
send(client -> sock, ": ", 2, 0);
send(client -> sock, curHeader -> value.c_str(), curHeader -> value.size(), 0);
send(client -> sock, "\r\n", 2, 0);
}
// Autogen headers here:
// Connection (for keep-alive and stuff):
send(client -> sock, "Connection: ", 12, 0);
if (response -> keepAlive){
send(client -> sock, "keep-alive\r\n", 12, 0);
}
else{
send(client -> sock, "close\r\n", 7, 0);
}
// Content-Length:
std::string contentLengthHeader = "Content-Length: ";
contentLengthHeader += std::to_string(response -> content.size());
contentLengthHeader += "\r\n";
send(client -> sock, contentLengthHeader.c_str(), contentLengthHeader.size(), 0);
// Content-Type:
send(client -> sock, "Content-Type: ", 14, 0);
send(client -> sock, response -> contentType.c_str(), response -> contentType.size(), 0);
send(client -> sock, "\r\n", 2, 0);
// Send the actual content:
send(client -> sock, "\r\n", 2, 0);
send(client -> sock, response -> content.c_str(), response -> content.size(), 0);
}
static void reader(HTTPServer* self){
while (true){
unsigned int curSize = self -> clients.size();
for (unsigned int x = 0; x < curSize; x ++){
Client *client = self -> clients[x];
char buffer;
if (recv(client -> sock, &buffer, 1, 0) == 0){ // Disconnect dead sockets.
close(client -> sock);
free(client);
self -> clients.erase(self -> clients.begin() + x);
printf("Socket disconnected.\n");
break; // Dirty, but effective.
}
if (!client -> hasRequest){
client -> currentRequest = new HTTPRequest;
client -> hasRequest = true; // Not including this was causing me annoying buggies and segfaultification, I think.
}
HTTPRequest *req = client -> currentRequest;
int length = 0;
if (req -> procpos == DETERMINING_HTTP_METHOD){
for (uint8_t i = 0; i < 7; i ++){
length = recv(client -> sock, &buffer, 1, 0); // Buffer, length, flags.
if (length != 1){ // Any non-one case
break;
}
if (buffer == ' '){ // It has reached the end if the buffer is space.
req -> procpos = DETERMINING_HTTP_PATH;
if (req -> _currentData == "GET"){
req -> method = HTTP_METHOD_GET;
}
else if (req -> _currentData == "POST"){
req -> method = HTTP_METHOD_POST;
}
else if (req -> _currentData == "DELETE"){
req -> method = HTTP_METHOD_DELETE;
}
else {
req -> method = HTTP_METHOD_INVALID;
}
req -> _currentData = ""; // Empty it
break;
}
req -> _currentData += buffer;
}
}
if (req -> procpos == DETERMINING_HTTP_PATH){
while (true){ // T'aint true, it uses break statements.
length = recv(client -> sock, &buffer, 1, 0); // Buffer, length, flags.
if (buffer == ' '){
req -> url = req -> _currentData;
req -> _currentData = ""; // Clear it
req -> procpos = DETERMINING_HTTP_VERSION; // It can support any HTTP version, this is just to get past it.
break;
}
if (length != 1){
break; // Nothing to do but break and wait.
}
req -> _currentData += buffer;
}
}
if (req -> procpos == DETERMINING_HTTP_VERSION){
for (uint8_t i = 0; i < 8; i ++){
length = recv(client -> sock, &buffer, 1, 0); // Buffer, length, flags.
if (buffer == '\n'){
req -> procpos = READING_HTTP_HEADERS_P1;
if (req -> _currentData == "HTTP/1.1"){
req -> HTTPVersion = HTTP_VERSION_1_1;
}
else {
req -> HTTPVersion = HTTP_VERSION_INVALID;
}
req -> _currentData = "";
break;
}
if (length != 1){
break;
}
if (buffer != ' ' && buffer != '\r' && buffer != '\n'){ // Both of these characters will throw it off.
req -> _currentData += buffer;
}
}
// Length of an HTTP version is 8 (HTTP/1.1 is the only one we intend to support as of this writing)
}
if (req -> procpos == READING_HTTP_HEADERS_P1){
req -> _ignoringSpaces = true; // Gotta reset it.
while (true){ // See the other one that does this, the comment is the same.
length = recv(client -> sock, &buffer, 1, 0); // Buffer, length, flags.
if (length != 1){
break;
}
if (buffer == '\n'){ // Never happens until the empty line before body.
req -> procpos = READ_CONTENT;
break;
}
if (buffer == ':'){
req -> procpos = READING_HTTP_HEADERS_P2;
req -> _currentHeader = new HTTPHeader;
req -> _currentHeader -> name = req -> _currentData;
req -> _currentData = "";
break;
}
req -> _currentData += buffer;
}
}
if (req -> procpos == READING_HTTP_HEADERS_P2){
while (true){
length = recv(client -> sock, &buffer, 1, 0); // Buffer, length, flags.
if (length != 1){
break;
}
if (buffer == '\n'){
req -> _currentHeader -> value = req -> _currentData;
req -> _currentData = "";
req -> procpos = READING_HTTP_HEADERS_P1;
if (req -> _currentHeader -> name == "Content-Length"){
req -> contentLength = std::stoi(req -> _currentHeader -> value);
}
else{
req -> _currentHeader = nullptr;
req -> headers.push_back(req -> _currentHeader);
}
break;
}
if (req -> _ignoringSpaces && buffer != ' '){
req -> _ignoringSpaces = false;
}
if (!(buffer == '\r') && !req -> _ignoringSpaces){
req -> _currentData += buffer;
}
}
}
if (req -> procpos == READ_CONTENT){
char* buf = (char*)malloc(req -> contentLength + 1);
buf[req -> contentLength] = 0; // Add a 0 at the end, so it is a valid string.
length = recv(client -> sock, buf, req -> contentLength, 0);
req -> content += buf;
free(buf);
if (req -> content.length() >= req -> contentLength){ // That's all, folks!
req -> procpos = DETERMINING_HTTP_METHOD;
HTTPResponse *response = new HTTPResponse;
response -> origReq = req;
self -> requestCallback(req, response, client);
self -> sendResponse(response, client);
free(response);
}
}
}
}
}
HTTPServer(unsigned int port, std::function<void(HTTPRequest*, HTTPResponse*, Client*)> rCbck, unsigned int timeout = 2){
signal(SIGPIPE, SIG_IGN);
requestCallback = rCbck;
serverSock = socket(AF_INET, SOCK_STREAM, 0);
memset(&address, 0, sizeof(struct sockaddr_in));
address.sin_family = AF_INET;
address.sin_port = htons(port);
address.sin_addr.s_addr = INADDR_ANY;
while (bind(serverSock, (struct sockaddr*)&address, sizeof(struct sockaddr_in)) == -1){
printf("Bind failed. Trying again in %d seconds.\n", timeout);
usleep(timeout * 1000000);
}
printf("Successfully bound to port %d\n", port);
listen(serverSock, 25); // Listen for maximum 25 peoples
std::thread accepterThread(accepter, this);
accepterThread.detach();
std::thread readerThread(reader, this);
readerThread.detach();
}
void run(){
}
};
/*
Use it by creating an object of HTTPServer and passing in arguments. This isn't complete, so I won't make it any better.
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment