nodemcu / nodemcu-firmware

Lua based interactive firmware for ESP8266, ESP8285 and ESP32

Home Page:https://nodemcu.readthedocs.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

A new HttpServer

vidalouiswang opened this issue · comments

A new HttpServer

I built a new http server module using with LFS.
There are 5 files need to be write into "lfs.img".
I don't have premission to push file so I put it here.

Memory leak detected when a client(browser) pushed a request but I don't know how to fix it.

ENV:
NodeMCU 3.0.0.0
branch: release
commit: d4ae3c3
release: 3.0.0-release_20210201 +1
release DTS: 202105102018
SSL: false
build type: float
LFS: 0x40000 bytes total capacity
modules: adc,coap,crypto,encoder,file,gpio,http,i2c,mdns,net,node,pwm,rtcfifo,rtcmem,rtctime,sjson,sntp,spi,tmr,uart,websocket,wifi
build 2021-06-01 00:56 powered by Lua 5.3.5 on SDK 3.0.1-dev(fce080e)

-- _httpCreateResponse.lua : 

return function(sck, req)
    local r = {};
    r.socket = sck;
    r.req = req;
    r.headers = {};
    r.isHeaderWrote = nil;
    r.queue = {};
    r.events = {};
    r.finished = nil;

    r._send = function(data) r.socket:send(data); end

    r.write = function(data) LFS._httpResponseWrite()(r, data); end

    r.on = function(event, fn) r.events[event] = fn; end

    r.emit = function(event, data)
        local fn = r.events[event];
        if fn then fn(data); end
    end

    r.writeFile = function(fileName, length)
        LFS._httpResponseWriteFile()(r, fileName, length);
    end

    r.setHeader = function(key, value) r.headers[key] = value; end

    r.finish = function(data)
        r.finished = true;
        r.write(data);
        r.dispose();
    end

    r.dispose = function()
        -- print(#self.queue, self.finished)
        if #r.queue <= 0 and r.finished then
            -- print("disposing")
            -- release the callback of "sent"
            r.socket:on("sent",nil);
            r.req = nil;

            -- self.socket:close will thore an error to raise esp8266 restart
            -- so use pcall to execute
            local function f() r.socket:close(); end
            pcall(f);

            -- mark all tables as nil manually
            -- f, res.socket = nil, nil;
            -- res, req, conn = nil, nil, nil;
            gc();
        end
    end

    sck:on("sent",function ()
        r.emit("drain"); -- emit the event to outer
        if #r.queue > 0 then
            r._send(table.remove(r.queue, 1)); -- send the item in the fifo
        else
            r.dispose(); -- release resources
        end
    end)
    return r;
end


-- _httpReceiveCallback.lua : 

return function(sck, data, sockets, fn)

    -- to match method, path and host information from raw data
    local _, _, method, url = data:find("^(%u+)%s+(.+)%s+HTTP/1.1");

    -- find is a same socket exists
    local obj = sockets[sck];

    if obj then
        if obj.method == "GET" and obj.url == url then
            -- close last socket(for saving memory) 
            -- if request from same client and has same url
            -- typical view is a client press F5 very fast
            -- if don't do this esp8266 will panic and reboot
            -- and that is unacceptable
            sck:close();
            sockets[sck] = nil;
        elseif obj.method == "POST" then
            -- if socket exists but there are plenty of data to send from 
            -- browser to esp8266
            -- then just push the data
            obj.res.emit("data", data);
            obj.postOffset = obj.postOffset + #data;
            if obj.postOffset >= obj.postLen then
                obj.res.emit("close", obj.postLen);
                sockets[sck] = nil;
            end
            return;
        end
    else
        -- this client is first time pushed request
        obj = {method = method, url = url};
        -- copy the ref
        sockets[sck] = obj;
    end

    -- if the code running to headers
    -- that means this socket has different url or the first time push the request
    local h = LFS.split()(data, "\r\n");
    local headers = {};
    for _, value in pairs(h) do
        if value:find("%S+") then
            local _, _, key, value = value:find("(.+):%s+(.+)");
            if key then
                table.insert(headers, {key = key, value = value});
            end
        end
    end

    -- save content length to objects
    if method == "POST" then
        for _, value in pairs(headers) do
            if value.key:find("Content-length") then
                obj.postLen = tonumber(value.value);
                obj.postOffset = 0;
            end
        end
        if obj.postLen == nil then
            sck:close();
            obj = nil;
        end
    end

    -- renew the request objects
    local req = {
        url = url, -- the path that browser request
        method = method, -- the method
        headers = headers
    };
    -- save request
    obj.req = req;
    local res = LFS._httpCreateResponse()(sck, req);
    obj.res = res;

    -- execute the callback
    fn(req, res);
end



-- _httpResponseWrite.lua : 

return function(res, data)
    if not res.isHeaderWrote then
        -- write the http headers before body 
        res.isHeaderWrote = true;

        -- add all headers to the send queue 
        for _, value in ipairs(res.headers) do
            table.insert(res.queue,
                         (value.name .. ": " .. value.value .. "\r\n"));
        end
        -- add endl
        table.insert(res.queue, "\r\n");

        -- send the "head" of the headers
        local head = "HTTP/1.1 " .. (res.statusCode or "200") .. " OK\r\n" ..
                         "Host: " .. (res.req.host or "NodeMCU") .. "\r\n";
        res._send(head);
    end

    if data then -- enqueue the data
        table.insert(res.queue, data);
    end
end



-- _httpResponseWriteFile.lua : 

return function(r, fileName, length)
    if r.f then
        gc();
        local d = r.f.read(length or 128);
        if d then
            r.write(d);
        else
            r.f.close();
            r.f = nil;
            r.finish();
            r.on("drain",nil);
        end
        return;
    end
    if file.exists(fileName) then
        local function rs(value) r.setHeader("Content-type", value); end
        local function nf(pattern) return fileName:find(pattern); end
        -- some common file type
        if nf(".+%.html") then
            rs("text/html");
        elseif nf(".+%.js") then
            rs("application/x-javascript");
        elseif nf(".+%.jpg") then
            rs("image/jpeg");
        elseif nf(".+%.jpeg") then
            rs("image/jpeg");
        elseif nf(".+%.png") then
            rs("image/png");
        elseif nf(".+%.gif") then
            rs("image/gif");
        elseif nf(".+%.xml") then
            rs("text/xml");
        elseif nf(".+%.json") then
            rs("text/json");
        elseif nf(".+%.pdf") then
            rs("application/pdf");
        elseif nf(".+%.bin") then
            rs("application/octet-stream");
        elseif nf(".+%.exe") then
            rs("application/octet-stream");
        elseif nf(".+%.mp3") then
            rs("audio/mp3");
        else
            rs("text/plain");
        end
        r.setHeader("Connection", "close");
        r.f = file.open(fileName, "r");
        r.on("drain", function() r.writeFile(); end)

        r.writeFile();
    end
end



-- httpCreateServer.lua : 

return function(timeout)
    timeout = timeout or 28000;
    local srv = {server = net.createServer(net.TCP, (60))};
    local sockets = {};

    -- to create function to start to listen
    srv.listen = function(port, fn)
        -- default port is 80
        port = port or 80;
        srv.server:listen(port, function(conn)
            -- bind receive function
            conn:on("receive", function(sck, data)
                LFS._httpReceiveCallback()(sck, data, sockets, fn);
            end);

            conn:on("disconnection", function(socket)
                if sockets[socket] then
                    sockets[socket].res.emit("close");
                    sockets[socket] = nil;
                end
            end)
        end);
    end

    return srv;
end



-- split.lua : 

return function(d, c)
    local t = {};
    for item in string.gmatch(d, "(.-)" .. c) do table.insert(t, item); end
    return t;
end



-- use : 

LFS = node.LFS;
local server = LFS.httpCreateServer()(60);
server.listen(80, function(req, res)
    if req.url == "/" then req.url = "/index.html"; end
    -- route
    if req.url:find("^/([^/]+)$") then
        local _, _, name = req.url:find("/(.+)");
        if file.exists(name) then
            res.writeFile(name);
        else
            res.finish();
            gc();
        end
    else
        res.finish("Hello NodeMCU");
        -- ...
    end
    local data = "";
    res.on("data", function(d) data = data .. d; end)
    res.on("close", function() print(data); end)
end)

Not an open issue.