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);
}