Fortinet white logo
Fortinet white logo

Use Cases

Use Cases

HTTP to HTTPS redirection

-- HTTP to HTTPS redirection for default HTTP and HTTPS ports
when HTTP_REQUEST {
	if not HTTP:is_https() then
		HTTP:redirect("https://%s%s", HTTP:header("host")[1], HTTP:url())
	end
}

HTTP to HTTPS redirection for special ports

Only use the first port in HTTPS service.

when HTTP_REQUEST {
	if not HTTP:is_https() then
		local host = HTTP:header("host")[1]
		local https_port = policy.https_ports()[1] -- get the first port in HTTP service
		local newhost = host:gsub(":(%d+)", "") -- remove port from host if it has

		if https_port ~= 443 then
			-- if https port is not 443, add port to host
			newhost = newhost .. ":" .. tostring(https_port)
		end

		HTTP:redirect("https://%s%s", newhost, HTTP:url())
	end
}

HTTP get commands

when HTTP_REQUEST {
    debug("============= Dump HTTP request header =============\n")
    debug("host: %s, path: %s, url: %s, method: %s, version: %s, content type: %s\n",
            HTTP:host(), HTTP:path(), HTTP:url(), HTTP:method(), HTTP:version(), HTTP:content_type())
    for k, v in pairs(HTTP:headers()) do
        for i = 1, #v do
            debug("HEADER: %s[%d]: %s\n", k, i, v[i])
        end
    end
    for k, v in pairs(HTTP:cookies()) do
        debug("Cookie: %s = %s\n", k, v)
    end
    for k, v in pairs(HTTP:args()) do
        debug("ARGS: %s = %s\n", k, v)
    end
    debug("========== Dump HTTP request header done ===========\n")
}

when HTTP_RESPONSE {
    debug("============= Dump HTTP response header =============\n")
    debug("version:%s, content-type: %s\n", HTTP:version(), HTTP:content_type())
    debug("status code: %s reason: %s\n", HTTP:status())
    for k, v in pairs(HTTP:headers()) do
        for i = 1, #v do
            debug("HEADER: %s[%d]: %s\n", k, i, v[i])
        end
    end
    for k, v in pairs(HTTP:cookies()) do
        debug("Cookie: %s = %s\n", k, v)
    end
    debug("========== Dump HTTP response header done ===========\n")
}

IP_PKG example

when CLIENT_ACCEPTED {
    local client_ip = IP:client_addr()
    local r = ip.reputation(client_ip)
    debug("Client IP: %s, reputation: <%s>, GEO country: <%s>, GEO country code: %s\n",
            client_ip,
            #r > 0 and table.concat(r, ', ') or "No Found",
            ip.geo(client_ip) or "unknown",
            ip.geo_code(client_ip) or "unknown")
}

TCPIP commands

function print_ips(event, TCP, IP)
    debug("%s: version: %s, local: %s:%s, remote: %s:%s, client: %s:%s, server: %s:%s\n",
            event, IP:version(),
            IP:local_addr(), TCP:local_port(),
            IP:remote_addr(), TCP:remote_port(),
            IP:client_addr(), TCP:client_port(),
            IP:server_addr(), TCP:server_port())
end

when CLIENT_ACCEPTED {
    print_ips("CLIENT_ACCEPTED", TCP, IP)
}

when HTTP_REQUEST {
    print_ips("HTTP_REQUEST", TCP, IP)
}

when HTTP_RESPONSE {
    print_ips("HTTP_RESPONSE", TCP, IP)
}

when SERVER_CONNECTED {
    print_ips("SERVER_CONNECTED", TCP, IP)
}

when SERVER_CLOSED {
    print_ips("SERVER_CLOSED", TCP, IP)
}

when CLIENT_CLOSED {
    print_ips("CLIENT_CLOSED", TCP, IP)
}

Content routing by URL

-- The policy should have four content routing configuration: cr, cr1, cr2, cr3
-- Don't need set any rule in the content routing, all rules will be ignored 
-- after calling LB:routing("content-routing-name")
when HTTP_REQUEST {
    local url = HTTP:url()

    if url:find("^/sports") or url:find("^/news") or url:find("^/government") then
        LB:routing("cr1")
        debug("url %s starts with sports|news|government, routing to cr1\n", url)
    elseif url:find("^/finance") or url:find("^/technology") or url:find("^/shopping") then
        LB:routing("cr2")
        debug("url %s starts with finance|technology|shopping, routing to cr2\n", url)
    elseif url:find("^/game") or url:find("^/travel") then
        LB:routing("cr3")
        debug("url %s starts with game|travel, routing to cr3\n", url)
    else
        LB:routing("cr")
        debug("No match for uri: %s, routing to default cr\n", url)
    end
}

Full scan for XFF

-- This script will full scan the XFF headers, if it found any IP 
-- in IP reputation database, it will close the HTTP connection

