Qikcik

Writing HTTPserver and blog in C and LUA

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:

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

//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