WFORCE.CONF(5)

πŸ“¦
version 3.0.0 Β· 3 versions
, in ubuntu stonking

πŸ’– WFORCE.CONF(5) πŸ’–

okay bestie, welcome to the wforce.conf manual page β€” yes it's a section 5 file format page, and yes it's literally the configuration file for the wforce daemon. drink water, stretch, and let's get into it because this one is long and we're doing all of it 🌸


✨ NAME ✨

wforce.conf β€” configuration file for wforce daemon πŸ‘‘

🌈 DESCRIPTION 🌈

bestie. listen. this file is read by wforce and it is a Lua script. not a config file with sad little key = value lines β€” a whole Lua script. it contains Lua commands that do two (2) big jobs:

  • (a) configuration of the wforce daemon, and
  • (b) functions that control the operation of the wforce daemon in response to HTTP commands β€” specifically "report", "allow" and "reset" πŸ’…

you can point at a different version of this file with:

wforce -C private_wforce.conf ...

slay. custom config file. main character energy fr fr πŸ¦‹


πŸŽ€ CONFIGURATION-ONLY FUNCTIONS πŸŽ€

okay so the following functions are for configuration of wforce only β€” like, they cannot be called inside the allow/report/reset functions. no cap. keep them in the config zone πŸ”₯

  • setACL(<list of netmasks>) β€” set the access control list for the HTTP Server. for example, to allow access from any IP address, you specify:
setACL({"0.0.0.0/0"})
  • addACL(<netmask>) β€” add a netmask to the access control list for the HTTP server. for example, to allow access from 127.0.0.0/8, you specify:
addACL("127.0.0.0/8")
  • addWebHook(<list of events>, <config key map>) β€” add a webhook for the specified events, with the specified configuration keys. see wforce_webhook(5) for the list of events, supported configuration keys, and details of the HTTP(S) POST sent to webhook URLs. for example:
config_keys={}
config_keys["url"] = "http://webhooks.example.com:8080/webhook/"
config_keys["secret"] = "verysecretcode"
events = { "report", "allow" }
addWebHook(events, config_keys)
  • addCustomWebHook(<custom webhook name>, <config key map>) β€” add a custom webhook, i.e. one which can be called from Lua (using "runCustomWebHook()" β€” see below) with arbitrary data, using the specified configuration keys. see wforce_webhook(5) for the supported config keys, and details of the HTTP(S) POST sent to webhook URLs. for example:
config_keys={}
config_keys["url"] = "http://webhooks.example.com:8080/webhook/"
config_keys["secret"] = "verysecretcode"
addCustomWebHook("mycustomhook", config_keys)
  • addCustomStat(<stat name>) β€” add a custom counter which can be used to track statistics. the stats for custom counters are logged every 5 minutes. the counter is incremented with the "incCustomStat" command. for example:
addCustomStat("custom_stat1")
  • setSiblings(<list of IP/Host[:port[:protocol]]>) β€” set the list of siblings to which stats db and blacklist/whitelist data should be replicated. if port is not specified it defaults to 4001. if protocol is not specified it defaults to udp. this function is safe to call while wforce is running. for example:
