Writing simplest HTTP server and blog in C and Lua

4 days ago 2

First I wanted to say Hi, as it is my first post in my blog!
After countless hours spent with my friend Qiu. I decided that this is finally the time to start writing my own blog. Of course, I could use WordPress or any other "blog engine", but then... where would the funniest part be?

Without further thinking I found my old small project of simple single threaded http server - assuming that I can ~200 lines of code call http server. It needed a little bit of tweaking. Let me briefly describe some part of it:

Implementation

code below was must have for me to keep my mental sanity, when using string operation in C (later I will delegate as much as possible of string operation to LUA):

//file: OwnedStr.h struct OwnedStr { char* str; size_t capacity; }; typedef struct OwnedStr OwnedStr; OwnedStr OwnedStr_Alloc(const char*); void OwnedStr_Concate(OwnedStr*,const char*); void OwnedStr_ConcateWithSize(OwnedStr* result, const char* in_str,size_t in_size); void OwnedStr_Free(OwnedStr*); OwnedStr OwnedStr_AllocFromFile(FILE *f); //file: OwnedStr.cpp #define JUST_IN_CASE 16 OwnedStr OwnedStr_Alloc(const char* in_c_str) { OwnedStr result; result.capacity = strlen(in_c_str); result.str = (char*)malloc(sizeof(char)*result.capacity+JUST_IN_CASE); strcpy(result.str,in_c_str); result.str[result.capacity] = '\0'; return result; } void OwnedStr_Concate(OwnedStr* result, const char* in_str) { result->capacity = result->capacity+strlen(in_str); char* old_str = result->str; result->str = (char*)malloc(sizeof(char)*result->capacity+JUST_IN_CASE); strcpy(result->str,old_str); strcat(result->str,in_str); result->str[result->capacity] = '\0'; free(old_str); } //...

Below there is main loop for handling requests. It is far from perfect, probably it not handle half of error, but for now what is the most important: it works

//file: tcp_server.c typedef void (*TCPServer_OnRequest_t)(TCPServer_RequestState* requestState); #define BUFF_SIZE (1024*10) int TCPServer_run(int port,TCPServer_OnRequest_t onRequest, TCPServer_OnLogPrint_t OnLogPrint, void* forwardedState) { signal(SIGPIPE, SIG_IGN); //resolved wierd bug in my WSL const int socket_descriptor = socket(AF_INET, SOCK_STREAM, 0); if(socket_descriptor == -1) { OnLogPrint(forwardedState,"unable to open socket!",LogType_Error); return -1; } struct sockaddr_in host_addr; int addrlen = sizeof(host_addr); host_addr.sin_family= AF_INET; host_addr.sin_port= htons(port); host_addr.sin_addr.s_addr= htonl(INADDR_ANY); if(bind(socket_descriptor,(struct sockaddr*)&host_addr, addrlen) != 0) if(socket_descriptor == -1) { OnLogPrint(forwardedState,"unable to bind address!",LogType_Error); return -1; } if(listen(socket_descriptor,SOMAXCONN) != 0) { OnLogPrint(forwardedState,"unable to listen!",LogType_Error); return -1; } const int running = 1; char* buffer = (char*)malloc( BUFF_SIZE*sizeof(char)); OnLogPrint(forwardedState,"start listening!",LogType_Info); while(running) { TCPServer_RequestState req; req.forwardedState = forwardedState; req.onLogPrintCallback = OnLogPrint; req.connection = accept(socket_descriptor, (struct sockaddr*)&host_addr, (socklen_t *)&addrlen); struct sockaddr_in client_addr; int client_addrlen = sizeof(client_addr); int conn_name = getsockname(req.connection,(struct sockaddr*)&client_addr, (socklen_t*)&client_addrlen); if(conn_name < 0) { OnLogPrint(forwardedState,"unable to get connection name",LogType_Error); close(req.connection); continue; } if(req.connection < 0) { OnLogPrint(forwardedState,"unable to accept connection",LogType_Error); close(req.connection); continue; } const int read_status = read(req.connection,buffer,BUFF_SIZE); if(read_status < 0) { OnLogPrint(forwardedState,"unable to read from connection",LogType_Error); close(req.connection); continue; } req.request = buffer; onRequest(&req); close(req.connection); } free(buffer); close(socket_descriptor); return 0; }

