summaryrefslogtreecommitdiffstats
path: root/src/lua/lua_spf.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/lua/lua_spf.c')
-rw-r--r--src/lua/lua_spf.c620
1 files changed, 620 insertions, 0 deletions
diff --git a/src/lua/lua_spf.c b/src/lua/lua_spf.c
new file mode 100644
index 0000000..a67a267
--- /dev/null
+++ b/src/lua/lua_spf.c
@@ -0,0 +1,620 @@
+/*-
+ * Copyright 2019 Vsevolod Stakhov
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @file lua_spf.c
+ * This module exports spf functions to Lua
+ */
+
+#include "lua_common.h"
+#include "libserver/spf.h"
+#include "libutil/ref.h"
+
+#define SPF_RECORD_CLASS "rspamd{spf_record}"
+
+LUA_FUNCTION_DEF(spf, resolve);
+LUA_FUNCTION_DEF(spf, config);
+
+LUA_FUNCTION_DEF(spf_record, check_ip);
+LUA_FUNCTION_DEF(spf_record, dtor);
+LUA_FUNCTION_DEF(spf_record, get_domain);
+LUA_FUNCTION_DEF(spf_record, get_elts);
+LUA_FUNCTION_DEF(spf_record, get_ttl);
+LUA_FUNCTION_DEF(spf_record, get_timestamp);
+LUA_FUNCTION_DEF(spf_record, get_digest);
+
+static luaL_reg rspamd_spf_f[] = {
+ LUA_INTERFACE_DEF(spf, resolve),
+ LUA_INTERFACE_DEF(spf, config),
+ {NULL, NULL},
+};
+
+static luaL_reg rspamd_spf_record_m[] = {
+ LUA_INTERFACE_DEF(spf_record, check_ip),
+ LUA_INTERFACE_DEF(spf_record, get_domain),
+ LUA_INTERFACE_DEF(spf_record, get_ttl),
+ LUA_INTERFACE_DEF(spf_record, get_digest),
+ LUA_INTERFACE_DEF(spf_record, get_elts),
+ LUA_INTERFACE_DEF(spf_record, get_timestamp),
+ {"__gc", lua_spf_record_dtor},
+ {NULL, NULL},
+};
+
+struct rspamd_lua_spf_cbdata {
+ struct rspamd_task *task;
+ lua_State *L;
+ struct rspamd_symcache_dynamic_item *item;
+ gint cbref;
+ ref_entry_t ref;
+};
+
+static gint
+lua_load_spf(lua_State *L)
+{
+ lua_newtable(L);
+
+ /* Create integer arguments to check SPF results */
+ lua_newtable(L);
+ lua_pushinteger(L, SPF_FAIL);
+ lua_setfield(L, -2, "fail");
+ lua_pushinteger(L, SPF_PASS);
+ lua_setfield(L, -2, "pass");
+ lua_pushinteger(L, SPF_NEUTRAL);
+ lua_setfield(L, -2, "neutral");
+ lua_pushinteger(L, SPF_SOFT_FAIL);
+ lua_setfield(L, -2, "soft_fail");
+
+ lua_setfield(L, -2, "policy");
+
+ /* Flags stuff */
+ lua_newtable(L);
+
+ lua_pushinteger(L, RSPAMD_SPF_RESOLVED_TEMP_FAILED);
+ lua_setfield(L, -2, "temp_fail");
+ lua_pushinteger(L, RSPAMD_SPF_RESOLVED_NA);
+ lua_setfield(L, -2, "na");
+ lua_pushinteger(L, RSPAMD_SPF_RESOLVED_PERM_FAILED);
+ lua_setfield(L, -2, "perm_fail");
+ lua_pushinteger(L, RSPAMD_SPF_FLAG_CACHED);
+ lua_setfield(L, -2, "cached");
+
+ lua_setfield(L, -2, "flags");
+
+ luaL_register(L, NULL, rspamd_spf_f);
+
+ return 1;
+}
+
+void luaopen_spf(lua_State *L)
+{
+ rspamd_lua_new_class(L, SPF_RECORD_CLASS, rspamd_spf_record_m);
+ lua_pop(L, 1); /* No need in metatable... */
+
+ rspamd_lua_add_preload(L, "rspamd_spf", lua_load_spf);
+ lua_settop(L, 0);
+}
+
+static void
+lua_spf_push_result(struct rspamd_lua_spf_cbdata *cbd, gint code_flags,
+ struct spf_resolved *resolved, const gchar *err)
+{
+ g_assert(cbd != NULL);
+ REF_RETAIN(cbd);
+
+ lua_pushcfunction(cbd->L, &rspamd_lua_traceback);
+ gint err_idx = lua_gettop(cbd->L);
+
+ lua_rawgeti(cbd->L, LUA_REGISTRYINDEX, cbd->cbref);
+
+ if (resolved) {
+ struct spf_resolved **presolved;
+
+ presolved = lua_newuserdata(cbd->L, sizeof(*presolved));
+ rspamd_lua_setclass(cbd->L, SPF_RECORD_CLASS, -1);
+ *presolved = spf_record_ref(resolved);
+ }
+ else {
+ lua_pushnil(cbd->L);
+ }
+
+ lua_pushinteger(cbd->L, code_flags);
+
+ if (err) {
+ lua_pushstring(cbd->L, err);
+ }
+ else {
+ lua_pushnil(cbd->L);
+ }
+
+ if (lua_pcall(cbd->L, 3, 0, err_idx) != 0) {
+ struct rspamd_task *task = cbd->task;
+
+ msg_err_task("cannot call callback function for spf: %s",
+ lua_tostring(cbd->L, -1));
+ }
+
+ lua_settop(cbd->L, err_idx - 1);
+
+ REF_RELEASE(cbd);
+}
+
+static void
+lua_spf_dtor(struct rspamd_lua_spf_cbdata *cbd)
+{
+ if (cbd) {
+ luaL_unref(cbd->L, LUA_REGISTRYINDEX, cbd->cbref);
+ if (cbd->item) {
+ rspamd_symcache_item_async_dec_check(cbd->task, cbd->item,
+ "lua_spf");
+ }
+ }
+}
+
+static void
+spf_lua_lib_callback(struct spf_resolved *record, struct rspamd_task *task,
+ gpointer ud)
+{
+ struct rspamd_lua_spf_cbdata *cbd = (struct rspamd_lua_spf_cbdata *) ud;
+
+ if (record) {
+ if ((record->flags & RSPAMD_SPF_RESOLVED_NA)) {
+ lua_spf_push_result(cbd, RSPAMD_SPF_RESOLVED_NA, NULL,
+ "no SPF record");
+ }
+ else if (record->elts->len == 0) {
+ if (record->flags & RSPAMD_SPF_RESOLVED_PERM_FAILED) {
+ lua_spf_push_result(cbd, RSPAMD_SPF_RESOLVED_PERM_FAILED, NULL,
+ "bad SPF record");
+ }
+ else if ((record->flags & RSPAMD_SPF_RESOLVED_TEMP_FAILED)) {
+ lua_spf_push_result(cbd, RSPAMD_SPF_RESOLVED_TEMP_FAILED, NULL,
+ "temporary DNS error");
+ }
+ else {
+ lua_spf_push_result(cbd, RSPAMD_SPF_RESOLVED_PERM_FAILED, NULL,
+ "empty SPF record");
+ }
+ }
+ else if (record->domain) {
+ spf_record_ref(record);
+ lua_spf_push_result(cbd, record->flags, record, NULL);
+ spf_record_unref(record);
+ }
+ else {
+ lua_spf_push_result(cbd, RSPAMD_SPF_RESOLVED_PERM_FAILED, NULL,
+ "internal error: non empty record for no domain");
+ }
+ }
+ else {
+ lua_spf_push_result(cbd, RSPAMD_SPF_RESOLVED_PERM_FAILED, NULL,
+ "internal error: no record");
+ }
+
+ REF_RELEASE(cbd);
+}
+
+/***
+ * @function rspamd_spf.resolve(task, callback)
+ * Resolves SPF credentials for a task
+ * @param {rspamd_task} task task
+ * @param {function} callback callback that is called on spf resolution
+*/
+gint lua_spf_resolve(lua_State *L)
+{
+ struct rspamd_task *task = lua_check_task(L, 1);
+
+ if (task && lua_isfunction(L, 2)) {
+ struct rspamd_lua_spf_cbdata *cbd = rspamd_mempool_alloc0(task->task_pool,
+ sizeof(*cbd));
+ struct rspamd_spf_cred *spf_cred;
+
+ cbd->task = task;
+ cbd->L = L;
+ lua_pushvalue(L, 2);
+ cbd->cbref = luaL_ref(L, LUA_REGISTRYINDEX);
+ /* TODO: make it as an optional parameter */
+ spf_cred = rspamd_spf_get_cred(task);
+ cbd->item = rspamd_symcache_get_cur_item(task);
+
+ if (cbd->item) {
+ rspamd_symcache_item_async_inc(task, cbd->item, "lua_spf");
+ }
+ REF_INIT_RETAIN(cbd, lua_spf_dtor);
+
+ if (!rspamd_spf_resolve(task, spf_lua_lib_callback, cbd, spf_cred)) {
+ msg_info_task("cannot make spf request for %s",
+ spf_cred ? spf_cred->domain : "empty domain");
+ if (spf_cred) {
+ lua_spf_push_result(cbd, RSPAMD_SPF_RESOLVED_TEMP_FAILED,
+ NULL, "DNS failed");
+ }
+ else {
+ lua_spf_push_result(cbd, RSPAMD_SPF_RESOLVED_NA,
+ NULL, "No domain");
+ }
+ REF_RELEASE(cbd);
+ }
+ }
+ else {
+ return luaL_error(L, "invalid arguments");
+ }
+
+ return 0;
+}
+
+static gint
+lua_spf_record_dtor(lua_State *L)
+{
+ struct spf_resolved *record;
+
+ RSPAMD_LUA_CHECK_UDATA_PTR_OR_RETURN(L, 1, SPF_RECORD_CLASS,
+ struct spf_resolved,
+ record);
+
+ if (record) {
+ spf_record_unref(record);
+ }
+
+ return 0;
+}
+
+static void
+lua_spf_push_spf_addr(lua_State *L, struct spf_addr *addr)
+{
+ gchar *addr_mask;
+
+ lua_createtable(L, 0, 4);
+
+ lua_pushinteger(L, addr->mech);
+ lua_setfield(L, -2, "result");
+ lua_pushinteger(L, addr->flags);
+ lua_setfield(L, -2, "flags");
+
+ if (addr->spf_string) {
+ lua_pushstring(L, addr->spf_string);
+ lua_setfield(L, -2, "str");
+ }
+
+ addr_mask = spf_addr_mask_to_string(addr);
+
+ if (addr_mask) {
+ lua_pushstring(L, addr_mask);
+ lua_setfield(L, -2, "addr");
+ g_free(addr_mask);
+ }
+}
+
+static gint
+spf_check_element(lua_State *L, struct spf_resolved *rec, struct spf_addr *addr,
+ struct rspamd_lua_ip *ip)
+{
+ gboolean res = FALSE;
+ const guint8 *s, *d;
+ guint af, mask, bmask, addrlen;
+
+
+ if (addr->flags & RSPAMD_SPF_FLAG_TEMPFAIL) {
+ /* Ignore failed addresses */
+
+ return -1;
+ }
+
+ af = rspamd_inet_address_get_af(ip->addr);
+ /* Basic comparing algorithm */
+ if (((addr->flags & RSPAMD_SPF_FLAG_IPV6) && af == AF_INET6) ||
+ ((addr->flags & RSPAMD_SPF_FLAG_IPV4) && af == AF_INET)) {
+ d = rspamd_inet_address_get_hash_key(ip->addr, &addrlen);
+
+ if (af == AF_INET6) {
+ s = (const guint8 *) addr->addr6;
+ mask = addr->m.dual.mask_v6;
+ }
+ else {
+ s = (const guint8 *) addr->addr4;
+ mask = addr->m.dual.mask_v4;
+ }
+
+ /* Compare the first bytes */
+ bmask = mask / CHAR_BIT;
+ if (mask > addrlen * CHAR_BIT) {
+ /* XXX: add logging */
+ }
+ else if (memcmp(s, d, bmask) == 0) {
+ if (bmask * CHAR_BIT < mask) {
+ /* Compare the remaining bits */
+ s += bmask;
+ d += bmask;
+ mask = (0xff << (CHAR_BIT - (mask - bmask * 8))) & 0xff;
+
+ if ((*s & mask) == (*d & mask)) {
+ res = TRUE;
+ }
+ }
+ else {
+ res = TRUE;
+ }
+ }
+ }
+ else {
+ if (addr->flags & RSPAMD_SPF_FLAG_ANY) {
+ res = TRUE;
+ }
+ else {
+ res = FALSE;
+ }
+ }
+
+ if (res) {
+ if (addr->flags & RSPAMD_SPF_FLAG_ANY) {
+ if (rec->flags & RSPAMD_SPF_RESOLVED_PERM_FAILED) {
+ lua_pushboolean(L, false);
+ lua_pushinteger(L, RSPAMD_SPF_RESOLVED_PERM_FAILED);
+ lua_pushfstring(L, "%cany", spf_mech_char(addr->mech));
+ }
+ else if (rec->flags & RSPAMD_SPF_RESOLVED_TEMP_FAILED) {
+ lua_pushboolean(L, false);
+ lua_pushinteger(L, RSPAMD_SPF_RESOLVED_TEMP_FAILED);
+ lua_pushfstring(L, "%cany", spf_mech_char(addr->mech));
+ }
+ else {
+ lua_pushboolean(L, true);
+ lua_pushinteger(L, addr->mech);
+ lua_spf_push_spf_addr(L, addr);
+ }
+ }
+ else {
+ lua_pushboolean(L, true);
+ lua_pushinteger(L, addr->mech);
+ lua_spf_push_spf_addr(L, addr);
+ }
+
+ return 3;
+ }
+
+ return -1;
+}
+
+/***
+ * @method rspamd_spf_record:check_ip(ip)
+ * Checks the processed record versus a specific IP address. This function
+ * returns 3 values normally:
+ * 1. Boolean check result
+ * 2. If result is `false` then the second value is the error flag (e.g. rspamd_spf.flags.temp_fail), otherwise it will be an SPF method
+ * 3. If result is `false` then this will be an error string, otherwise - an SPF string (e.g. `mx` or `ip4:x.y.z.1`)
+ * @param {rspamd_ip|string} ip address
+ * @return {result,flag_or_policy,error_or_addr} - triplet
+*/
+static gint
+lua_spf_record_check_ip(lua_State *L)
+{
+ struct spf_resolved *record;
+ RSPAMD_LUA_CHECK_UDATA_PTR_OR_RETURN(L, 1, SPF_RECORD_CLASS,
+ struct spf_resolved,
+ record);
+ struct rspamd_lua_ip *ip = NULL;
+ gint nres = 0;
+ gboolean need_free_ip = FALSE;
+
+ if (lua_type(L, 2) == LUA_TUSERDATA) {
+ ip = lua_check_ip(L, 2);
+ }
+ else if (lua_type(L, 2) == LUA_TSTRING) {
+ const gchar *ip_str;
+ gsize iplen;
+
+ ip = g_malloc0(sizeof(struct rspamd_lua_ip));
+ ip_str = lua_tolstring(L, 2, &iplen);
+
+ if (!rspamd_parse_inet_address(&ip->addr,
+ ip_str, iplen, RSPAMD_INET_ADDRESS_PARSE_DEFAULT)) {
+ g_free(ip);
+ ip = NULL;
+ }
+ else {
+ need_free_ip = TRUE;
+ }
+ }
+
+ if (record && ip && ip->addr) {
+ for (guint i = 0; i < record->elts->len; i++) {
+ struct spf_addr *addr = &g_array_index(record->elts, struct spf_addr, i);
+ if ((nres = spf_check_element(L, record, addr, ip)) > 0) {
+ if (need_free_ip) {
+ g_free(ip);
+ }
+
+ return nres;
+ }
+ }
+ }
+ else {
+ if (need_free_ip) {
+ g_free(ip);
+ }
+
+ return luaL_error(L, "invalid arguments");
+ }
+
+ if (need_free_ip) {
+ g_free(ip);
+ }
+
+ /* If we are here it means that there is no ALL record */
+ /*
+ * According to https://tools.ietf.org/html/rfc7208#section-4.7 it means
+ * SPF neutral
+ */
+ struct spf_addr fake_all;
+
+ fake_all.mech = SPF_NEUTRAL;
+ fake_all.flags = RSPAMD_SPF_FLAG_ANY;
+ fake_all.spf_string = "all";
+
+ lua_pushboolean(L, true);
+ lua_pushinteger(L, SPF_NEUTRAL);
+ lua_spf_push_spf_addr(L, &fake_all);
+
+ return 3;
+}
+
+/***
+ * @method rspamd_spf_record:get_domain()
+ * Returns domain for the specific spf record
+*/
+static gint
+lua_spf_record_get_domain(lua_State *L)
+{
+ struct spf_resolved *record;
+ RSPAMD_LUA_CHECK_UDATA_PTR_OR_RETURN(L, 1, SPF_RECORD_CLASS,
+ struct spf_resolved,
+ record);
+
+ if (record) {
+ lua_pushstring(L, record->domain);
+ }
+ else {
+ return luaL_error(L, "invalid arguments");
+ }
+
+ return 1;
+}
+
+/***
+ * @method rspamd_spf_record:get_ttl()
+ * Returns ttl for the specific spf record
+*/
+static gint
+lua_spf_record_get_ttl(lua_State *L)
+{
+ struct spf_resolved *record;
+ RSPAMD_LUA_CHECK_UDATA_PTR_OR_RETURN(L, 1, SPF_RECORD_CLASS,
+ struct spf_resolved,
+ record);
+
+ if (record) {
+ lua_pushinteger(L, record->ttl);
+ }
+ else {
+ return luaL_error(L, "invalid arguments");
+ }
+
+ return 1;
+}
+
+/***
+ * @method rspamd_spf_record:get_timestamp()
+ * Returns ttl for the specific spf record
+*/
+static gint
+lua_spf_record_get_timestamp(lua_State *L)
+{
+ struct spf_resolved *record;
+ RSPAMD_LUA_CHECK_UDATA_PTR_OR_RETURN(L, 1, SPF_RECORD_CLASS,
+ struct spf_resolved,
+ record);
+
+ if (record) {
+ lua_pushnumber(L, record->timestamp);
+ }
+ else {
+ return luaL_error(L, "invalid arguments");
+ }
+
+ return 1;
+}
+
+/***
+ * @method rspamd_spf_record:get_digest()
+ * Returns string hex representation of the record digest (fast hash function)
+*/
+static gint
+lua_spf_record_get_digest(lua_State *L)
+{
+ struct spf_resolved *record;
+ RSPAMD_LUA_CHECK_UDATA_PTR_OR_RETURN(L, 1, SPF_RECORD_CLASS,
+ struct spf_resolved,
+ record);
+
+ if (record) {
+ gchar hexbuf[64];
+
+ rspamd_snprintf(hexbuf, sizeof(hexbuf), "%xuL", record->digest);
+ lua_pushstring(L, hexbuf);
+ }
+ else {
+ return luaL_error(L, "invalid arguments");
+ }
+
+ return 1;
+}
+
+/***
+ * @method rspamd_spf_record:get_elts()
+ * Returns a list of all elements in an SPF record. Each element is a table with the
+ * following fields:
+ *
+ * - result - mech flag from rspamd_spf.results
+ * - flags - all flags
+ * - addr - address and mask as a string
+ * - str - string representation (if available)
+*/
+static gint
+lua_spf_record_get_elts(lua_State *L)
+{
+ struct spf_resolved *record;
+ RSPAMD_LUA_CHECK_UDATA_PTR_OR_RETURN(L, 1, SPF_RECORD_CLASS,
+ struct spf_resolved,
+ record);
+
+ if (record) {
+ guint i;
+ struct spf_addr *addr;
+
+ lua_createtable(L, record->elts->len, 0);
+
+ for (i = 0; i < record->elts->len; i++) {
+ addr = (struct spf_addr *) &g_array_index(record->elts,
+ struct spf_addr, i);
+ lua_spf_push_spf_addr(L, addr);
+
+ lua_rawseti(L, -2, i + 1);
+ }
+ }
+ else {
+ return luaL_error(L, "invalid arguments");
+ }
+
+ return 1;
+}
+
+/***
+ * @function rspamd_spf.config(object)
+ * Configures SPF library according to the UCL config
+ * @param {table} object configuration object
+*/
+gint lua_spf_config(lua_State *L)
+{
+ ucl_object_t *config_obj = ucl_object_lua_import(L, 1);
+
+ if (config_obj) {
+ spf_library_config(config_obj);
+ ucl_object_unref(config_obj); /* As we copy data all the time */
+ }
+ else {
+ return luaL_error(L, "invalid arguments");
+ }
+
+ return 0;
+} \ No newline at end of file