login

local tcp = require "tcp"

local function parse_query(query_str)
    local query = {}
    if #query_str > 0 then
        for pair in (query_str.."&"):gmatch("([^&]*)&") do
            local key, val = pair:match("([^=]*)=(.*)")
            query[key] = val
        end
    end
    return query
end

local function parse_uri(uri)
    local path, query_str, fragment = uri:match("([^?#]*)%??([^#]*)#?(.*)")
    local query = parse_query(query_str)
    return path, query, fragment
end

local function parse_cookies(cookie_values)
    local cookies = {}
    if cookie_values ~= nil then
        for pair in (cookie_values..";"):gmatch("([^;]*);") do
            local key, val = pair:match("%s*([^=]*)=(.*)")
            cookies[key] = val
        end
    end
    return cookies
end

local function parse_request(data)
    local req = {payload="", headers={}}
    local stage = "status"
    for line in (data.."\n"):gmatch("(.-)\r?\n") do
        if stage == "status" then
            local target, protocol
            req.method, target, protocol = line:match("(%S*) (%S*) (%S*)")
            assert(protocol == "HTTP/1.1")
            req.path, req.query, req.fragment = parse_uri(target)
            stage = "header"
        elseif stage == "header" then
            if line == "" then
                stage = "payload"
            else
                local key, value = line:match("([^:]*):%s*(.*)")
                req.headers[key:lower()] = value
            end
        else -- payload
            req.payload = req.payload..line.."\n"
        end
    end
    if req.headers["content-type"] == "application/x-www-form-urlencoded" then
        local query_str = req.payload
        query_str = query_str:sub(1, #query_str-1)
        req["form"] = parse_query(query_str)
    end
    req.cookies = parse_cookies(req.headers["cookie"])
    return req
end

-- cookies is a sequence of tables with the following keys:
--   key, val -> cookie entry (required)
--   path -> cookie scope, e.g., /
--   age -> cookie expiration time in seconds
local function build_cookie_data(cookies)
    local data = ""
    for i, c in ipairs(cookies or {}) do
        local line = ("Set-Cookie: %s=%s; HttpOnly"):format(c.key, c.val)
        if c.path ~= nil then
            line = line .. "; Path=" .. c.path
        end
        if c.age ~= nil then
            line = line .. "; Max-Age=" .. c.age
        end
        data = data .. line .. "\n"
    end
    return data
end

local function build_response(data, status, reason, cookies)
    local header = ""
    if status == nil then
        status = 200
        reason = "OK"
    elseif status == 303 then
        reason = reason or "See Other"
        header = "Location: " .. data .. "\n"
    end
    header = header .. build_cookie_data(cookies)
    local fmt = "HTTP/1.1 %03d %s\r\n%s\r\n%s"
    return fmt:format(status, reason, header, data)
end

local HTTP = {}
HTTP.__index = HTTP

function HTTP:run(port)
    self.tcp:init(port)
    self.tcp:run()
end

local function new_http()
    local obj = setmetatable({}, HTTP)
    obj.tcp = tcp.new_tcp(1000, 200)
    function obj.tcp:process(datain)
        local req = parse_request(datain)
        local dataout, status, reason, cookies = obj:process(req)
        if dataout == nil then
            return nil
        end
        return build_response(dataout, status, reason, cookies)
    end
    return obj
end

return {new_http=new_http}