-- This function extracts all IPs from XFF headers and returns IP array
function extract_xff(xff)
	local t = {}
	local k, v, s

	for k, v in ipairs(xff) do
		for s in v:gmatch("([^,]+)") do
			t[#t + 1] = s:gsub("%s+", "")
		end
	end
	return t
end

when HTTP_REQUEST {
	local ips = extract_xff(HTTP:header("X-Forwarded-For"))
	local r, i, v

	for i, v in ipairs(ips) do
		r = ip.reputation(v) -- check ip, will return an array
		if #r > 0 then -- Found IP in reputation database
			debug("Found bad IP %s in XFF headers, reputation: <%s>, GEO country: <%s>, GEO country code: %s\n",
				v, table.concat(r, ', '),
				ip.geo(v) or "unknown", ip.geo_code(v) or "unknown")
			HTTP:close() -- force close this HTTP connection
			return -- Stop script and return
		end
	end
}

Persist by cookie

-- Example of LB persistence
-- the type of persistence of the server pool must be "scripting"
-- LB:persist(string, timeout)
--   string: persist on this string, can use any string
--   timeout: persist on this timeout, unit is second,
--			  if doesn't exist, use the default value in the persistence configuration

cookie_name = "anyone"

when HTTP_REQUEST {
	local value = HTTP:cookie(cookie_name)
	if value then
		-- persist on value and the timeout is default
		-- will record the persistence when doesn't find the persistence
	    LB:persist(value)
	end
}

when HTTP_RESPONSE {
	local value = HTTP:cookie(cookie_name)
	if value then
		-- if server respone has this cookie
		-- record the persistence to the persistence table
		LB:persist(value)
	end
}

HTTP rewrite headers

-- Example of rewriting HTTP headers
-- GET /rewrite_request to rewrite the request with arguments
-- GET /rewrite_response to rewrite the response with arguments

function rewrite_request(HTTP, args)
    local v
  
    -- rewrite method
    v = args["method"]
    if v then HTTP:set_method(v) end

    -- rewrite path
    v = args["path"]
    if v then HTTP:set_path(v) end

    -- add/del/set header
    HTTP:del_header("cookie") -- remove header "cookie"
    HTTP:add_header("Accept", "*/*") -- add a new header line
    HTTP:set_header("Test", { "line1", "line2", "line3" }) -- add new header
end

function rewrite_response(HTTP, args)
    local v

    -- rewrite status, includes code and reason
    v = args["code"]
    if v then v = v:match("%d+") end -- get first number from code string
    if v then
        local code = tonumber(v) -- convert code string to number
        local reason = args["reason"]

        if reason then
            HTTP:set_status(code, reason) -- if reason exists, set code and reason 
        else
            HTTP:set_status(code) -- set code and use default reason
        end
    end
end
when HTTP_REQUEST {
    local path = HTTP:path()

    if path == "/rewrite_request" then
        rewrite_request(HTTP, HTTP:args())
    elseif path == "/rewrite_response" then
        -- use the HTTP transaction private data to pass data to response
        -- Do not use the global variable, it is shared with all TCP connections and HTTP transactions
        local t = HTTP:priv() -- get the HTTP transaction private table, it is an empty table on first read
        t["rewrite_response"] = true -- set rewrite_response to true in the HTTP transaction private table
        t["args"] = HTTP:args() -- store request arguments to the private table
    end

    HTTP:set_query("test=1")
}

when HTTP_RESPONSE {
    local t = HTTP:priv()

    if t["rewrite_response"] then
        return rewrite_response(HTTP, t["args"])
    end
}

HTTP custom reply

-- Example of HTTP custom reply

function reply_invalid(HTTP)
    HTTP:reply{
        status = 400,
        headers = {
                ["content-type"] = { "text/html" },
                ["cache-control"] = { "no-cache", "no-store" },
        },
        body = "<html><body><h1>Invalid API Request<h1></body></html>",
    }
end

function check_ip_reputation(HTTP)
    local v = HTTP:arg("ip")
    if v then v = ip.addr(v) end -- convert string to ip
    if not v then return reply_invalid(HTTP) end

    local r = ip.reputation(v)
    local body = string.format("<html><body><h1>Reputation of IP %s: %s<h1></body></html>",
        v, #r > 0 and table.concat(r, ', ') or "No Found")

    HTTP:reply{
        status = 200,
        headers = {
                ["content-type"] = { "text/html" },
                ["cache-control"] = { "no-cache", "no-store" },
        },
        body = body,
    }
end

function check_ip_geo(HTTP)
    local v = HTTP:arg("ip")
    if v then v = ip.addr(v) end -- convert string to ip
    if not v then return reply_invalid(HTTP) end

    local geo = ip.geo(v)
    local geo_code = ip.geo_code(v)
    local body = string.format("<html><body><h1>GEO of IP %s: %s, code: %s<h1></body></html>",
        v, geo, geo_code)

    HTTP:reply{
        status = 200,
        headers = {
                ["content-type"] = { "text/html" },
                ["cache-control"] = { "no-cache", "no-store" },
        },
        body = body,
    }
end

when RULE_INIT {
    actions = {}
    actions["reputation"] = check_ip_reputation
    actions["geo"] = check_ip_geo

    convert = {}
    convert["testing"] = "test"
    convert["debugging"] = "debug"

    whitelist = {}
    whitelist["test"] = true
    whitelist["debug"] = true
    whitelist["others"] = true
}

when HTTP_REQUEST {
    local path = HTTP:path()

    path = path:gsub("^/api/", "/api2/") -- convert /api/ to /api2/
    local api = path:match("^/api2/(.+)") -- get api string that is after /api2/

    -- check api
    if api then
        if actions[api] then -- if api is in table "actions", run function
            return actions[api](HTTP)
        end
        if convert[api] then -- if api is in table "convert", convert api
            api = convert[api] -- change to new api
        end
        if not whitelist[api] then -- if api is not in whitelist, reply invalid
            return reply_invalid(HTTP)
        end
        HTTP:set_path("/api2/" .. api) -- pass the api to server
        return
    end

    -- if path doesn't starts with /api or /api2, do nothing
}

SSL commands

-- Example of SSL
-- SET SNI when the fwb is about to send client hello to pservers based on the host name of client2FWB request


-- the variable to record the host of client2fortiweb request
local host_name

-- set the host name
when SERVERSSL_CLIENTHELLO_SEND {
    local name = host_name
    if name then
        debug("set Server Name Indication(SNI) in ClientHello = %s\n", name)
        SSL:set_sni(name)
    end

}

-- a function to print a table, i represents the number of \t for formatting purpose.
function print_table(table, indent)
    local space = string.rep('\t',indent)
    for key, value in pairs(table) do
        if(type(value)=='table') then
             debug("%s sub-table[%s]\n", space, key)
             print_table(value, indent+1)
        else
            debug("%s %s: %s\n", space, key, value)
        end
    end
end



when CLIENTSSL_HANDSHAKE {
    -- get server name extension.
    local svr_name = SSL:sni()
    if svr_name then
        debug("client handshake sni: %s\n", svr_name)
    end

    if svr_name == "www.xyz.com" then
        SSL:close()
        debug("client handshake sni: %s, close the connection\n", svr_name)
    end

    -- get ssl_version
    local ssl_version = SSL:version()
    debug("client ssl version : %s\n", ssl_version)


    -- get alpn_protocol
    local alpn_protocol = SSL:alpn()
    if alpn_protocol then
        debug("alpn_protocol in client handshake =  %s\n", alpn_protocol)
    end


    -- get cipher
    local cipher = SSL:cipher()
    if cipher then
        debug("cipher in client handshake =%s\n", cipher)
    end

    -- get client ceritificate information
    if SSL:client_cert_verify() then
        debug("client cert verify enabled\n")
        local cert_cnt = SSL:cert_count()
        debug("number of certificates %d\n", cert_cnt)
        -- get the client leaf cert information
        if cert_cnt >= 1 then
            for i = 0, cert_cnt-1 do
              -- code to be executed
              debug("cert index %d\n", i)
              local cert_table = SSL:get_peer_cert_by_idx(i)
              print_table(cert_table, 0)
            end
        end
        debug("verify result: %d\n", SSL: verify_result())
    end
}

when HTTP_REQUEST {
    local host = HTTP:header("host")[1]
    if host then
        host_name = host
        debug("when http request, set the global host_name = %s\n", host)
    end
}


when SERVERSSL_HANDSHAKE {

    debug("when server ssl handshake \n")
    -- get server name extension.
    local svr_name = SSL:sni()
    if svr_name then
        debug(" handshake sni: %s\n", svr_name)
    end

    if svr_name == "www.abc.com" then
        SSL:close()
        debug("client handshake sni: %s, close the connection\n", svr_name)
    end

    -- get ssl_version
    local ssl_version = SSL:version()
    debug(" ssl version : %s\n", ssl_version)


    -- get alpn_protocol
    local alpn_protocol = SSL:alpn()
    if alpn_protocol then
        debug("alpn_protocol in handshake =  %s\n", alpn_protocol)
    end

    -- get cipher
    local cipher = SSL:cipher()
    if cipher then
        debug("cipher in client handshake =%s\n", cipher)
    end
}

URL certificate verify by SSL renegotiation

-- In this sample script, when an HTTPS request with the prefix "autotest" is received,
-- it triggers client certificate verification through SSL renegotiation.
-- Once the SSL renegotiation is completed, it checks the content-routing policy.
-- If the client certificate presented by the client meets certain conditions, it matches a specific HTTP content routing policy,
-- directing traffic to a designated server pool.


-- a function to print a table, i represents the number of \t for formatting purpose.
function print_table(table, indent)
    local space = string.rep('\t',indent)
    for key, value in pairs(table) do
        if(type(value)=='table') then
             debug("%s sub-table[%s]\n", space, key)
             print_table(value, indent+1)
        else
            debug("%s %s: %s\n", space, key, value)
        end
    end
end

when HTTP_REQUEST {
    local url = HTTP:url()
    if url:find("^/autotest") and HTTP:is_https() and SSL:client_cert_verify() then
        -- Trigger SSL renegotiate only when it's https request and SSL connection has already been established
        -- Example URL-based certificate verify and then Content-Routing
        debug("url: %s match rule, need client certificate verify\n", url)
        local cert_count = SSL:cert_count()
        debug("cert_count = %s\n", cert_count)
        if cert_count and cert_count == 0 then
            SSL:renegotiate()
            debug("emit SSL renegotiation\n")
        end
    end
}

when CLIENTSSL_RENEGOTIATE {
    local cert_count = SSL:cert_count()
    debug("cert_count = %s\n", cert_count)
    if cert_count and cert_count > 0 then
        local cert_table = SSL:get_peer_cert_by_idx(0)
        print_table(cert_table, 0)
        local subject = cert_table["subject"]
        -- match CN value with regular expression
        local cn_value = subject:match("CN%s-=%s-([^,%s]+)")
        debug("CN value in X509 subject is: %s\n", cn_value)
        if cn_value and cn_value == "test1" then
            LB:routing("ctrt")
        end
    end
}

Utility functions demo

-- This is a demo for utilities functions that are available in all types of events.
-- For more details, please refer to FortiWeb Administration Guide and Script Reference Guide.


-- helper function to convert byte string into hex representation
function bytes2hex(bytestr)
    local hexString = ""
    for i = 1, string.len(bytestr) do
        hexString = hexString .. string.format("%02x", string.byte(bytestr, i))
    end
    return hexString
end

when HTTP_REQUEST {

    -- Generates a random number
    -- returns an integer value between 0 and RAND_MAX(2^31-1)
    local rand_num = rand()
    debug("rand_num=%d\n",rand_num)

    -- time(), return the current time as an integer, in Unix time format
    local now = time()
    debug("time now = %d\n", now)

    -- time_ms(), return the current time in million seconds, in Unix time format
    local now_ms = time_ms()
    debug("time now in million seconds = %d\n", now_ms)

    -- ctime(), return the current time as a string, For instance Thu Apr 15 09:01:46 2024 CST +0800
    local now_str = ctime()
    debug("time now in string format:  %s\n", now_str)

    -- md5(input_msg), return the Calculate the MD5 of a string input and return the result in string representation
    local md5_encrypted = md5_str("123")
    debug("length of md5_encrypted is %d \n", string.len(md5_encrypted))
    debug("encrypted md5 of string 123 is:  %s\n", bytes2hex(md5_encrypted))

    -- md5_hex_str(input_msg), Calculate the hex representation of the MD5 of a string, and return the result in string representation
    local md5_encrypted_hex = md5_hex_str("123")
    debug("encrypted md5 of string 123 in hex representation is:  %s\n", md5_encrypted_hex)

    -- Calculates the SHA1 of a string input, and return the result in string representation
    local sha1_123 = sha1_str("123")
    debug("length of sha1_123 is %d \n", string.len(sha1_123))
    debug("encrypted sha1 of string 123  is:  %s\n", bytes2hex(sha1_123))

    -- Calculates the hex representation of SHA1 of a string input, and return the result in string representation
    local sha1_123_hex = sha1_hex_str("123")
    debug("encrypted sha1 of string 123 in hex representation is:  %s\n", sha1_123_hex)

    -- Calculates the SHA256 of a string input, and return the result in string representation
    local sha256_123 = sha256_str("123")
    debug("length of sha256_123 is %d \n", string.len(sha256_123))
    debug("encrypted sha256 of string 123  is:  %s\n", bytes2hex(sha256_123))

    -- Calculates the hex representation of SHA1 of a string input, and return the result in string representation
    local sha256_123_hex = sha256_hex_str("123")
    debug("encrypted sha256 of string 123 in hex representation is:  %s\n", sha256_123_hex)

    -- Calculates the SHA512 of a string input, and return the result in string representation
    local sha512_123 = sha512_str("123")
    debug("length of sha512_123 is %d \n", string.len(sha512_123))
    debug("encrypted sha512 of string 123  is:  %s\n", bytes2hex(sha512_123))

    -- Calculates the hex representation of SHA1 of a string input, and return the result in string representation
    local sha512_123_hex = sha512_hex_str("123")
    debug("encrypted sha512 of string 123 in hex representation is:  %s\n", sha512_123_hex)

    -- Encodes a string input in base64 and outputs the results in string format.
    local b64_msg = base64_enc("https://www.base64encode.org/")
    debug("base64 encoded message is:  %s\n", b64_msg)

    -- Decodes a base64 encoded string input and outputs the results in string format.
    local b64_dec_msg = base64_dec(b64_msg)
    debug("base64 decoded message is:  %s\n", b64_dec_msg)

    -- Encodes a string input in base32 and outputs the results in string format.
    local b32_msg = base32_enc("https://www.base64encode.org/")
    debug("base32 encoded message is:  %s\n", b32_msg)

    -- Decodes a base32 encoded string input and outputs the results in string format.
    local b32_dec_msg = base32_dec(b32_msg)
    debug("base32 decoded message is:  %s\n", b32_dec_msg)

    -- Converts a long integer input into network byte order
    local network_a  = htonl(32)
    debug("htonl of 32 is:  %s\n", network_a)

    -- Converts a short integer input into network byte order
    local network_a_short  = htons(32)
    debug("htons of 32 is:  %s\n", network_a_short)

    -- Remember htonl(ntohl(x)) == x
    -- Converts a long integer input into host byte order
    local host_a = ntohl(network_a)
    debug("ntohl of network_a is:  %s\n", host_a)

    -- Converts a short integer input into host byte order
    local host_a_short = ntohs(network_a_short)
    debug("ntohs of network_a_short is:  %s\n", host_a_short)

    --Convert a string to its hex representation
    local hexit = to_hex("it")
    debug("hexit is:  %s\n", hexit)

    -- Returns the crc32 check value of the string, return value is the crc32 code
    local crc32_code = crc32("123456789")
    debug("CRC 32 code is:  %d\n", crc32_code)

    -- key_gen(pass, salt, iter, key_len);
    -- This function derives an AES key from a password using a salt and iteration count as specified in RFC 2898 (Password-Based Key Derivation Function 2 with HMAC-SHA256).
    local new_key = key_gen("pass", "salt", 32, 32)
    debug("new key is %s\n", bytes2hex(new_key))

    -- Encrypts a string using AES algorithm
    local aes_encrypted = aes_enc("your message", "paste your key here", 128)
    debug("encrypted in hex is %s, after b64 encoding %s\n", to_hex(aes_encrypted), base64_enc(aes_encrypted))

    -- Decrypt a string using AES algorithm
    local aes_decrypted = aes_dec(aes_encrypted, "paste your key here", 128);
    debug("decrypted msg is %s\n", aes_decrypted)

    -- EVP_Digest(alg, str) EVP_Digest for one-shot digest calculation.
    local evpd = EVP_Digest("MD5", "your data")
    debug("the digest in hex is %s\n", bytes2hex(evpd))

    -- HMAC message authentication code.
    local hm = HMAC("SHA256", "your data", "paste your key here")
    debug("the HMAC in hex is %s\n", bytes2hex(hm))

    -- HMAC_verify(alg, msg, key, verify) Check if the signature is same as the current digest.
    local is_same = HMAC_verify("SHA256", "your data", "paste your key here", hm)
    if is_same then
        debug("HMAC verified\n")
    else
        debug("HMAC not verified\n")
    end

    -- Generate a random number in HEX
    local rand_h = rand_hex(16);
    debug("the random hex number  is %s\n", rand_h);

    -- Generate a random alphabet+number sequence:
    local alphanumber = rand_alphanum(16);
    debug("the alphabet+number sequence  is %s\n", alphanumber);

    -- Generate a random number sequence:
    local randseq = rand_seq(16);
    debug("the random sequence is %s\n", to_hex(randseq));

    -- Encode the target url (Converted the url into a valid ASCII format, will not replace space by "+" sign)
    local encoded_url = url_encode("https://docs.fortinet.com/product/fortiweb/7.4");
    debug("the encoded url is %s\n", encoded_url);

    -- Decode the encoding-url into its original url
    local decoded_url = url_decode(encoded_url);
    debug("the decoded url is %s\n", decoded_url);
}

Use Cases

Use Cases

HTTP to HTTPS redirection

-- HTTP to HTTPS redirection for default HTTP and HTTPS ports
when HTTP_REQUEST {
	if not HTTP:is_https() then
		HTTP:redirect("https://%s%s", HTTP:header("host")[1], HTTP:url())
	end
}

HTTP to HTTPS redirection for special ports

Only use the first port in HTTPS service.

when HTTP_REQUEST {
	if not HTTP:is_https() then
		local host = HTTP:header("host")[1]
		local https_port = policy.https_ports()[1] -- get the first port in HTTP service
		local newhost = host:gsub(":(%d+)", "") -- remove port from host if it has

		if https_port ~= 443 then
			-- if https port is not 443, add port to host
			newhost = newhost .. ":" .. tostring(https_port)
		end

		HTTP:redirect("https://%s%s", newhost, HTTP:url())
	end
}

HTTP get commands

when HTTP_REQUEST {
    debug("============= Dump HTTP request header =============\n")
    debug("host: %s, path: %s, url: %s, method: %s, version: %s, content type: %s\n",
            HTTP:host(), HTTP:path(), HTTP:url(), HTTP:method(), HTTP:version(), HTTP:content_type())
    for k, v in pairs(HTTP:headers()) do
        for i = 1, #v do
            debug("HEADER: %s[%d]: %s\n", k, i, v[i])
        end
    end
    for k, v in pairs(HTTP:cookies()) do
        debug("Cookie: %s = %s\n", k, v)
    end
    for k, v in pairs(HTTP:args()) do
        debug("ARGS: %s = %s\n", k, v)
    end
    debug("========== Dump HTTP request header done ===========\n")
}

when HTTP_RESPONSE {
    debug("============= Dump HTTP response header =============\n")
    debug("version:%s, content-type: %s\n", HTTP:version(), HTTP:content_type())
    debug("status code: %s reason: %s\n", HTTP:status())
    for k, v in pairs(HTTP:headers()) do
        for i = 1, #v do
            debug("HEADER: %s[%d]: %s\n", k, i, v[i])
        end
    end
    for k, v in pairs(HTTP:cookies()) do
        debug("Cookie: %s = %s\n", k, v)
    end
    debug("========== Dump HTTP response header done ===========\n")
}

IP_PKG example

when CLIENT_ACCEPTED {
    local client_ip = IP:client_addr()
    local r = ip.reputation(client_ip)
    debug("Client IP: %s, reputation: <%s>, GEO country: <%s>, GEO country code: %s\n",
            client_ip,
            #r > 0 and table.concat(r, ', ') or "No Found",
            ip.geo(client_ip) or "unknown",
            ip.geo_code(client_ip) or "unknown")
}

TCPIP commands

function print_ips(event, TCP, IP)
    debug("%s: version: %s, local: %s:%s, remote: %s:%s, client: %s:%s, server: %s:%s\n",
            event, IP:version(),
            IP:local_addr(), TCP:local_port(),
            IP:remote_addr(), TCP:remote_port(),
            IP:client_addr(), TCP:client_port(),
            IP:server_addr(), TCP:server_port())
end

when CLIENT_ACCEPTED {
    print_ips("CLIENT_ACCEPTED", TCP, IP)
}

when HTTP_REQUEST {
    print_ips("HTTP_REQUEST", TCP, IP)
}

when HTTP_RESPONSE {
    print_ips("HTTP_RESPONSE", TCP, IP)
}

when SERVER_CONNECTED {
    print_ips("SERVER_CONNECTED", TCP, IP)
}

when SERVER_CLOSED {
    print_ips("SERVER_CLOSED", TCP, IP)
}

when CLIENT_CLOSED {
    print_ips("CLIENT_CLOSED", TCP, IP)
}

Content routing by URL

-- The policy should have four content routing configuration: cr, cr1, cr2, cr3
-- Don't need set any rule in the content routing, all rules will be ignored 
-- after calling LB:routing("content-routing-name")
when HTTP_REQUEST {
    local url = HTTP:url()

    if url:find("^/sports") or url:find("^/news") or url:find("^/government") then
        LB:routing("cr1")
        debug("url %s starts with sports|news|government, routing to cr1\n", url)
    elseif url:find("^/finance") or url:find("^/technology") or url:find("^/shopping") then
        LB:routing("cr2")
        debug("url %s starts with finance|technology|shopping, routing to cr2\n", url)
    elseif url:find("^/game") or url:find("^/travel") then
        LB:routing("cr3")
        debug("url %s starts with game|travel, routing to cr3\n", url)
    else
        LB:routing("cr")
        debug("No match for uri: %s, routing to default cr\n", url)
    end
}

Full scan for XFF

-- This script will full scan the XFF headers, if it found any IP 
-- in IP reputation database, it will close the HTTP connection

-- This function extracts all IPs from XFF headers and returns IP array
function extract_xff(xff)
	local t = {}
	local k, v, s

	for k, v in ipairs(xff) do
		for s in v:gmatch("([^,]+)") do
			t[#t + 1] = s:gsub("%s+", "")
		end
	end
	return t
end

when HTTP_REQUEST {
	local ips = extract_xff(HTTP:header("X-Forwarded-For"))
	local r, i, v

	for i, v in ipairs(ips) do
		r = ip.reputation(v) -- check ip, will return an array
		if #r > 0 then -- Found IP in reputation database
			debug("Found bad IP %s in XFF headers, reputation: <%s>, GEO country: <%s>, GEO country code: %s\n",
				v, table.concat(r, ', '),
				ip.geo(v) or "unknown", ip.geo_code(v) or "unknown")
			HTTP:close() -- force close this HTTP connection
			return -- Stop script and return
		end
	end
}

Persist by cookie

-- Example of LB persistence
-- the type of persistence of the server pool must be "scripting"
-- LB:persist(string, timeout)
--   string: persist on this string, can use any string
--   timeout: persist on this timeout, unit is second,
--			  if doesn't exist, use the default value in the persistence configuration

cookie_name = "anyone"

when HTTP_REQUEST {
	local value = HTTP:cookie(cookie_name)
	if value then
		-- persist on value and the timeout is default
		-- will record the persistence when doesn't find the persistence
	    LB:persist(value)
	end
}

when HTTP_RESPONSE {
	local value = HTTP:cookie(cookie_name)
	if value then
		-- if server respone has this cookie
		-- record the persistence to the persistence table
		LB:persist(value)
	end
}

HTTP rewrite headers

-- Example of rewriting HTTP headers
-- GET /rewrite_request to rewrite the request with arguments
-- GET /rewrite_response to rewrite the response with arguments

function rewrite_request(HTTP, args)
    local v
  
    -- rewrite method
    v = args["method"]
    if v then HTTP:set_method(v) end

    -- rewrite path
    v = args["path"]
    if v then HTTP:set_path(v) end

    -- add/del/set header
    HTTP:del_header("cookie") -- remove header "cookie"
    HTTP:add_header("Accept", "*/*") -- add a new header line
    HTTP:set_header("Test", { "line1", "line2", "line3" }) -- add new header
end

function rewrite_response(HTTP, args)
    local v

    -- rewrite status, includes code and reason
    v = args["code"]
    if v then v = v:match("%d+") end -- get first number from code string
    if v then
        local code = tonumber(v) -- convert code string to number
        local reason = args["reason"]

        if reason then
            HTTP:set_status(code, reason) -- if reason exists, set code and reason 
        else
            HTTP:set_status(code) -- set code and use default reason
        end
    end
end
when HTTP_REQUEST {
    local path = HTTP:path()

    if path == "/rewrite_request" then
        rewrite_request(HTTP, HTTP:args())
    elseif path == "/rewrite_response" then
        -- use the HTTP transaction private data to pass data to response
        -- Do not use the global variable, it is shared with all TCP connections and HTTP transactions
        local t = HTTP:priv() -- get the HTTP transaction private table, it is an empty table on first read
        t["rewrite_response"] = true -- set rewrite_response to true in the HTTP transaction private table
        t["args"] = HTTP:args() -- store request arguments to the private table
    end

    HTTP:set_query("test=1")
}

when HTTP_RESPONSE {
    local t = HTTP:priv()

    if t["rewrite_response"] then
        return rewrite_response(HTTP, t["args"])
    end
}

HTTP custom reply

-- Example of HTTP custom reply

function reply_invalid(HTTP)
    HTTP:reply{
        status = 400,
        headers = {
                ["content-type"] = { "text/html" },
                ["cache-control"] = { "no-cache", "no-store" },
        },
        body = "<html><body><h1>Invalid API Request<h1></body></html>",
    }
end

function check_ip_reputation(HTTP)
    local v = HTTP:arg("ip")
    if v then v = ip.addr(v) end -- convert string to ip
    if not v then return reply_invalid(HTTP) end

    local r = ip.reputation(v)
    local body = string.format("<html><body><h1>Reputation of IP %s: %s<h1></body></html>",
        v, #r > 0 and table.concat(r, ', ') or "No Found")

    HTTP:reply{
        status = 200,
        headers = {
                ["content-type"] = { "text/html" },
                ["cache-control"] = { "no-cache", "no-store" },
        },
        body = body,
    }
end

function check_ip_geo(HTTP)
    local v = HTTP:arg("ip")
    if v then v = ip.addr(v) end -- convert string to ip
    if not v then return reply_invalid(HTTP) end

    local geo = ip.geo(v)
    local geo_code = ip.geo_code(v)
    local body = string.format("<html><body><h1>GEO of IP %s: %s, code: %s<h1></body></html>",
        v, geo, geo_code)

    HTTP:reply{
        status = 200,
        headers = {
                ["content-type"] = { "text/html" },
                ["cache-control"] = { "no-cache", "no-store" },
        },
        body = body,
    }
end

when RULE_INIT {
    actions = {}
    actions["reputation"] = check_ip_reputation
    actions["geo"] = check_ip_geo

    convert = {}
    convert["testing"] = "test"
    convert["debugging"] = "debug"

    whitelist = {}
    whitelist["test"] = true
    whitelist["debug"] = true
    whitelist["others"] = true
}

when HTTP_REQUEST {
    local path = HTTP:path()

    path = path:gsub("^/api/", "/api2/") -- convert /api/ to /api2/
    local api = path:match("^/api2/(.+)") -- get api string that is after /api2/

    -- check api
    if api then
        if actions[api] then -- if api is in table "actions", run function
            return actions[api](HTTP)
        end
        if convert[api] then -- if api is in table "convert", convert api
            api = convert[api] -- change to new api
        end
        if not whitelist[api] then -- if api is not in whitelist, reply invalid
            return reply_invalid(HTTP)
        end
        HTTP:set_path("/api2/" .. api) -- pass the api to server
        return
    end

    -- if path doesn't starts with /api or /api2, do nothing
}

SSL commands

-- Example of SSL
-- SET SNI when the fwb is about to send client hello to pservers based on the host name of client2FWB request


-- the variable to record the host of client2fortiweb request
local host_name

-- set the host name
when SERVERSSL_CLIENTHELLO_SEND {
    local name = host_name
    if name then
        debug("set Server Name Indication(SNI) in ClientHello = %s\n", name)
        SSL:set_sni(name)
    end

}

-- a function to print a table, i represents the number of \t for formatting purpose.
function print_table(table, indent)
    local space = string.rep('\t',indent)
    for key, value in pairs(table) do
        if(type(value)=='table') then
             debug("%s sub-table[%s]\n", space, key)
             print_table(value, indent+1)
        else
            debug("%s %s: %s\n", space, key, value)
        end
    end
end



when CLIENTSSL_HANDSHAKE {
    -- get server name extension.
    local svr_name = SSL:sni()
    if svr_name then
        debug("client handshake sni: %s\n", svr_name)
    end

    if svr_name == "www.xyz.com" then
        SSL:close()
        debug("client handshake sni: %s, close the connection\n", svr_name)
    end

    -- get ssl_version
    local ssl_version = SSL:version()
    debug("client ssl version : %s\n", ssl_version)


    -- get alpn_protocol
    local alpn_protocol = SSL:alpn()
    if alpn_protocol then
        debug("alpn_protocol in client handshake =  %s\n", alpn_protocol)
    end


    -- get cipher
    local cipher = SSL:cipher()
    if cipher then
        debug("cipher in client handshake =%s\n", cipher)
    end

    -- get client ceritificate information
    if SSL:client_cert_verify() then
        debug("client cert verify enabled\n")
        local cert_cnt = SSL:cert_count()
        debug("number of certificates %d\n", cert_cnt)
        -- get the client leaf cert information
        if cert_cnt >= 1 then
            for i = 0, cert_cnt-1 do
              -- code to be executed
              debug("cert index %d\n", i)
              local cert_table = SSL:get_peer_cert_by_idx(i)
              print_table(cert_table, 0)
            end
        end
        debug("verify result: %d\n", SSL: verify_result())
    end
}

when HTTP_REQUEST {
    local host = HTTP:header("host")[1]
    if host then
        host_name = host
        debug("when http request, set the global host_name = %s\n", host)
    end
}


when SERVERSSL_HANDSHAKE {

    debug("when server ssl handshake \n")
    -- get server name extension.
    local svr_name = SSL:sni()
    if svr_name then
        debug(" handshake sni: %s\n", svr_name)
    end

    if svr_name == "www.abc.com" then
        SSL:close()
        debug("client handshake sni: %s, close the connection\n", svr_name)
    end

    -- get ssl_version
    local ssl_version = SSL:version()
    debug(" ssl version : %s\n", ssl_version)


    -- get alpn_protocol
    local alpn_protocol = SSL:alpn()
    if alpn_protocol then
        debug("alpn_protocol in handshake =  %s\n", alpn_protocol)
    end

    -- get cipher
    local cipher = SSL:cipher()
    if cipher then
        debug("cipher in client handshake =%s\n", cipher)
    end
}

URL certificate verify by SSL renegotiation

-- In this sample script, when an HTTPS request with the prefix "autotest" is received,
-- it triggers client certificate verification through SSL renegotiation.
-- Once the SSL renegotiation is completed, it checks the content-routing policy.
-- If the client certificate presented by the client meets certain conditions, it matches a specific HTTP content routing policy,
-- directing traffic to a designated server pool.


-- a function to print a table, i represents the number of \t for formatting purpose.
function print_table(table, indent)
    local space = string.rep('\t',indent)
    for key, value in pairs(table) do
        if(type(value)=='table') then
             debug("%s sub-table[%s]\n", space, key)
             print_table(value, indent+1)
        else
            debug("%s %s: %s\n", space, key, value)
        end
    end
end

when HTTP_REQUEST {
    local url = HTTP:url()
    if url:find("^/autotest") and HTTP:is_https() and SSL:client_cert_verify() then
        -- Trigger SSL renegotiate only when it's https request and SSL connection has already been established
        -- Example URL-based certificate verify and then Content-Routing
        debug("url: %s match rule, need client certificate verify\n", url)
        local cert_count = SSL:cert_count()
        debug("cert_count = %s\n", cert_count)
        if cert_count and cert_count == 0 then
            SSL:renegotiate()
            debug("emit SSL renegotiation\n")
        end
    end
}

when CLIENTSSL_RENEGOTIATE {
    local cert_count = SSL:cert_count()
    debug("cert_count = %s\n", cert_count)
    if cert_count and cert_count > 0 then
        local cert_table = SSL:get_peer_cert_by_idx(0)
        print_table(cert_table, 0)
        local subject = cert_table["subject"]
        -- match CN value with regular expression
        local cn_value = subject:match("CN%s-=%s-([^,%s]+)")
        debug("CN value in X509 subject is: %s\n", cn_value)
        if cn_value and cn_value == "test1" then
            LB:routing("ctrt")
        end
    end
}

Utility functions demo

-- This is a demo for utilities functions that are available in all types of events.
-- For more details, please refer to FortiWeb Administration Guide and Script Reference Guide.


-- helper function to convert byte string into hex representation
function bytes2hex(bytestr)
    local hexString = ""
    for i = 1, string.len(bytestr) do
        hexString = hexString .. string.format("%02x", string.byte(bytestr, i))
    end
    return hexString
end

when HTTP_REQUEST {

    -- Generates a random number
    -- returns an integer value between 0 and RAND_MAX(2^31-1)
    local rand_num = rand()
    debug("rand_num=%d\n",rand_num)

    -- time(), return the current time as an integer, in Unix time format
    local now = time()
    debug("time now = %d\n", now)

    -- time_ms(), return the current time in million seconds, in Unix time format
    local now_ms = time_ms()
    debug("time now in million seconds = %d\n", now_ms)

    -- ctime(), return the current time as a string, For instance Thu Apr 15 09:01:46 2024 CST +0800
    local now_str = ctime()
    debug("time now in string format:  %s\n", now_str)

    -- md5(input_msg), return the Calculate the MD5 of a string input and return the result in string representation
    local md5_encrypted = md5_str("123")
    debug("length of md5_encrypted is %d \n", string.len(md5_encrypted))
    debug("encrypted md5 of string 123 is:  %s\n", bytes2hex(md5_encrypted))

    -- md5_hex_str(input_msg), Calculate the hex representation of the MD5 of a string, and return the result in string representation
    local md5_encrypted_hex = md5_hex_str("123")
    debug("encrypted md5 of string 123 in hex representation is:  %s\n", md5_encrypted_hex)

    -- Calculates the SHA1 of a string input, and return the result in string representation
    local sha1_123 = sha1_str("123")
    debug("length of sha1_123 is %d \n", string.len(sha1_123))
    debug("encrypted sha1 of string 123  is:  %s\n", bytes2hex(sha1_123))

    -- Calculates the hex representation of SHA1 of a string input, and return the result in string representation
    local sha1_123_hex = sha1_hex_str("123")
    debug("encrypted sha1 of string 123 in hex representation is:  %s\n", sha1_123_hex)

    -- Calculates the SHA256 of a string input, and return the result in string representation
    local sha256_123 = sha256_str("123")
    debug("length of sha256_123 is %d \n", string.len(sha256_123))
    debug("encrypted sha256 of string 123  is:  %s\n", bytes2hex(sha256_123))

    -- Calculates the hex representation of SHA1 of a string input, and return the result in string representation
    local sha256_123_hex = sha256_hex_str("123")
    debug("encrypted sha256 of string 123 in hex representation is:  %s\n", sha256_123_hex)

    -- Calculates the SHA512 of a string input, and return the result in string representation
    local sha512_123 = sha512_str("123")
    debug("length of sha512_123 is %d \n", string.len(sha512_123))
    debug("encrypted sha512 of string 123  is:  %s\n", bytes2hex(sha512_123))

    -- Calculates the hex representation of SHA1 of a string input, and return the result in string representation
    local sha512_123_hex = sha512_hex_str("123")
    debug("encrypted sha512 of string 123 in hex representation is:  %s\n", sha512_123_hex)

    -- Encodes a string input in base64 and outputs the results in string format.
    local b64_msg = base64_enc("https://www.base64encode.org/")
    debug("base64 encoded message is:  %s\n", b64_msg)

    -- Decodes a base64 encoded string input and outputs the results in string format.
    local b64_dec_msg = base64_dec(b64_msg)
    debug("base64 decoded message is:  %s\n", b64_dec_msg)

    -- Encodes a string input in base32 and outputs the results in string format.
    local b32_msg = base32_enc("https://www.base64encode.org/")
    debug("base32 encoded message is:  %s\n", b32_msg)

    -- Decodes a base32 encoded string input and outputs the results in string format.
    local b32_dec_msg = base32_dec(b32_msg)
    debug("base32 decoded message is:  %s\n", b32_dec_msg)

    -- Converts a long integer input into network byte order
    local network_a  = htonl(32)
    debug("htonl of 32 is:  %s\n", network_a)

    -- Converts a short integer input into network byte order
    local network_a_short  = htons(32)
    debug("htons of 32 is:  %s\n", network_a_short)

    -- Remember htonl(ntohl(x)) == x
    -- Converts a long integer input into host byte order
    local host_a = ntohl(network_a)
    debug("ntohl of network_a is:  %s\n", host_a)

    -- Converts a short integer input into host byte order
    local host_a_short = ntohs(network_a_short)
    debug("ntohs of network_a_short is:  %s\n", host_a_short)

    --Convert a string to its hex representation
    local hexit = to_hex("it")
    debug("hexit is:  %s\n", hexit)

    -- Returns the crc32 check value of the string, return value is the crc32 code
    local crc32_code = crc32("123456789")
    debug("CRC 32 code is:  %d\n", crc32_code)

    -- key_gen(pass, salt, iter, key_len);
    -- This function derives an AES key from a password using a salt and iteration count as specified in RFC 2898 (Password-Based Key Derivation Function 2 with HMAC-SHA256).
    local new_key = key_gen("pass", "salt", 32, 32)
    debug("new key is %s\n", bytes2hex(new_key))

    -- Encrypts a string using AES algorithm
    local aes_encrypted = aes_enc("your message", "paste your key here", 128)
    debug("encrypted in hex is %s, after b64 encoding %s\n", to_hex(aes_encrypted), base64_enc(aes_encrypted))

    -- Decrypt a string using AES algorithm
    local aes_decrypted = aes_dec(aes_encrypted, "paste your key here", 128);
    debug("decrypted msg is %s\n", aes_decrypted)

    -- EVP_Digest(alg, str) EVP_Digest for one-shot digest calculation.
    local evpd = EVP_Digest("MD5", "your data")
    debug("the digest in hex is %s\n", bytes2hex(evpd))

    -- HMAC message authentication code.
    local hm = HMAC("SHA256", "your data", "paste your key here")
    debug("the HMAC in hex is %s\n", bytes2hex(hm))

    -- HMAC_verify(alg, msg, key, verify) Check if the signature is same as the current digest.
    local is_same = HMAC_verify("SHA256", "your data", "paste your key here", hm)
    if is_same then
        debug("HMAC verified\n")
    else
        debug("HMAC not verified\n")
    end

    -- Generate a random number in HEX
    local rand_h = rand_hex(16);
    debug("the random hex number  is %s\n", rand_h);

    -- Generate a random alphabet+number sequence:
    local alphanumber = rand_alphanum(16);
    debug("the alphabet+number sequence  is %s\n", alphanumber);

    -- Generate a random number sequence:
    local randseq = rand_seq(16);
    debug("the random sequence is %s\n", to_hex(randseq));

    -- Encode the target url (Converted the url into a valid ASCII format, will not replace space by "+" sign)
    local encoded_url = url_encode("https://docs.fortinet.com/product/fortiweb/7.4");
    debug("the encoded url is %s\n", encoded_url);

    -- Decode the encoding-url into its original url
    local decoded_url = url_decode(encoded_url);
    debug("the decoded url is %s\n", decoded_url);
}