To keep up appearances of any project aggregations boundary: All TCP related functions are encapsulated in simple header style interface (I use this pattern a lot in this project)

//file: tcp_server.h typedef struct TCPServer_RequestState TCPServer_RequestState; const char* TCPServer_GetRequestString(TCPServer_RequestState*); void TCPServer_sendString(TCPServer_RequestState*,const char*); void TCPServer_sendStringWithLength(TCPServer_RequestState*,const char*,size_t); void TCPServer_sendFile(TCPServer_RequestState*,FILE*); void* TCPServer_GetForwardedState(TCPServer_RequestState*); typedef void (*TCPServer_OnRequest_t)(TCPServer_RequestState* requestState); typedef enum LogType_t { LogType_Error, LogType_Info } LogType_t; typedef void (* TCPServer_OnLogPrint_t)(void* forwardedState,const char*,LogType_t type); int TCPServer_run(int port,TCPServer_OnRequest_t onRequest,TCPServer_OnLogPrint_t OnLogPrint, void* forwardedState);

For convince I wrote simple generic function to handle static file serving.

//file: staticFile.h void sendDefaultOkHeader(TCPServer_RequestState* s,const char* contentType); void sendDefault404Header(TCPServer_RequestState* s); void sendDefault500Header(TCPServer_RequestState* s); void serveStaticFile(TCPServer_RequestState* s, const char* filename); //file: staticFile.c typedef struct ExtFileInfo { enum {File_Binary, File_Text} type; const char* default_contentType; } ExtFileInfo; ExtFileInfo getExtFileInfo(const char* filename) { char *ext = strrchr(filename, '.'); ExtFileInfo def = {File_Text,"Content-Type: text/plain\n"}; if(!ext) return def; if (strcmp(ext, ".html") == 0) return (ExtFileInfo){File_Text,"Content-Type: text/html;charset=utf-8\n"}; if (strcmp(ext, ".css" ) == 0) return (ExtFileInfo){File_Text,"Content-Type: text/css;charset=utf-8\n"}; if (strcmp(ext, ".js" ) == 0) return (ExtFileInfo){File_Text,"Content-Type: application/javascript;charset=utf-8\n"}; if (strcmp(ext, ".json") == 0) return (ExtFileInfo){File_Text,"Content-Type: application/json;charset=utf-8\n"}; if (strcmp(ext, ".txt" ) == 0) return (ExtFileInfo){File_Text,"Content-Type: text/plain;charset=utf-8\n"}; if (strcmp(ext, ".jpg" ) == 0) return (ExtFileInfo){File_Binary,"Content-Type: image/jpg\n"}; if (strcmp(ext, ".png" ) == 0) return (ExtFileInfo){File_Binary,"Content-Type: image/png\n"}; if (strcmp(ext, ".gif" ) == 0) return (ExtFileInfo){File_Binary,"Content-Type: image/gif\n"}; if (strcmp(ext, ".ttf" ) == 0) return (ExtFileInfo){File_Binary,"Content-Type: font/ttf\n"}; return def; } void sendDefaultOkHeader(TCPServer_RequestState* s,const char* contentType) { TCPServer_sendString(s,"HTTP/1.1 200 OK\n"); TCPServer_sendString(s,"Server: qws\n"); TCPServer_sendString(s,contentType); TCPServer_sendString(s,"\n"); } void serveStaticFile(TCPServer_RequestState* s, const char* filename) { const ExtFileInfo extFileInfo = getExtFileInfo(filename); if(extFileInfo.type == File_Text) { FILE* file = fopen(filename, "r"); if (file) { sendDefaultOkHeader(s,extFileInfo.default_contentType); char buff[1024*10]; while (fgets(buff, 1024*10, file) != NULL) { TCPServer_sendString(s,buff); } fclose(file); printf("sent: %s \n",filename); return; } } if(extFileInfo.type == File_Binary) { FILE* file = fopen(filename, "rb"); if (file) { sendDefaultOkHeader(s,extFileInfo.default_contentType); TCPServer_sendFile(s,file); fclose(file); printf("sent: %s \n",filename); return; } } sendDefault404Header(s); }

