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 9.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. --[[[
  2. -- @module lua_maps
  3. -- This module contains helper functions for managing rspamd maps
  4. --]]
  5. --[[
  6. Copyright (c) 2017, Vsevolod Stakhov <vsevolod@highsecure.ru>
  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. if mtype == 'radix' then
  129. if string.find(opt[1], '^%d') then
  130. local map = rspamd_config:radix_from_ucl(opt)
  131. if map then
  132. ret.__data = map
  133. setmetatable(ret, ret_mt)
  134. maps_cache[cache_key] = ret
  135. return ret
  136. end
  137. else
  138. -- Plain table
  139. local map = rspamd_config:add_map{
  140. type = mtype,
  141. description = description,
  142. url = opt,
  143. }
  144. if map then
  145. ret.__data = map
  146. setmetatable(ret, ret_mt)
  147. maps_cache[cache_key] = ret
  148. return ret
  149. end
  150. end
  151. elseif mtype == 'regexp' or mtype == 'glob' then
  152. if string.find(opt[1], '^/%a') or string.find(opt[1], '^http') then
  153. -- Plain table
  154. local map = rspamd_config:add_map{
  155. type = mtype,
  156. description = description,
  157. url = opt,
  158. }
  159. if map then
  160. ret.__data = map
  161. setmetatable(ret, ret_mt)
  162. maps_cache[cache_key] = ret
  163. return ret
  164. end
  165. else
  166. local map = rspamd_config:add_map{
  167. type = mtype,
  168. description = description,
  169. url = {
  170. url = 'static',
  171. data = opt,
  172. }
  173. }
  174. if map then
  175. ret.__data = map
  176. setmetatable(ret, ret_mt)
  177. maps_cache[cache_key] = ret
  178. return ret
  179. end
  180. end
  181. else
  182. if string.find(opt[1], '^/%a') or string.find(opt[1], '^http') then
  183. -- Plain table
  184. local map = rspamd_config:add_map{
  185. type = mtype,
  186. description = description,
  187. url = opt,
  188. }
  189. if map then
  190. ret.__data = map
  191. setmetatable(ret, ret_mt)
  192. maps_cache[cache_key] = ret
  193. return ret
  194. end
  195. else
  196. local data = {}
  197. local nelts = 0
  198. -- Plain array of keys, count merely numeric elts
  199. for _,elt in ipairs(opt) do
  200. if type(elt) == 'string' then
  201. -- Numeric table
  202. if mtype == 'hash' then
  203. -- Treat as KV pair
  204. local pieces = lua_util.str_split(elt, ' ')
  205. if #pieces > 1 then
  206. local key = table.remove(pieces, 1)
  207. data[key] = table.concat(pieces, ' ')
  208. else
  209. data[elt] = true
  210. end
  211. else
  212. data[elt] = true
  213. end
  214. nelts = nelts + 1
  215. end
  216. end
  217. if nelts > 0 then
  218. -- Plain Lua table that is used as a map
  219. ret.__data = data
  220. ret.get_key = function(t, k)
  221. if k ~= '__data' then
  222. return t.__data[k]
  223. end
  224. return nil
  225. end
  226. maps_cache[cache_key] = ret
  227. return ret
  228. else
  229. -- Empty map, huh?
  230. rspamd_logger.errx(rspamd_config, 'invalid map element: %s',
  231. opt)
  232. end
  233. end
  234. end
  235. else
  236. -- We have some non-trivial object so let C code to deal with it somehow...
  237. local map = rspamd_config:add_map{
  238. type = mtype,
  239. description = description,
  240. url = opt,
  241. }
  242. if map then
  243. ret.__data = map
  244. setmetatable(ret, ret_mt)
  245. maps_cache[cache_key] = ret
  246. return ret
  247. end
  248. end -- opt[1]
  249. end
  250. return nil
  251. end
  252. --[[[
  253. -- @function lua_maps.map_add(mname, optname, mtype, description)
  254. -- Creates a map from configuration elements (static data or URL)
  255. -- Returns true if map was added or nil
  256. -- @param {string} mname config section to use
  257. -- @param {string} optname option name to use
  258. -- @param {string} mtype type of map ('set', 'hash', 'radix', 'regexp', 'glob')
  259. -- @param {string} description human-readable description of map
  260. -- @return {bool} true on success, or `nil`
  261. --]]
  262. local function rspamd_map_add(mname, optname, mtype, description)
  263. local opt = rspamd_config:get_module_opt(mname, optname)
  264. return rspamd_map_add_from_ucl(opt, mtype, description)
  265. end
  266. exports.rspamd_map_add = rspamd_map_add
  267. exports.map_add = rspamd_map_add
  268. exports.rspamd_map_add_from_ucl = rspamd_map_add_from_ucl
  269. exports.map_add_from_ucl = rspamd_map_add_from_ucl
  270. -- Check `what` for being lua_map name, otherwise just compares key with what
  271. local function rspamd_maybe_check_map(key, what)
  272. local fun = require "fun"
  273. if type(what) == "table" then
  274. return fun.any(function(elt) return rspamd_maybe_check_map(key, elt) end, what)
  275. end
  276. if type(rspamd_maps) == "table" then
  277. local mn
  278. if starts(what, "map:") then
  279. mn = string.sub(what, 4)
  280. elseif starts(what, "map://") then
  281. mn = string.sub(what, 6)
  282. end
  283. if mn and rspamd_maps[mn] then
  284. return rspamd_maps[mn]:get_key(key)
  285. else
  286. return what:lower() == key
  287. end
  288. else
  289. return what:lower() == key
  290. end
  291. end
  292. exports.rspamd_maybe_check_map = rspamd_maybe_check_map
  293. exports.map_schema = ts.one_of{
  294. ts.string, -- 'http://some_map'
  295. ts.array_of(ts.string), -- ['foo', 'bar']
  296. ts.shape{ -- complex object
  297. name = ts.string:is_optional(),
  298. description = ts.string:is_optional(),
  299. timeout = ts.number,
  300. data = ts.array_of(ts.string):is_optional(),
  301. -- Tableshape has no options support for something like key1 or key2?
  302. upstreams = ts.one_of{
  303. ts.string,
  304. ts.array_of(ts.string),
  305. }:is_optional(),
  306. url = ts.one_of{
  307. ts.string,
  308. ts.array_of(ts.string),
  309. }:is_optional(),
  310. }
  311. }
  312. return exports