setSiblings({"127.0.1.2", "sibling1.example.com:4004", "[::1]:4004:tcp"})
  • setSiblingsWithKey(<list of {IP/Host[:port[:protocol]], Encryption Key>) β€” identical to setSiblings() except that it allows an encryption key to be specified for each sibling. for example:
setSiblingsWithKey({{"127.0.1.2", "Ay9KXgU3g4ygK+qWT0Ut4gH8PPz02gbtPeXWPdjD0HE="}, {"127.0.1.3:4004:tcp", "KaiQkCHloe2ysXv2HbxBAFqHI4N8+ahmwYwsbYlDdF0="}})
  • addSibling(<IP/Hostname[:port[:protocol]]>) β€” add a sibling to the list to which all stats db and blacklist/whitelist data should be replicated. use [] to enclose IPv6 addresses. if port is not specified it defaults to 4001. if protocol is not specified it defaults to udp. this function is safe to call while wforce is running. for example:
addSibling("192.168.1.23")
addSibling("192.168.1.23:4001:udp")
addSibling("192.168.1.23:4003:tcp")
addSibling("[::1]:4003:tcp")
addSibling("sibling1.example.com")
  • addSiblingWithKey(<IP[:port[:protocol]]>, <Encryption Key>) β€” identical to addSibling(), except that an encryption key is specified to enable per-sibling encryption. for example:
addSiblingWithKey("192.168.1.23", "Ay9KXgU3g4ygK+qWT0Ut4gH8PPz02gbtPeXWPdjD0HE=")
  • removeSibling(<IP/Host[:port[:protocol]]>) β€” remove a sibling to the list to which all stats db and blacklist/whitelist data should be replicated. use [] to enclose IPv6 addresses. if port is not specified it defaults to 4001. if protocol is not specified it defaults to udp. this function is safe to call while wforce is running. for example:
removeSibling("192.168.1.23")
removeSibling("sibling1.example.com:4001:udp")
removeSibling("[::1]:4003:tcp")
  • siblingListener(<IP[:port]>) β€” listen for reports from siblings on the specified IP address and port. if port is not specified it defaults to 4001. Wforce will always listen on both UDP and TCP ports. for example:
siblingListener("0.0.0.0:4001")
siblingListener("[::1]:4001")
  • setSiblingConnectTimeout(<timeout ms>) β€” sets a timeout in milliseconds for new connections to siblings. defaults to 5000 ms (5 seconds). for example:
setSiblingConnectTimeout(1000)
  • setMaxSiblingQueueSize(<size>) β€” sets the maximum size of the send and receive queues for replication events waiting to be processed. defaults to 5000. this is only to handle short-term spikes in load/latency β€” if error messages relating to the recv queue max size being reached are seen, then you should consider using sharded string stats dbs (newShardedStringStatsDB), and/or tuning the stats db expiry sleep time (twSetExpireSleep). ✨
setMaxSiblingQueueSize(10000)
  • setJA3AttrName(<attribute name>) β€” sets the attribute name that will be used for determining the JA3 hash. it defaults to ja3. note that a (single-valued) attribute with this name must be present in the LoginTuple attrs object. πŸ‘‘
  • setNamedReportSinks(<name>, <list of IP[:port]>) β€” set a named list of report sinks to which all received reports should be forwarded over UDP. reports will be sent to the configured report sinks for a given name in a round-robin fashion if more than one is specified. reports are sent separately to each named report sink. if port is not specified it defaults to 4501. replaces the deprecated "setReportSinks()". for example:
setNamedReportSinks("logstash", {"127.0.1.2", "127.0.1.3:4501"})
  • addNamedReportSink(<name>, <IP[:port]>) β€” add a report sink to the named list to which all received reports should be forwarded over UDP. reports will be sent to the configured report sinks for a given name in a round-robin fashion if more than one is specified. reports are sent separately to each named report sink. if port is not specified it defaults to 4501. replaces the deprecated "addReportSink()". for example:
addNamedReportSink("logstash", "192.168.1.23")
  • webserver(<IP:port>, <password>) β€” (deprecated β€” see addListener() instead) listen for HTTP commands on the specified IP address and port. the password is used to authenticate client connections using basic authentication. for example:
webserver("0.0.0.0:8084", "super")
  • addListener(<IP:port>, <useSSL>, <cert file>, <key file>, <options>) β€” (replacement for webserver()) listen for HTTP commands on the specified IP address and port. if useSSL is true, then HTTPS must be used, and cert_file and key file are used, otherwise they are empty. options contains a list of key value pairs to configure the TLS connection; these follow the command line option names in https://www.openssl.org/docs/manmaster/man3/SSL_CONF_cmd.html. for example, "min_protocol" to set the minimum TLS protocol version. you can add as many listeners as you choose. for example:
addListener("0.0.0.0:8084", false, "", "", {})
addListener("1.2.3.4:1234", true, "/etc/wforce/cert.pem", "/etc/wforce/key.pem", {minimum_protocol="TLSv1.2"})
addListener("[::1]:9000", true, "/etc/wforce/cert.pem", "/etc/wforce/key.pem", {minimum_protocol="TLSv1.3"})
  • setWebserverPassword(<Password>) β€” (replacement for webserver password parameter) sets the basic authentication password for access to the webserver. this has been decoupled from the addListener() command because multiple listeners can now be created, which was not previously possible. for example:
setWebserverPassword("foobar")
  • controlSocket(<IP[:port]>) β€” listen for control connections on the specified IP address and port. if port is not specified it defaults to 5199. for example:
controlSocket("0.0.0.0:4004")
  • setKey(<key>) β€” use the specified key for authenticating connections from siblings. the key can be generated with makeKey() from the console. see wforce(1) for instructions on running a console client. returns false if the key could not be set (e.g. invalid base64). for example:
if not setKey("Ay9KXgU3g4ygK+qWT0Ut4gH8PPz02gbtPeXWPdjD0HE=")
    then
      ...
  • setNumLuaStates(<num states>) β€” set the number of Lua Contexts that will be created to run allow/report/reset commands. defaults to 10 if not specified. see also setNumWorkerThreads() and setNumSiblingThreads(). should be at least equal to NumWorkerThreads + NumSiblingThreads. for example:
setNumLuaStates(10)
  • setNumWorkerThreads(<num threads>) β€” set the number of threads in the pool used to run allow/report/reset commands. each thread uses a separate Lua Context, (see setNumLuaStates()). defaults to 4 if not specified. for example:
setNumWorkerThreads(4)
  • setNumSiblingThreads(<num threads>) β€” set the number of threads in the pool used to process reports received from siblings. each thread uses a separate Lua Context, the number of which is set with setNumLuaStates(). defaults to 2 if not specified. for example:
setNumSiblingThreads(2)
  • setNumWebHookThreads(<num threads>) β€” set the number of threads in the pool used to send webhook events. defaults to 5 if not specified. for example:
setNumWebHookThreads(2)
  • setMaxWebserverConns(<max conns>) β€” set the maximum number of active connections to the webserver. this can be used to limit the effect of too many queries to wforce. it defaults to 10,000. for example:
setMaxWebserverConns(5000)
  • setNumWebHookConnsPerThread(<num conns>) β€” sets the max number of connections that each WebHook thread gets to play with. If you don't say anything, it defaults to 10. Heads up: this setting replaces the deprecated num_conns per-hook configuration setting, so like… stop using that one bestie πŸ’…
setNumWebHookConnsPerThread(50)
  • setWebHookQueueSize(<queue size>) β€” sets how big the queue for webhook events is. If that queue gets too chonky, webhooks get discarded and an error gets logged 😭. The default queue size is 50000, which honestly should be perfect for most use-cases, no cap.
setWebHookQueueSize(100000)
  • setWebHookTimeoutSecs(<timeout secs>) β€” the max time a request is allowed to take for webhooks, periodt. For example:
setWebHookTimeoutSecs(2)
  • newGeoIP2DB(<db name>, <filename>) β€” opens and initializes a GeoIP2 database ✨. You gotta pick a name, and you also gotta hand over the filename of the database you want opened. Then, to actually get an object you can run lookups with, you use the getGeoIP2DB() function. For example:
newGeoIP2DB("CityDB", "/usr/share/GeoIP/GeoLite2-City.mmdb")
  • initGeoIPDB() β€” Deprecated β€” use newGeoIP2DB() πŸ‘‘. Initializes the country-level IPv4 and IPv6 GeoIP Legacy databases. If either of these databases isn't installed, this command fails and wforce will not start. For example:
initGeoIPDB()
  • initGeoIPCityDB() β€” Deprecated β€” use newGeoIP2DB() 🫢. Initializes the city-level IPv4 and IPv6 GeoIP Legacy databases. Same deal: if either one isn't installed, the command fails and wforce will not start. And listen β€” make sure these databases have the right names if you're rocking the free/lite DBs, because you might need to create symbolic links, e.g. GeoIPCityv6.dat β†’ GeoLiteCityv6.dat. For example:
initGeoIPCityDB()
  • initGeoIPISPDB() β€” Deprecated β€” use newGeoIP2DB() 🌸. Initializes the ISP-level IPv4 and IPv6 GeoIP Legacy databases. And yeah, if either of these isn't installed, this command fails and wforce will not start. For example:
initGeoIPISPDB()
  • newDNSResolver(<resolver name>) β€” creates a shiny new DNS resolver object with the name you give it. Quick note tho: this does not return the resolver object β€” you get that from the getDNSResolver() function. For example:
newDNSResolver("MyResolver")
  • setHLLBits(<num bits>) β€” sets the accuracy of the HyperLogLog algorithm. The value can be anywhere from 4–30, and a higher number means more accuracy β€” but at the cost of using (potentially a lot) more memory πŸ’€. The default value is 6. If you need more accuracy, consider bumping it up slightly, but check your memory usage carefully, fr fr.
  • setCountMinBits(<eps>, <gamma>) β€” sets the accuracy of the CountMin algorithm. eps can be between 0.01 and 1, and a lower number gives more accuracy at the expense of memory. The default is 0.05. gamma is between 0 and 1, and a higher number gives higher accuracy; the default for gamma is 0.2. If you want more accuracy, consider nudging these values slightly, but check your memory usage carefully 🫢.
  • newStringStatsDB(<stats db name>, <window size>, <num windows>, <field map>) β€” creates a new StatsDB object with the name you specify. Just like the others: this does not return the object β€” you get that with the getStringStatsDB() function. For example:
field_map = {}
field_map["countLogins"] = "int"
field_map["diffPasswords"] = "hll"
field_map["countCountries"] = "countmin"
newStringStatsDB("OneHourDB", 900, 4, field_map)
  • newShardedStringStatsDB(<stats db name>, <window size>, <num windows>, <field map>, <num shards>) β€” literally identical to newStringStatsDB except it creates a sharded DB, which is more scalable at higher query loads πŸ”₯. A good starting value for the number of shards might be 10.
  • StringStatsDB:twSetv4Prefix(<prefix>) β€” sets the prefix to use for any IPv4 ComboAddress keys stored in the db. Like, specify 24 to group statistics for class C networks. For example:
statsdb:twSetv4Prefix(24)
  • StringStatsDB:twSetv6Prefix(<prefix>) β€” sets the prefix to use for any IPv6 ComboAddress keys stored in the db. For example:
statsdb:twSetv4Prefix(64)
  • StringStatsDB:twSetMaxSize(<size>) β€” sets the max number of keys allowed to be stored in the db. Once the db hits that size, keys get expired on an LRU basis πŸ‘‹. The default size is 500K keys. For example:
statsdb:twSetMaxSize(1000000)
  • StringStatsDB:twEnableReplication() β€” turns on replication for this db; it only makes a difference if siblings have been configured. For example:
statsdb:twEnableReplication()
  • disableBuiltinBlacklists() β€” disables the built-in blacklisting checks, which lets them be checked from Lua instead. For example:
disableBuiltinBlacklists()
  • blacklistPersistDB(<ip>, <port>) β€” makes the blacklist persistent by storing entries in the specified redis DB πŸ’Ύ. The IP address is a string, and port should be 6379 unless you're running redis on a non-standard port. If you specify this option, wforce will read all the blacklist entries from the redis DB on startup. For example:
blacklistPersistDB("127.0.0.1", 6379)
  • blacklistPersistReplicated() β€” stores blacklist entries that have been replicated in the redis DB. By default, replicated blacklist entries will not be persisted. So: if you use a local redis DB for each wforce server, then use this config option 🫢. But if you use a single redis instance for all your wforce servers, then you should not specify this option, because it'll cause unnecessary writes to the redis DB. For example:
blacklistPersistReplicated()
  • blacklistRedisUsername(<username>) β€” sets the Redis username for authentication to the redis DB. For example:
blacklistRedisUsername("foobar")
  • blacklistRedisPassword(<password>) β€” sets the Redis password for authentication to the redis DB πŸ”. For example:
blacklistRedisPassword("secret")
  • blacklistPersistConnectTimeout(<timeout secs>) β€” sets the connect timeout for connecting to the persistent redis DB. If the timeout is exceeded during connection at startup, wforce will exit; otherwise, during normal operation, if the timeout is exceeded an error gets logged. For example:
blacklistPersistConnectTimeout(2)
  • blacklistPersistRWTimeout(<timeout secs>, <timeout usecs>) β€” sets the timeout for reading from and writing to the Redis DB. For example:
blacklistPersistRWTimeout(0, 50000)
  • disableBuiltinWhitelists() β€” disables the built-in whitelisting checks, so they can be checked from Lua instead. For example:
disableBuiltinWhitelists()
  • whitelistPersistDB(<ip>, <port>) β€” makes the whitelist persistent by storing entries in the specified redis DB ✨. The IP address is a string, and port should be 6379 unless redis is running on a non-standard port. If you specify this option, wforce reads all the whitelist entries from the redis DB on startup. For example:
whitelistPersistDB("127.0.0.1", 6379)
  • whitelistRedisUsername(<username>) β€” sets the Redis username for authentication to the redis DB. For example:
whitelistRedisUsername("foobar")
  • whitelistRedisPassword(<password>) β€” sets the Redis password for authentication to the redis DB πŸ”. For example:
whitelistRedisPassword("secret")
  • whitelistPersistReplicated() β€” stores whitelist entries that have been replicated in the redis DB. By default, replicated whitelist entries are not persisted. If you use a local redis DB for each wforce server, use this config option. But if you use a single redis instance for all wforce servers, you should not specify it, since it'll cause unnecessary writes to the redis DB. For example:
whitelistPersistReplicated()
  • whitelistPersistConnectTimeout(<timeout secs>) β€” sets the connect timeout for connecting to the persistent redis DB. If the timeout is exceeded during connection at startup, wforce will exit; otherwise, during normal operation, if the timeout is exceeded an error gets logged. For example:
whitelistPersistConnectTimeout(2)
  • whitelistPersistRWTimeout(<timeout secs>, <timeout usecs>) β€” sets the timeout for reading from and writing to the Redis DB. For example:
whitelistPersistRWTimeout(0, 50000)
  • setBlacklistIPRetMsg(<msg>) β€” sets the message returned to clients whose IP address is blacklisted 😀. The strings "{ip}" and "{login}" get substituted for the actual IP address and login name. For example:
setBlacklistIPRetMsg("Go away your IP {ip} is blacklisted")
  • setBlacklistLoginRetMsg(<msg>) β€” sets the message returned to clients whose login is blacklisted 😀. The strings "{ip}" and "{login}" get substituted for the actual IP address and login name. For example:
setBlacklistLoginRetMsg("Go away your login {login} is blacklisted")
  • setBlacklistIPLoginRetMsg(<msg>) β€” sets the message returned to clients whose IP address/login is blacklisted 😀. The strings "{ip}" and "{login}" get substituted for the actual IP address and login name. For example:
setBlacklistIPLoginRetMsg("Go away your IP {ip}/Login {login} is blacklisted")
  • setBlacklistJA3RetMsg(<msg>) β€” sets the message returned to clients whose JA3 hash is blacklisted 😀. The string "{ja3}" gets substituted for the actual JA3 hash. For example:
setBlacklistJA3RetMsg("Go away your client {ja3} is blacklisted")
  • setBlacklistIPJA3RetMsg(<msg>) β€” sets the message returned to clients whose IP address/JA3 hash is blacklisted 😀. The strings "{ip}" and "{ja3}" get substituted for the actual IP address and JA3 hash. For example:
setBlacklistIPLoginRetMsg("Go away your IP {ip}/Client {ja3} is blacklisted")
  • setAllow(<allow func>) β€” tells wforce to use the specified Lua function for handling all "allow" commands. For example:
setAllow(allow)
  • setReport(<report func>) β€” tells wforce to use the specified Lua function for handling all "report" commands. For example:
setReport(report)
  • setReset(<reset func>) β€” tells wforce to use the specified Lua function for handling all "reset" commands. For example:

and that reset function gets wired up like this:

setReset(reset)

  • setCanonicalize(<canonicalize func>) β€” bestie, this is how you tell wforce which Lua function handles all the "canonicalisation" duties. What does that even mean? It means mapping every alias a user has onto like, one login string. So ncook, neil.cook and neil.cook@foobar.com all collapse down to neil.cook@foobar.com. 🌸 Want to go look people up in a real user database? Grab Lua modules like LuaSQL-MySQL (luarocks install luasql-mysql) or LuaLDAP (luarocks install lualdap). For example:
setCanonicalize(canonicalize)
  • setCustomEndpoint(<name of endpoint>, <boolean>, <custom lua function>) β€” okay so this one is main character energy: it creates a brand new custom REST endpoint with the name you give it, and when somebody hits that endpoint, your custom Lua function runs. Admins can arbitrarily extend the wforce REST API with new endpoints β€” and make as many as they want, no cap. πŸ’… Heads up though: custom endpoints can only be reached via a POST method, and every argument arrives as key-value pairs of a top-level attrs json object (those get split into two tables β€” one for single-valued attrs, the other for multi-valued attrs β€” see CustomFuncArgs below πŸ‘€). And if that boolean argument is true, then all the arguments to the custom function get sent to all configured named report sinks. The return info comes back with a boolean success and an r_attrs json object holding the return key-value pairs. Peep wforce.conf.example for a full example. For example:
function custom(args)
  for k,v in pairs(args.attrs) do
    infoLog("custom func argument attrs", { key=k, value=v });
  end
  -- return consists of a boolean, followed by { key-value pairs }
  return true, { key=value }
end
setCustomEndpoint("custom", false, custom)
-- Set boolean to true to enable arguments to be sent to all
-- report sinks
-- setCustomEndpoint("custom", true, custom)
  • setCustomGetEndpoint(<name of endpoint>, <custom lua function>) β€” same vibe as above, except this custom REST endpoint is reachable with a GET command (setCustomEndpoint() only does POST, remember). The function's return value is a string, and that string gets handed to the HTTP client as a text/plain response body. For example:
function textIPBlacklist()
    local ipbl = getIPBlacklist()
    local ret_table = {}
    for i,j in pairs(ipbl)
    do
        for k,v in pairs(j)
        do
            if k == "ip"
            then
                table.insert(ret_table, v)
            end
        end
    end
    local s =  table.concat(ret_table, "\n") .. "\n"
    return s
end

setCustomGetEndpoint("textIPBlacklist", textIPBlacklist)
  • setVerboseAllowLog() β€” so when wforce is logging allow requests, it skips logging the ones that return 0, purely for performance reasons (default behaviour, no hate). But if you do want those 0-returning allow requests in your logs, call this function. For example:
setVerboseAllowLog()
  • addSyncHost(<sync host address>, <sync host password>, <replication address>, <callback address>) β€” use this config command if you want wforce to synchronize the contents of its StatsDBs with the rest of a cluster. πŸ¦‹ Once any sync hosts are added, wforce tries to contact them in turn and hunts for a host that's been up longer than the number of seconds you set in setMinSyncHostUptime(). The sync host address should include a port (it defaults to 8084). If it works out, that sync host sends the contents of its StatsDBs to this wforce instance over the replication address you specified β€” and that address should have the same port as configured in siblingListener() and defaults to 4001. When it's all done it notifies this wforce instance using the callback address β€” which should have the same port as configured in webserver and defaults to 8084. N.B. ⚠️ important: the encryption key of the local instance gets sent to the sync host, so if you care about keeping that key confidential, you may want to consider running an HTTPS reverse proxy in front of the sync host. For example:
-- Add 10.2.3.1:8084 as a sync host,
-- and use the password "super"
-- Send the DB dump to 10.2.1.1:4001
-- and let me know on 10.2.1.1:8084 when the dump is finished
addSyncHost("10.2.3.1:8084", "super", "10.2.1.1:4001", "10.2.1.1:8084")
-- Add https://10.2.3.1:8084 as a sync host,
-- and use the password "super"
-- Send the DB dump to https://10.2.1.1:4001
addSyncHost("https://host1.example.com:8084", "super", "10.2.1.1:4001", "https://host2.example.com:8084")
  • setMinSyncHostUptime(<seconds>) β€” the minimum amount of time any sync host has to have been up before it's allowed to send you the contents of its DBs. Defaults to 3600. For example:
setMinSyncHostUptime(1800)
  • disableCurlPeerVerification() β€” turns off peer certificate checking on all outbound HTTPS connections. 🚨 Not recommended, bestie, except for debugging or development purposes. For example:
disableCurlPeerVerification()
  • disableCurlHostVerification() β€” turns off hostname checking in the peer certificate for all outbound HTTPS connections. Again, not recommended unless you're debugging or developing. 🫠
disableCurlHostVerification()
  • setCurlCABundleFile(<Path to CA File>) β€” points at a file holding the certificate authorities to use when checking HTTPS server certificates. Use this when the standard installed root certs don't contain the certs you need. It should be a file containing 1:N certs in PEM format. ✨
setCurlCABundleFile("/etc/ca/local_cas.pem")
  • setCurlClientCertAndKey(<Path to Cert File>, <Path to Key File>) β€” points at the certificate and key files to use for mutual TLS authentication (PEM format, obviously πŸ’…).
setCurlClientCertAndKey("/etc/certs/clientcert.pem", "/etc/certs/clientkey.pem")
  • setMetricsNoPassword() β€” disables password protection for the /metrics endpoint. That's it, that's the whole function. πŸ™ˆ

🌍 GENERAL FUNCTIONS 🌍

These ones are available anywhere β€” either as part of the configuration, or inside the allow/report/reset functions. So versatile. Iconic. ✨

  • getGeoIP2DB(<db name>) β€” returns an object you can use to do GeoIP lookups. Just so you know: the database has to be initialised first with newGeoIP2DB(). For example:
local citydb = getGeoIP2DB("CityDB")
  • GeoIP2DB:lookupCountry(<ComboAddress>) β€” returns the two-character country code of the country that IP address lives in. You can make a ComboAddress object with the newCA() function. For example:
my_country = countrydb:lookupCountry(newCA("8.8.8.8"))
      my_country = countrydb:lookupCountry(lt.remote)
  • GeoIP2DB:lookupISP(<ComboAddress>) β€” returns the name of the ISP hosting that IP address. Make a ComboAddress with newCA(). For example:
local my_isp = ispdb:lookupISP(newCA("128.243.16.21"))
  • GeoIP2DB:lookupCity(<ComboAddress>) β€” returns a map stuffed with info about the IP address, like the city name plus latitude and longitude. The full list of fields lives in GeoIPRecord below πŸ‘‡. For example:
local gip_record = citydb:lookupCity(lt.remote)
local my_city = gip_record.city
local my_latitude = gip_record.latitude
  • GeoIP2DB:lookupStringValue(<ComboAddress>, <array of string values>) β€” returns a string matching the value of the field you specify with that array of string values. For example:
local city_name = citydb:lookupStringValue(newCA(ip_address), {"city", "names", "en"})
  • GeoIP2DB:lookupUIntValue(<ComboAddress>, <array of string values>) β€” returns an integer matching the value of the field you specify with that array of string values. For example:
local accuracy = citydb:lookupUIntValue(newCA(ip_address), {"location", "accuracy_radius"})
  • GeoIP2DB:lookupBoolValue(<ComboAddress>, <array of string values>) β€” returns a boolean matching the value of the field you specify with that array of string values. For example:
local eu = citydb:lookupBoolValue(newCA(ip_address), {"country", "is_in_european_union"})
  • GeoIP2DB:lookupDoubleValue(<ComboAddress>, <array of string values>) β€” returns the value corresponding to the field you specify with that array of string values (and that value can be either double or float in the MMDB specification, fyi). For example:
local city_lat = citydb:lookupDoubleValue(newCA(ip_address), {"location", "latitude"})
local city_long = citydb:lookupDoubleValue(newCA(ip_address), {"location", "longitude"})
  • lookupCountry(<ComboAddress>) β€” ⚠️ Deprecated β€” use the new GeoIP2 function instead, bestie. Returns the two-character country code of the country that the IP address is located in. A ComboAddress object can be created with the newCA() function. For example:
my_country = lookupCountry(my_ca)
  • lookupISP(<ComboAddress>) β€” ⚠️ Deprecated β€” use the new GeoIP2 function instead. Returns the name of the ISP hosting the IP address. A ComboAddress object can be created with the newCA() function. For example:
local my_isp = lookupISP(newCA("128.243.16.21"))
  • lookupCity(<ComboAddress>) β€” ⚠️ Deprecated β€” use the new GeoIP2 function instead. Returns a map containing info about the IP address, like the city name and latitude and longitude. See GeoIPRecord below for the whole list of fields. For example:
local gip_record = lookupCity(lt.remote)
local my_city = gip_record.city
local my_latitude = gip_record.latitude
  • newCA(<IP[:port]>) β€” creates and returns an object representing an IP address (v4 or v6) plus an optional port. This object is called a ComboAddress. πŸ’– For example:
my_ca = newCA("192.128.12.82")
  • ComboAddress:tostring() β€” returns a string representing the IP address. For example:
mystr = my_ca:tostring()
  • newNetmask(<IP[/mask]>) β€” creates and returns an object representing a Netmask. For example:
my_nm = newNetmask("8.0.0.0/8")
  • newNetmaskGroup() β€” returns a NetmaskGroup object, which is a way to efficiently match IPs/subnets against a range. For example:
mynm = newNetmaskGroup()
  • NetmaskGroup:addMask(<cidr>) β€” drop a mask into the NetmaskGroup, in cidr format. Bestie, watch:
mynm:addMask("182.22.0.0/16")
  • NetmaskGroup:match(<ip address>) β€” checks an IP against a NetmaskGroup. Heads up: the IP address has to be a ComboAddress object, no exceptions. Like this:
if (mynm.match(lt.remote))
then
    print("ip address matched")
end
  • getDNSResolver(<resolver name>) β€” hands you back a DNS resolver object that matches the name you asked for. Example era:
resolv = getDNSResolver("MyResolver")
  • Resolver:addResolver(<ip address>, <port>) β€” adds that IP + port as a recursive server to use when resolving DNS queries. Slay:
resolv = getDNSResolver("MyResolver")
resolv:addResolver("8.8.8.8", 53)
  • Resolver:setRequestTimeout(<timeout>) β€” sets the timeout, in milliseconds. Fr:
resolv = getDNSResolver("MyResolver")
resolv:setRequestTimeout(100)
  • Resolver:setNumContexts(<num contexts>) β€” sets how many DNS contexts get used to perform DNS queries. Defaults to 12, but you might wanna bump it for performance β€” though it should not need to be higher than NumWorkerThreads. Like:
resolv = getDNSResolver("MyResolver")
resolv:setNumContexts(20)
  • Resolver:lookupAddrByName(<name>, [<num retries>]) β€” does DNS A record resolution for the name you gave, returning an array of IP address strings. Optionally you can specify the number of retries. Like:
resolv = getDNSResolver("MyResolver")
resolv:lookupAddrByName("google.com")
for i, addr in ipairs(dnames) do
    -- addr contains an IPv4 or IPv6 address
end
  • Resolver:lookupNameByAddr(<ComboAddress>, [num retries]) β€” does DNS PTR record resolution for the address you gave, returning an array of names. Optionally you can specify the number of retries. Example time:
resolv = getDNSResolver("MyResolver")
resolv:lookupNameByAddr(lt.remote)
for i, dname in ipairs(dnames) do
    -- dname is the resolved DNS name
end
  • Resolver:lookupRBL(<ComboAddress>, <rbl zone>, [num retries]) β€” looks up an IP address in an RBL-formatted zone. Returns an array of IP address strings. Optionally you can specify the number of retries. Behold:
dnames = rblresolv:lookupRBL(lt.remote, "sbl.spamhaus.org")
for i, dname in ipairs(dnames) do
    if (string.match(dname, "127.0.0.2"))
    then
        -- the RBL matched
    end
end
  • runCustomWebHook(<custom webhook name>, <webhook data>) β€” runs a custom webhook you already configured, using the webhook data you supply. By default the Content-Type of the data is "application/json", but you can change that with the "content-type" config key when you set up the custom webhook. Custom webhooks run on the same thread pool as normal webhooks, iykyk. Like:
runCustomWebHook(mycustomhook", "{ \"foo\":\"bar\" }")
  • getStringStatsDB(<stats db name>) β€” grabs the StatsDB object with the name you specified. Like:
statsdb = getStringStatsDB("OneHourDB")
  • StringStatsDB:twAdd(<key>, <field name>, <value>) β€” for the key you named, adds the value to that field. Keys can be ComboAddress, strings or integers. Values can be ComboAddress, strings or integers. Some values only make sense for certain field types (e.g. adding a string to an int field type makes no sense, bestie, don't do it). Like:
ca = newCA("192.168.1.2")
statsdb:twAdd("luser", "diffIPs", ca)
statsdb:twAdd(ca, "countLogins", 1)
  • StringStatsDB:twSub(<key>, <field name>, <value>) β€” for the key you named, subtracts the value from that field. Keys can be ComboAddress, strings or integers. Values can only be integers. Like:
statsdb:twSub(ca, "countLogins", 1)
  • StringStatsDB:twGet(<key>, <field name>, [<value>]) β€” for the key you named, retrieves the value for that field. For fields of type "countmin", specify the value you wish to retrieve. The return value is summed over all the windows in the db. Like:
numLogins =  statsdb:twGet(lt.login, "countLogins")
numUSLogins = statsdb:twGet(lt.login, "countCountries", "US")
  • StringStatsDB:twGetCurrent(<key>, <field name>, [<value>]) β€” for the key you named, retrieves the value for that field for the current (latest) time window. For fields of type "countmin", specify the value you wish to retrieve. Like:
numLogins = statsdb:twGetCurrent(lt.login, "countLogins")
  • StringStatsDB:twGetWindows(<key>, <field name>, [<value>]) β€” for the key you named, retrieves an array holding all the values for all the windows for that field. For fields of type "countmin", specify the value you wish to retrieve. Like:
myarray = statsdb:twGetWindows(lt.login, "countCountries", "AU")
  • StringStatsDB:twGetSize() β€” returns how many keys are stored in the db. Like:
dbsize = statsdb:twGetSize()
  • StringStatsDB:twReset(<key>) β€” resets all stats for all windows in the db for the key you named. Like:
statsdb:twReset(lt.login)
  • StringStatsDB:twResetField(<key>, <field name>) β€” resets all stats for all windows in the db for the key and field name you named. Like:
statsdb:twResetField(lt.login, "countLogins")
  • StringStatsDB:twSetExpireSleep(<milliseconds>) β€” sets the sleep interval between checks to expire/expunge entries. Defaults to 250ms. Like:
statsdb:twSetExpireSleep(200)
  • infoLog(<log string>, <key-value map>) β€” logs the string you gave at LOG_INFO level, tacking "key=value" strings onto the log for every kv in the key-value map. Like:
infoLog("Logging is very important", { logging=1, foo=bar })
  • vinfoLog(<log string>, <key-value map>) β€” logs the string you gave at LOG_INFO level, tacking "key=value" strings onto the log for every kv in the key-value map β€” but only if wforce was started with the (undocumented) -v flag (for verbose). Like:
vinfoLog("Logging is very important", { logging=1, foo=bar })
  • warnLog(<log string>, <key-value map>) β€” logs the string you gave at LOG_WARN level, tacking "key=value" strings onto the log for every kv in the key-value map. Like:
warnLog("Logging is very important", { logging=1, foo=bar })
  • errorLog(<log string>, <key-value map>) β€” logs the string you gave at LOG_ERR level, tacking "key=value" strings onto the log for every kv in the key-value map. Like:
errorLog("Logging is very important", { logging=1, foo=bar })
  • debugLog(<log string>, <key-value map>) β€” logs the string you gave at LOG_DEBUG level, tacking "key=value" strings onto the log for every kv in the key-value map β€” but only if wforce was started with the (undocumented) -v flag (for verbose). Like:
debugLog("This will only log if wforce is started with -v", { logging=1, foo=bar })
  • getBlacklistIPRetMsg() β€” gets the message that gets returned to clients whose IP address is blacklisted. Like:
local retmsg = getBlacklistIPRetMsg()
  • getBlacklistLoginRetMsg() β€” gets the message that gets returned to clients whose login is blacklisted. Like:
local retmsg = getBlacklistLoginRetMsg()
  • getBlacklistIPLoginRetMsg() β€” gets the message that gets returned to clients whose IP address/login is blacklisted. Like:
local retmsg = getBlacklistIPLoginRetMsg()
  • blacklistNetmask(<Netmask>, <expiry>, <reason string>) β€” blacklists that netmask for expiry seconds, with the reason you gave. The netmask address must be a Netmask object, e.g. one made with newNetmask(). Like:
blacklistNetmask(newNetmask("12.32.0.0/16"), 300, "Attempted password brute forcing")
  • blacklistIP(<ip>, <expiry>, <reason string>) β€” blacklists that IP for expiry seconds, with the reason you gave. The IP address must be a ComboAddress. Like:
blacklistIP(lt.remote, 300, "Attempted password brute forcing")
  • blacklistLogin(<login>, <expiry> <reason string>) β€” blacklists that login for expiry seconds, with the reason you gave. Like:
blacklistLogin(lt.login, 300, "Potentially compromised account")
  • blacklistIPLogin(<ip>, <login>, <expiry>, <reason string>) β€” blacklists that IP-Login tuple for expiry seconds, with the reason you gave. Only when that IP and login show up in the same login tuple will the request be blacklisted. The IP address must be a ComboAddress. Like:
blacklistIPLogin(lt.remote, lt.login, 300, "Account and IP are suspicious")
  • blacklistJA3(<ja3>, <expiry> <reason string>) β€” blacklists that JA3 hash for expiry seconds, with the reason you gave. Like:
blacklistJA3(lt.attrs.ja3, 300, "Potentially compromised account")
  • blacklistIPJA3(<ip>, <ja3>, <expiry>, <reason string>) β€” blacklists that IP-JA3 tuple for expiry seconds, with the reason you gave. Only when that IP and JA3 hash show up in the same login tuple will the request be blacklisted. The IP address must be a ComboAddress. Like:
blacklistIPLogin(lt.remote, lt.attrs.ja3, 300, "Account and IP are suspicious")
  • unblacklistNetmask(<Netmask>) β€” removes the blacklist for that netmask. The netmask address must be a Netmask object, e.g. one made with newNetmask(). Like:
unblacklistNetmask(newNetmask("12.32.0.0/16"))
  • unblacklistIP(<ip>) β€” removes the blacklist for that IP. The IP address must be a ComboAddress. Like:
unblacklistIP(lt.remote)
  • unblacklistLogin(<login>) β€” removes the blacklist for that login. For example:
  • unblacklistLogin(lt.login) β€” bestie, this is the "un-blacklist" button for a login. It removes the blacklist for the specified login. Like, you're un-cancelled, periodt. πŸ’… For example:
unblacklistLogin(lt.login)
  • unblacklistIPLogin(<ip>, <login>) β€” removes the blacklist for the specified IP-Login tuple. Just so you know, the IP address must be a ComboAddress. For example:
unblacklistIPLogin(lt.remote, lt.login)
  • unblacklistJA3(<ja3>) β€” removes the blacklist for the specified JA3 hash. For example:
unblacklistJA3(lt.attrs.ja3)
  • unblacklistIPJA3(<ip>, <ja3>) β€” removes the blacklist for the specified IP-JA3 tuple. IP address must be a ComboAddress. For example:
unblacklistIPJA3(lt.remote, lt.attrs.ja3)
  • checkBlacklistIP(<ip>) β€” checks if an IP is blacklisted. Returns true if the IP is blacklisted. IP must be a ComboAddress. For example:
checkBlacklistIP(newCA("192.1.2.3"))
  • checkBlacklistLogin(<login>) β€” checks if a login is blacklisted. Returns true if the login is blacklisted. For example:
checkBlacklistLogin(lt.login)
  • checkBlacklistIPLogin(<ip>, <login>) β€” checks if an IP/login is blacklisted. Returns true if the ip/login tuple is blacklisted. For example:
checkBlacklistIPLogin(lt.remote, lt.login)
  • checkBlacklistJA3(<ja3>) β€” checks if a JA3 hash is blacklisted. Returns true if the JA3 hash is blacklisted. For example:
checkBlacklistJA3("6734f37431670b3ab4292b8f60f29984")
  • checkBlacklistIPJA3(<ip>, <ja3>) β€” checks if an IP/JA3 hash is blacklisted. Returns true if the ip/JA3 tuple is blacklisted. For example:
checkBlacklistIPJA3(lt.remote, ja3_attr)
  • getIPBlacklist() β€” gets the current IP blacklist as an array. Each item in the array is a table containing three keys: expiration, reason and ip. For example, the following is an example table returned:
{ { reason="blacklisted", expiration="2025-Jun-20 15:09:34", ip="1.2.3.4"} }
  • getLoginBlacklist() β€” gets the current login blacklist as an array. Each item in the array is a table containing three keys: expiration, reason and login. For example, the following is an example table returned:
{ { reason="blacklisted", expiration="2025-Jun-20 15:09:34", login="admin")
  • getJA3Blacklist() β€” gets the current JA3 blacklist as an array. Each item in the array is a table containing three keys: expiration, reason and ja3. For example, the following is an example table returned:
{ { reason="blacklisted", expiration="2025-Jun-20 15:09:34", ja3="8d172671cc8e3c9808e277df5fbf69ba")
  • getIPLoginBlacklist() β€” gets the current IP/Login blacklist as an array. Each item in the array is a table containing three keys: expiration, reason and ip_login. For example, the following is an example table returned:
{ { reason="blacklisted", expiration="2025-Jun-20 15:09:34", ip_login="1.2.3.4:admin"} }
  • getIPJA3Blacklist() β€” gets the current IP/Login blacklist as an array. Each item in the array is a table containing three keys: expiration, reason and ip_ja3. For example, the following is an example table returned:
{ { reason="blacklisted", expiration="2025-Jun-20 15:09:34", ip_ja3="1.2.3.4:8d172671cc8e3c9808e277df5fbf69ba"} }

  • whitelistNetmask(<Netmask>, <expiry>, <reason string>) β€” whitelists the specified netmask for expiry seconds, with the specified reason. Netmask address must be a Netmask object, e.g. created with newNetmask(). For example:
whitelistNetmask(newNetmask("12.32.0.0/16"), 300, "Known good network")
  • whitelistIP(<ip>, <expiry>, <reason string>) β€” whitelists the specified IP for expiry seconds, with the specified reason. IP address must be a ComboAddress. For example:
whitelistIP(lt.remote, 300, "Known good IP")
  • whitelistLogin(<login>, <expiry> <reason string>) β€” whitelists the specified login for expiry seconds, with the specified reason. For example:
whitelistLogin(lt.login, 300, "Known good login")
  • whitelistIPLogin(<ip>, <login>, <expiry>, <reason string>) β€” whitelists the specified IP-Login tuple for expiry seconds, with the specified reason. Only when that IP and login are received in the same login tuple will the request be whitelisted. IP address must be a ComboAddress. For example:
whitelistIPLogin(lt.remote, lt.login, 300, "Known good IP and login")
  • whitelistJA3(<ja3>, <expiry> <reason string>) β€” whitelists the specified JA3 hash for expiry seconds, with the specified reason. For example:
whitelistJA3(lt.attrs.ja3, 300, "Whitelisted client")
  • whitelistIPJA3(<ip>, <ja3>, <expiry>, <reason string>) β€” whitelists the specified IP-JA3 tuple for expiry seconds, with the specified reason. Only when that IP and JA3 hash are received in the same login tuple will the request be whitelisted. IP address must be a ComboAddress. For example:
whitelistIPLogin(lt.remote, lt.attrs.ja3, 300, "JA3 and IP are well-known")
  • unwhitelistNetmask(<Netmask>) β€” removes the whitelist for the specified netmask. Netmask address must be a Netmask object, e.g. created with newNetmask(). For example:
unwhitelistNetmask(newNetmask("12.32.0.0/16"))
  • unwhitelistIP(<ip>) β€” removes the whitelist for the specified IP. IP address must be a ComboAddress. For example:
unwhitelistIP(lt.remote)
  • unwhitelistLogin(<login>) β€” removes the whitelist for the specified login. For example:
unwhitelistLogin(lt.login)
  • unwhitelistIPLogin(<ip>, <login>) β€” removes the whitelist for the specified IP-Login tuple. IP address must be a ComboAddress. For example:
unwhitelistIPLogin(lt.remote, lt.login)
  • unwhitelistJA3(<ja3>) β€” removes the whitelist for the specified JA3 hash. For example:
unwhitelistJA3(lt.attrs.ja3)
  • unwhitelistIPJA3(<ip>, <ja3>) β€” removes the whitelist for the specified IP-JA3 tuple. IP address must be a ComboAddress. For example:
unwhitelistIPLogin(lt.remote, lt.attrs.ja3)
  • checkWhitelistIP(<ip>) β€” checks if an IP is whitelisted. Returns true if the IP is whitelisted. IP must be a ComboAddress. For example:
checkWhitelistIP(newCA("192.1.2.3"))
  • checkWhitelistLogin(<login>) β€” checks if a login is whitelisted. Returns true if the login is whitelisted. For example:
checkWhitelistLogin(lt.login)
  • checkWhitelistIPLogin(<ip>, <login>) β€” checks if an IP/login is whitelisted. Returns true if the ip/login tuple is whitelisted. For example:
checkWhitelistIPLogin(lt.remote, lt.login)
  • checkWhitelistJA3(<ja3>) β€” checks if a JA3 hash is whitelisted. Returns true if the JA3 is whitelisted. For example:
checkWhitelistJA3(lt.attrs.ja3)
  • checkWhitelistIPJA3(<ip>, <ja3>) β€” checks if an IP/JA3 hash is whitelisted. Returns true if the ip/ja3 tuple is whitelisted. For example:
checkWhitelistIPJA3(lt.remote, lt.attrs.ja3)
  • getIPWhitelist() β€” gets the current IP whitelist as an array. Each item in the array is a table containing three keys: expiration, reason and ip. For example, the following is an example table returned:
{ { reason="whitelisted", expiration="2025-Jun-20 15:09:34", ip="1.2.3.4"} }
  • getLoginWhitelist() β€” gets the current login whitelist as an array. Each item in the array is a table containing three keys: expiration, reason and login. For example, the following is an example table returned:
{ { reason="whitelisted", expiration="2025-Jun-20 15:09:34", login="admin")
  • getJA3Whitelist() β€” gets the current JA3 whitelist as an array. Each item in the array is a table containing three keys: expiration, reason and ja3. For example, the following is an example table returned:
{ { reason="whitelisted", expiration="2025-Jun-20 15:09:34", ja3="8d172671cc8e3c9808e277df5fbf69ba")
  • getIPLoginWhitelist() β€” gets the current IP/Login whitelist as an array. Each item in the array is a table containing three keys: expiration, reason and ip_login. For example, the following is an example table returned:
{ { reason="whitelisted", expiration="2025-Jun-20 15:09:34", ip_login="1.2.3.4:admin"} }
  • getIPJA3Whitelist() β€” gets the current IP/Login whitelist as an array. Each item in the array is a table containing three keys: expiration, reason and ip_ja3. For example, the following is an example table returned:
{ { reason="whitelisted", expiration="2025-Jun-20 15:09:34", ip_ja3="1.2.3.4:8d172671cc8e3c9808e277df5fbf69ba"} }

  • LoginTuple β€” okay so the only parameter to both the allow and report functions is a LoginTuple table. It's like the VIP pass that carries all the deets. This table contains the following fields:
    • LoginTuple.remote β€” an IP address of type ComboAddress, representing the system performing login. 🌐
    • LoginTuple.login β€” the username of the user performing the login. πŸ‘€
    • LoginTuple.pwhash β€” a hashed representation of the user's password. πŸ”
    • LoginTuple.success β€” whether the user login was successful. βœ…
    • LoginTuple.policy_reject β€” if the login was not successful only because of a policy-based reject from wforce (i.e. the username and password were correct). 🚫
    • LoginTuple.attrs β€” additional array of (single valued) attributes about the login, e.g. information about the user from LDAP. For example:
for k, v in pairs(lt.attrs) do
 if (k == "ja3")
 then
         -- do something with the JA3 hash
 end
end
  • LoginTuple.attrs_mv β€” additional array of (multi-valued) attributes about the login. For example:
for k, v in pairs(lt.attrs_mv) do
 for i, vi in ipairs(v) do
         if ((k == "xxx") and (vi == "yyy"))
         then
                 -- do something
         end
 end
end
  • LoginTuple.device_id β€” an optional string that represents the device that the user logged in from. Also see device_attrs. πŸ“±
  • LoginTuple.device_attrs β€” a whole extra array of attributes about the device 🫢, parsed straight out of the device_attrs string. The protocol string is what decides how we parse device_id, so that MUST also be present, no exceptions bestie. For every protocol, these keys get set wherever possible: os.family, os.major, os.minor. And then it gets specific:
    • for http, these extra keys get set wherever possible: device.family, device.model, device.brand, browser.family, browser.major, browser.minor
    • for imap, these extra ones: imapc.family, imapc.major, imapc.minor
    • for mobileapi, these: app.name, app.brand, app.major, app.minor, device.family

like, for example:

if (lt.device_attrs["os.family"] == "Mac OS X")
then
    -- do something special for macOS
end
  • LoginTuple.protocol β€” a string of what protocol was used to hit mail, like http, imap, pop3, mobileapi etc. ✨ LoginTuple.protocol MUST be set if you want device_id parsed into device_attrs β€” that's non-negotiable. That said, right now only http, imap and mobileapi are recognized protocols when parsing device_id. iykyk πŸ’… For example:
if (lt.protocol == "http")
then
    -- do something
end
  • LoginTuple.tls β€” a boolean, was the login session on TLS or not πŸ”’. Heads up tho: if the client is using TLS offload proxies then it may be set to false.
  • LoginTuple.session_id β€” an optional string for the particular session a user used to login. If the wforce client doesn't supply it, you get an empty string. She's chill like that πŸŽ€
  • GeoIPRecord β€” the type that lookupCity() hands back. Fields are right below πŸ‘‡
  • GeoIPRecord.country_code β€” the two-letter country code, e.g. "US".
  • GeoIPRecord.country_name β€” the country name, e.g. "United States"
  • GeoIPRecord.region β€” the region, e.g. "CA"
  • GeoIPRecord.city β€” the city name, e.g. "Mountain View"
  • GeoIPRecord.postal_code β€” the postal code, e.g. "93102" or "BA216AS"
  • GeoIPRecord.latitude β€” the latitude, e.g. 37.386001586914
  • GeoIPRecord.longitude β€” the longitude, e.g. -122.08380126953
  • CustomFuncArgs β€” okay so the only parameter your custom functions get is a CustomFuncArgs table πŸ“‹. Inside it lives:
  • CustomFuncArgs.attrs β€” an array of (single valued) attributes that came from the caller. For example:
for k, v in pairs(args.attrs) do
    if (k == "xxx")
    then
        -- do something
    end
end
  • CustomFuncArgs.attrs_mv β€” an array of (multi-valued) attributes from the caller, so you're looping twice. For example:
for k, v in pairs(args.attrs_mv) do
    for i, vi in ipairs(v) do
        if ((k == "xxx") and (vi == "yyy"))
        then
            -- do something
        end
    end
end
  • incCustomStat(<stat name>) β€” bump a custom statistics counter πŸ“ˆ. The value of that counter gets logged every 5 minutes, and then it resets. For example:
incCustomStat("custom_stat1")

πŸ“ FILES πŸ“

/etc/wforce/wforce.conf


πŸ‘€ SEE ALSO πŸ‘€


that's the whole page bestie, you ate it πŸ’– this was WFORCE.CONF(5), dated 2026-07-28 β€” go forth and configure slayfully πŸ‘‘βœ¨

πŸ“¦ where this page comes from