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.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  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. }
  57. for _,t in ipairs(known_types) do
  58. if check_prefix(t[1], t[2]) then
  59. return data,mtype
  60. end
  61. end
  62. -- No change
  63. return data,mtype
  64. end
  65. --[[[
  66. -- @function lua_maps.map_add_from_ucl(opt, mtype, description)
  67. -- Creates a map from static data
  68. -- Returns true if map was added or nil
  69. -- @param {string or table} opt data for map (or URL)
  70. -- @param {string} mtype type of map (`set`, `map`, `radix`, `regexp`)
  71. -- @param {string} description human-readable description of map
  72. -- @return {bool} true on success, or `nil`
  73. --]]
  74. local function rspamd_map_add_from_ucl(opt, mtype, description)
  75. local ret = {
  76. get_key = function(t, k)
  77. if t.__data then
  78. return t.__data:get_key(k)
  79. end
  80. return nil
  81. end
  82. }
  83. local ret_mt = {
  84. __index = function(t, k)
  85. if t.__data then
  86. return t.get_key(k)
  87. end
  88. return nil
  89. end
  90. }
  91. if not opt then
  92. return nil
  93. end
  94. if type(opt) == 'string' then
  95. opt,mtype = maybe_adjust_type(opt, mtype)
  96. local cache_key = map_hash_key(opt, mtype)
  97. if maps_cache[cache_key] then
  98. rspamd_logger.infox(rspamd_config, 'reuse url for %s(%s)',
  99. opt, mtype)
  100. return maps_cache[cache_key]
  101. end
  102. -- We have a single string, so we treat it as a map
  103. local map = rspamd_config:add_map{
  104. type = mtype,
  105. description = description,
  106. url = opt,
  107. }
  108. if map then
  109. ret.__data = map
  110. ret.hash = cache_key
  111. setmetatable(ret, ret_mt)
  112. maps_cache[cache_key] = ret
  113. return ret
  114. end
  115. elseif type(opt) == 'table' then
  116. local cache_key = lua_util.table_digest(opt)
  117. if maps_cache[cache_key] then
  118. rspamd_logger.infox(rspamd_config, 'reuse url for complex map definition %s: %s',
  119. cache_key:sub(1,8), description)
  120. return maps_cache[cache_key]
  121. end
  122. if opt[1] then
  123. if mtype == 'radix' then
  124. if string.find(opt[1], '^%d') then
  125. local map = rspamd_config:radix_from_ucl(opt)
  126. if map then
  127. ret.__data = map
  128. setmetatable(ret, ret_mt)
  129. maps_cache[cache_key] = ret
  130. return ret
  131. end
  132. else
  133. -- Plain table
  134. local map = rspamd_config:add_map{
  135. type = mtype,
  136. description = description,
  137. url = opt,
  138. }
  139. if map then
  140. ret.__data = map
  141. setmetatable(ret, ret_mt)
  142. maps_cache[cache_key] = ret
  143. return ret
  144. end
  145. end
  146. elseif mtype == 'regexp' or mtype == 'glob' then
  147. if string.find(opt[1], '^/%a') or string.find(opt[1], '^http') then
  148. -- Plain table
  149. local map = rspamd_config:add_map{
  150. type = mtype,
  151. description = description,
  152. url = opt,
  153. }
  154. if map then
  155. ret.__data = map
  156. setmetatable(ret, ret_mt)
  157. maps_cache[cache_key] = ret
  158. return ret
  159. end
  160. else
  161. local map = rspamd_config:add_map{
  162. type = mtype,
  163. description = description,
  164. url = {
  165. url = 'static',
  166. data = opt,
  167. }
  168. }
  169. if map then
  170. ret.__data = map
  171. setmetatable(ret, ret_mt)
  172. maps_cache[cache_key] = ret
  173. return ret
  174. end
  175. end
  176. else
  177. if string.find(opt[1], '^/%a') or string.find(opt[1], '^http') then
  178. -- Plain table
  179. local map = rspamd_config:add_map{
  180. type = mtype,
  181. description = description,
  182. url = opt,
  183. }
  184. if map then
  185. ret.__data = map
  186. setmetatable(ret, ret_mt)
  187. maps_cache[cache_key] = ret
  188. return ret
  189. end
  190. else
  191. local data = {}
  192. local nelts = 0
  193. -- Plain array of keys, count merely numeric elts
  194. for _,elt in ipairs(opt) do
  195. if type(elt) == 'string' then
  196. -- Numeric table
  197. if mtype == 'hash' then
  198. -- Treat as KV pair
  199. local pieces = lua_util.str_split(elt, ' ')
  200. if #pieces > 1 then
  201. local key = table.remove(pieces, 1)
  202. data[key] = table.concat(pieces, ' ')
  203. else
  204. data[elt] = true
  205. end
  206. else
  207. data[elt] = true
  208. end
  209. nelts = nelts + 1
  210. end
  211. end
  212. if nelts > 0 then
  213. -- Plain Lua table that is used as a map
  214. ret.__data = data
  215. ret.get_key = function(t, k)
  216. if k ~= '__data' then
  217. return t.__data[k]
  218. end
  219. return nil
  220. end
  221. maps_cache[cache_key] = ret
  222. return ret
  223. else
  224. -- Empty map, huh?
  225. rspamd_logger.errx(rspamd_config, 'invalid map element: %s',
  226. opt)
  227. end
  228. end
  229. end
  230. else
  231. -- We have some non-trivial object so let C code to deal with it somehow...
  232. local map = rspamd_config:add_map{
  233. type = mtype,
  234. description = description,
  235. url = opt,
  236. }
  237. if map then
  238. ret.__data = map
  239. setmetatable(ret, ret_mt)
  240. maps_cache[cache_key] = ret
  241. return ret
  242. end
  243. end -- opt[1]
  244. end
  245. return nil
  246. end
  247. --[[[
  248. -- @function lua_maps.map_add(mname, optname, mtype, description)
  249. -- Creates a map from configuration elements (static data or URL)
  250. -- Returns true if map was added or nil
  251. -- @param {string} mname config section to use
  252. -- @param {string} optname option name to use
  253. -- @param {string} mtype type of map ('set', 'hash', 'radix', 'regexp', 'glob')
  254. -- @param {string} description human-readable description of map
  255. -- @return {bool} true on success, or `nil`
  256. --]]
  257. local function rspamd_map_add(mname, optname, mtype, description)
  258. local opt = rspamd_config:get_module_opt(mname, optname)
  259. return rspamd_map_add_from_ucl(opt, mtype, description)
  260. end
  261. exports.rspamd_map_add = rspamd_map_add
  262. exports.map_add = rspamd_map_add
  263. exports.rspamd_map_add_from_ucl = rspamd_map_add_from_ucl
  264. exports.map_add_from_ucl = rspamd_map_add_from_ucl
  265. -- Check `what` for being lua_map name, otherwise just compares key with what
  266. local function rspamd_maybe_check_map(key, what)
  267. local fun = require "fun"
  268. if type(what) == "table" then
  269. return fun.any(function(elt) return rspamd_maybe_check_map(key, elt) end, what)
  270. end
  271. if type(rspamd_maps) == "table" then
  272. local mn
  273. if starts(what, "map:") then
  274. mn = string.sub(what, 4)
  275. elseif starts(what, "map://") then
  276. mn = string.sub(what, 6)
  277. end
  278. if mn and rspamd_maps[mn] then
  279. return rspamd_maps[mn]:get_key(key)
  280. else
  281. return what:lower() == key
  282. end
  283. else
  284. return what:lower() == key
  285. end
  286. end
  287. exports.rspamd_maybe_check_map = rspamd_maybe_check_map
  288. exports.map_schema = ts.one_of{
  289. ts.string, -- 'http://some_map'
  290. ts.array_of(ts.string), -- ['foo', 'bar']
  291. ts.shape{ -- complex object
  292. name = ts.string:is_optional(),
  293. description = ts.string:is_optional(),
  294. timeout = ts.number,
  295. data = ts.array_of(ts.string):is_optional(),
  296. -- Tableshape has no options support for something like key1 or key2?
  297. upstreams = ts.one_of{
  298. ts.string,
  299. ts.array_of(ts.string),
  300. }:is_optional(),
  301. url = ts.one_of{
  302. ts.string,
  303. ts.array_of(ts.string),
  304. }:is_optional(),
  305. }
  306. }
  307. return exports