At that point this server was capable to server static hosted file, but I couldn't stop there:
I wanted to support at least two feature that requires some form of scripting:

  • handling user friendly URL
  • website templates, to inject one html file into another
  • thinking of future: some way to represents data structures like post

Also, I wanted to write main content in something else than HTML.
Considering all of this: I decided to use LUA for scripting and Markdown (using Md4c) for content.

Please do not look at 2 dirty hack that I still don't believe that I wrote them in 2025

  • First of them is using goto to clean up heap allocated memory
  • Second is to cast ptr to (long long) in order to pass and receive it from LuaState in API functions. I wanted to avoid using static (global) memory
//file: main.c void handleTcpRequest(TCPServer_RequestState* s) { char method[128], uri[2048], version[128]; sscanf(TCPServer_GetRequestString(s), "%s %s %s", method, uri, version); FILE* file = fopen("../content/entry.lua", "r"); OwnedStr buffer = OwnedStr_AllocFromFile(file); lua_State *L = luaL_newstate(); luaL_openlibs(L); // set LUA Context LuaUserContext context; context.s = s; LUA_INTEGER ptr = (LUA_INTEGER) (&context); lua_pushinteger(L,ptr); lua_setglobal(L,"_CONTEXT_PTR"); Lua_SetPackagePath(L, "../content/?.lua"); // register functions lua_register(L, "GetFileContent", Lua_GetFileContent); lua_register(L, "MdToHTML", Lua_MdToHTML); lua_register(L, "Print", Lua_Print); lua_register(L, "PrintDefaultHtmlOkHeader", Lua_sendDefaultHtmlOkHeader); lua_register(L, "Print", Lua_Print); lua_register(L, "ServeStaticFile", Lua_ServeStaticFile); // LoadScript int status = luaL_loadstring(L,buffer.str); if(status){ sendDefault500Header(s); if(status == LUA_ERRSYNTAX){ TCPServer_sendString(s,"<h1> SYNTAX ERROR: </h1>\n"); TCPServer_sendString(s,lua_tostring(L, -1)); handleScriptError(s,lua_tostring(L, -1),LogType_Error); } else{ TCPServer_sendString(s,"<h1> UNKNOWN LOAD ERROR: </h1>\n"); } goto cleanupAndReturn; } if(lua_pcall(L, 0, 0, 0)) { sendDefault500Header(s); TCPServer_sendString(s,"<h1> INIT ERROR: </h1>\n"); TCPServer_sendString(s,lua_tostring(L, -1)); handleScriptError(s,lua_tostring(L, -1),LogType_Error); goto cleanupAndReturn; } lua_getglobal(L, "HandleRequest"); lua_pushstring(L,TCPServer_GetRequestString(s)); if (lua_pcall(L, 1, 0, 0)) { sendDefault500Header(s); TCPServer_sendString(s,"<h1> RUNTIME ERROR: </h1>\n"); TCPServer_sendString(s,lua_tostring(L, -1)); handleScriptError(s,lua_tostring(L, -1),LogType_Error); goto cleanupAndReturn; } cleanupAndReturn: OwnedStr_Free(&buffer); fclose(file); lua_close(L); return; }

As you can see I used LUA to generate both header and content.
Is it bad? - yes
Is it work (for now)? - yes

I exposed only: "GetFileContent", "MdToHTML", "ServeStaticFile", "Print", "sendDefaultHtmlOkHeader", "sendDefault404Header" to LUA
Surprisingly, for website that you can see right now it is enough. and event not all of them are necessary. at least for now.

