You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

lua_maps.lua 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. --[[[
  2. -- @module lua_maps
  3. -- This module contains helper functions for managing rspamd maps
  4. --]]
  5. --[[
  6. Copyright (c) 2022, Vsevolod Stakhov <vsevolod@rspamd.com>
  7. Licensed under the Apache License, Version 2.0 (the "License");
  8. you may not use this file except in compliance with the License.
  9. You may obtain a copy of the License at
  10. http://www.apache.org/licenses/LICENSE-2.0
  11. Unless required by applicable law or agreed to in writing, software
  12. distributed under the License is distributed on an "AS IS" BASIS,
  13. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. See the License for the specific language governing permissions and
  15. limitations under the License.
  16. ]]--
  17. local rspamd_logger = require "rspamd_logger"
  18. local ts = require("tableshape").types
  19. local lua_util = require "lua_util"
  20. local exports = {}
  21. local maps_cache = {}
  22. local function map_hash_key(data, mtype)
  23. local hash = require "rspamd_cryptobox_hash"
  24. local st = hash.create_specific('xxh64')
  25. st:update(data)
  26. st:update(mtype)
  27. return st:hex()
  28. end
  29. local function starts(where,st)
  30. return string.sub(where,1,string.len(st))==st
  31. end
  32. local function cut_prefix(where,st)
  33. return string.sub(where,#st + 1)
  34. end
  35. local function maybe_adjust_type(data,mtype)
  36. local function check_prefix(prefix, t)
  37. if starts(data, prefix) then
  38. data = cut_prefix(data, prefix)
  39. mtype = t
  40. return true
  41. end
  42. return false
  43. end
  44. local known_types = {
  45. {'regexp;', 'regexp'},
  46. {'re;', 'regexp'},
  47. {'regexp_multi;', 'regexp_multi'},
  48. {'re_multi;', 'regexp_multi'},
  49. {'glob;', 'glob'},
  50. {'glob_multi;', 'glob_multi'},
  51. {'radix;', 'radix'},
  52. {'ipnet;', 'radix'},
  53. {'set;', 'set'},
  54. {'hash;', 'hash'},
  55. {'plain;', 'hash'},
  56. {'cdb;', 'cdb'},
  57. {'cdb:/', 'cdb'},
  58. }
  59. if mtype == 'callback' then
  60. return mtype
  61. end
  62. for _,t in ipairs(known_types) do
  63. if check_prefix(t[1], t[2]) then
  64. return data,mtype
  65. end
  66. end
  67. -- No change
  68. return data,mtype
  69. end
  70. --[[[
  71. -- @function lua_maps.map_add_from_ucl(opt, mtype, description)
  72. -- Creates a map from static data
  73. -- Returns true if map was added or nil
  74. -- @param {string or table} opt data for map (or URL)
  75. -- @param {string} mtype type of map (`set`, `map`, `radix`, `regexp`)
  76. -- @param {string} description human-readable description of map
  77. -- @return {bool} true on success, or `nil`
  78. --]]
  79. local function rspamd_map_add_from_ucl(opt, mtype, description)
  80. local ret = {
  81. get_key = function(t, k)
  82. if t.__data then
  83. return t.__data:get_key(k)
  84. end
  85. return nil
  86. end
  87. }
  88. local ret_mt = {
  89. __index = function(t, k)
  90. if t.__data then
  91. return t.get_key(k)
  92. end
  93. return nil
  94. end
  95. }
  96. if not opt then
  97. return nil
  98. end
  99. if type(opt) == 'string' then
  100. opt,mtype = maybe_adjust_type(opt, mtype)
  101. local cache_key = map_hash_key(opt, mtype)
  102. if maps_cache[cache_key] then
  103. rspamd_logger.infox(rspamd_config, 'reuse url for %s(%s)',
  104. opt, mtype)
  105. return maps_cache[cache_key]
  106. end
  107. -- We have a single string, so we treat it as a map
  108. local map = rspamd_config:add_map{
  109. type = mtype,
  110. description = description,
  111. url = opt,
  112. }
  113. if map then
  114. ret.__data = map
  115. ret.hash = cache_key
  116. setmetatable(ret, ret_mt)
  117. maps_cache[cache_key] = ret
  118. return ret
  119. end
  120. elseif type(opt) == 'table' then
  121. local cache_key = lua_util.table_digest(opt)
  122. if maps_cache[cache_key] then
  123. rspamd_logger.infox(rspamd_config, 'reuse url for complex map definition %s: %s',
  124. cache_key:sub(1,8), description)
  125. return maps_cache[cache_key]
  126. end
  127. if opt[1] then
  128. -- Adjust each element if needed
  129. local adjusted
  130. for i,source in ipairs(opt) do
  131. local nsrc,ntype = maybe_adjust_type(source, mtype)
  132. if mtype ~= ntype then
  133. if not adjusted then
  134. mtype = ntype
  135. end
  136. adjusted = true
  137. end
  138. opt[i] = nsrc
  139. end
  140. if mtype == 'radix' then
  141. if string.find(opt[1], '^%d') then
  142. local map = rspamd_config:radix_from_ucl(opt)
  143. if map then
  144. ret.__data = map
  145. setmetatable(ret, ret_mt)
  146. maps_cache[cache_key] = ret
  147. return ret
  148. end
  149. else
  150. -- Plain table
  151. local map = rspamd_config:add_map{
  152. type = mtype,
  153. description = description,
  154. url = opt,
  155. }
  156. if map then
  157. ret.__data = map
  158. setmetatable(ret, ret_mt)
  159. maps_cache[cache_key] = ret
  160. return ret
  161. end
  162. end
  163. elseif mtype == 'regexp' or mtype == 'glob' then
  164. if string.find(opt[1], '^/%a') or string.find(opt[1], '^http') then
  165. -- Plain table
  166. local map = rspamd_config:add_map{
  167. type = mtype,
  168. description = description,
  169. url = opt,
  170. }
  171. if map then
  172. ret.__data = map
  173. setmetatable(ret, ret_mt)
  174. maps_cache[cache_key] = ret
  175. return ret
  176. end
  177. else
  178. local map = rspamd_config:add_map{
  179. type = mtype,
  180. description = description,
  181. url = {
  182. url = 'static',
  183. data = opt,
  184. }
  185. }
  186. if map then
  187. ret.__data = map
  188. setmetatable(ret, ret_mt)
  189. maps_cache[cache_key] = ret
  190. return ret
  191. end
  192. end
  193. else
  194. if string.find(opt[1], '^/%a') or string.find(opt[1], '^http') then
  195. -- Plain table
  196. local map = rspamd_config:add_map{
  197. type = mtype,
  198. description = description,
  199. url = opt,
  200. }
  201. if map then
  202. ret.__data = map
  203. setmetatable(ret, ret_mt)
  204. maps_cache[cache_key] = ret
  205. return ret
  206. end
  207. else
  208. local data = {}
  209. local nelts = 0
  210. -- Plain array of keys, count merely numeric elts
  211. for _,elt in ipairs(opt) do
  212. if type(elt) == 'string' then
  213. -- Numeric table
  214. if mtype == 'hash' then
  215. -- Treat as KV pair
  216. local pieces = lua_util.str_split(elt, ' ')
  217. if #pieces > 1 then
  218. local key = table.remove(pieces, 1)
  219. data[key] = table.concat(pieces, ' ')
  220. else
  221. data[elt] = true
  222. end
  223. else
  224. data[elt] = true
  225. end
  226. nelts = nelts + 1
  227. end
  228. end
  229. if nelts > 0 then
  230. -- Plain Lua table that is used as a map
  231. ret.__data = data
  232. ret.get_key = function(t, k)
  233. if k ~= '__data' then
  234. return t.__data[k]
  235. end
  236. return nil
  237. end
  238. maps_cache[cache_key] = ret
  239. return ret
  240. else
  241. -- Empty map, huh?
  242. rspamd_logger.errx(rspamd_config, 'invalid map element: %s',
  243. opt)
  244. end
  245. end
  246. end
  247. else
  248. -- We have some non-trivial object so let C code to deal with it somehow...
  249. local map = rspamd_config:add_map{
  250. type = mtype,
  251. description = description,
  252. url = opt,
  253. }
  254. if map then
  255. ret.__data = map
  256. setmetatable(ret, ret_mt)
  257. maps_cache[cache_key] = ret
  258. return ret
  259. end
  260. end -- opt[1]
  261. end
  262. return nil
  263. end
  264. --[[[
  265. -- @function lua_maps.map_add(mname, optname, mtype, description)
  266. -- Creates a map from configuration elements (static data or URL)
  267. -- Returns true if map was added or nil
  268. -- @param {string} mname config section to use
  269. -- @param {string} optname option name to use
  270. -- @param {string} mtype type of map ('set', 'hash', 'radix', 'regexp', 'glob')
  271. -- @param {string} description human-readable description of map
  272. -- @return {bool} true on success, or `nil`
  273. --]]
  274. local function rspamd_map_add(mname, optname, mtype, description)
  275. local opt = rspamd_config:get_module_opt(mname, optname)
  276. return rspamd_map_add_from_ucl(opt, mtype, description)
  277. end
  278. exports.rspamd_map_add = rspamd_map_add
  279. exports.map_add = rspamd_map_add
  280. exports.rspamd_map_add_from_ucl = rspamd_map_add_from_ucl
  281. exports.map_add_from_ucl = rspamd_map_add_from_ucl
  282. -- Check `what` for being lua_map name, otherwise just compares key with what
  283. local function rspamd_maybe_check_map(key, what)
  284. local fun = require "fun"
  285. if type(what) == "table" then
  286. return fun.any(function(elt) return rspamd_maybe_check_map(key, elt) end, what)
  287. end
  288. if type(rspamd_maps) == "table" then
  289. local mn
  290. if starts(what, "map:") then
  291. mn = string.sub(what, 4)
  292. elseif starts(what, "map://") then
  293. mn = string.sub(what, 6)
  294. end
  295. if mn and rspamd_maps[mn] then
  296. return rspamd_maps[mn]:get_key(key)
  297. else
  298. return what:lower() == key
  299. end
  300. else
  301. return what:lower() == key
  302. end
  303. end
  304. exports.rspamd_maybe_check_map = rspamd_maybe_check_map
  305. --[[[
  306. -- @function lua_maps.fill_config_maps(mname, options, defs)
  307. -- Fill maps that could be defined in defs, from the config in the options
  308. -- Defs is a table indexed by a map's parameter name and defining it's config,
  309. -- for example:
  310. defs = {
  311. my_map = {
  312. type = 'map',
  313. description = 'my cool map',
  314. optional = true,
  315. }
  316. }
  317. -- Then this function will look for opts.my_map parameter and try to replace it's with
  318. -- a map with the specific type, description but not failing if it was empty.
  319. -- It will also set options.my_map_orig to the original value defined in the map
  320. --]]
  321. exports.fill_config_maps = function(mname, opts, map_defs)
  322. assert(type(opts) == 'table')
  323. assert(type(map_defs) == 'table')
  324. for k, v in pairs(map_defs) do
  325. if opts[k] then
  326. local map = rspamd_map_add_from_ucl(opts[k], v.type or 'map', v.description)
  327. if not map then
  328. rspamd_logger.errx(rspamd_config, 'map add error %s for module %s', k, mname)
  329. return false
  330. end
  331. opts[k..'_orig'] = opts[k]
  332. opts[k] = map
  333. elseif not v.optional then
  334. rspamd_logger.errx(rspamd_config, 'cannot find non optional map %s for module %s', k, mname)
  335. return false
  336. end
  337. end
  338. return true
  339. end
  340. exports.map_schema = ts.one_of{
  341. ts.string, -- 'http://some_map'
  342. ts.array_of(ts.string), -- ['foo', 'bar']
  343. ts.shape{ -- complex object
  344. name = ts.string:is_optional(),
  345. description = ts.string:is_optional(),
  346. timeout = ts.number,
  347. data = ts.array_of(ts.string):is_optional(),
  348. -- Tableshape has no options support for something like key1 or key2?
  349. upstreams = ts.one_of{
  350. ts.string,
  351. ts.array_of(ts.string),
  352. }:is_optional(),
  353. url = ts.one_of{
  354. ts.string,
  355. ts.array_of(ts.string),
  356. }:is_optional(),
  357. }
  358. }
  359. return exports