//file: main.c typedef struct LuaUserContext { TCPServer_RequestState* s; } LuaUserContext; LuaUserContext* GetUserContext(lua_State* L) { lua_getglobal(L,"_CONTEXT_PTR"); LUA_INTEGER ptr = lua_tointeger(L, -1); lua_pop(L,1); LuaUserContext* result = (LuaUserContext*)ptr; return result; } int Lua_GetFileContent(lua_State* L) { const char* arg = lua_tostring(L, 1); OwnedStr path = OwnedStr_Alloc(FILE_PREFIX); OwnedStr_Concate(&path,arg); FILE* file = fopen(path.str, "r"); OwnedStr_Free(&path); if(file) { OwnedStr buffer = OwnedStr_AllocFromFile(file); lua_pushstring(L, buffer.str); OwnedStr_Free(&buffer); fclose(file); return 1; } lua_pushstring(L, ""); return 1; } void md_process_output(const MD_CHAR* str , MD_SIZE length, void* inState) { OwnedStr* buffer = (OwnedStr*)inState; OwnedStr_ConcateWithSize(buffer,str,length); } int Lua_MdToHTML( lua_State* L) { const char* arg = lua_tostring(L, 1); OwnedStr buffer = OwnedStr_Alloc(""); md_html(arg, strlen(arg), md_process_output, &buffer, MD_FLAG_TABLES|MD_FLAG_TASKLISTS|MD_FLAG_WIKILINKS,0); lua_pushstring(L, buffer.str); OwnedStr_Free(&buffer); return 1; } int Lua_ServeStaticFile(lua_State* L) { LuaUserContext* ctx = GetUserContext(L); const char* arg = lua_tostring(L, 1); OwnedStr path = OwnedStr_Alloc(FILE_PREFIX); OwnedStr_Concate(&path,arg); serveStaticFile(ctx->s,path.str); OwnedStr_Free(&path); return 0; } int Lua_Print(lua_State* L) { LuaUserContext* ctx = GetUserContext(L); const char* arg = lua_tostring(L, 1); TCPServer_sendString(ctx->s,arg); return 0; } int Lua_sendDefaultHtmlOkHeader(lua_State* L) { LuaUserContext* ctx = GetUserContext(L); sendDefaultOkHeader(ctx->s,"Content-Type: text/html;charset=utf-8\n"); return 0; } int Lua_sendDefault404Header(lua_State* L) { LuaUserContext* ctx = GetUserContext(L); sendDefault404Header(ctx->s); return 0; }

And finally bellow is "entry.lua'

require('common') function HandleRequest(request) local request_exploder = string.gmatch(request, "%S+") local method = request_exploder() local location = request_exploder() --Routes if location == "/quick-notes" then PrintDefaultHtmlOkHeader() Print(GetFileContent("template/main.html") % { content = GetFileContent("template/quickNotes.html")}) --Notes elseif location == "/quick-note/writing-http-server-and-blog-in-c-and-lua" then PrintDefaultHtmlOkHeader() Print(GetFileContent("template/main.html") % { content = MdToHTML(GetFileContent("quickNotes/writingHttpServerAndBlogInCAndLua.md"))}) --Static elseif location == "/public/style.css" then ServeStaticFile("public/style.css"); --Homepage else PrintDefaultHtmlOkHeader() Print(GetFileContent("template/main.html") % { content = GetFileContent("template/quickNotes.html")}) end end --file: common.lua function interp(s, tab) return (s:gsub('($%b{})', function(w) return tab[w:sub(3, -2)] or w end)) end getmetatable("").__mod = interp

and simplified "template/main.html"

<!doctype html> <html lang="en"> <head> <title>Qikcik Blog</title> </head> <body> <h1>Qikcik</h1> ${content} </body> </html>

Hosting issues

I wanted to host it on CT8 server, but there is a catch - it is running on freebsd. First issue was missing "struct sockaddr_in" header. it was simply fixable by including <netinet/in.h>

source/tcp_server.c:54:24: error: variable has incomplete type 'struct sockaddr_in'

Second was trickier: SendFile is slightly different implemented in FreeBsd then in Linux. After something, I decided that this is too small project to care about host os. ...so I rewrote TCPServer_sendFile to use additional buffer and only read/write. (I keep telling to myself that at least I do not use garbage collector)

void TCPServer_sendFile(TCPServer_RequestState* s ,FILE* f) { OwnedStr buffor = OwnedStr_AllocFromFile(f); int write_status = write(s->connection,buffor.str,buffor.capacity); if(write_status < 0) s->onLogPrintCallback(s->forwardedState,"unable to write to connection",LogType_Error); OwnedStr_Free(&buffor); }

End thought

Is all of this in any aspect commercial ready product? - absolutely not
Is it safe, or Is it blazingly fast? - absolutely not
Was it great journey? - absolutely yes
Is it enough to serve simple blog? - I hope so

PS: There you can find all the source code

Read Entire Article