Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

replies.lua 9.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. --[[
  2. Copyright (c) 2022, Vsevolod Stakhov <vsevolod@rspamd.com>
  3. Copyright (c) 2016, Andrew Lewis <nerf@judo.za.org>
  4. Licensed under the Apache License, Version 2.0 (the "License");
  5. you may not use this file except in compliance with the License.
  6. You may obtain a copy of the License at
  7. http://www.apache.org/licenses/LICENSE-2.0
  8. Unless required by applicable law or agreed to in writing, software
  9. distributed under the License is distributed on an "AS IS" BASIS,
  10. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11. See the License for the specific language governing permissions and
  12. limitations under the License.
  13. ]]--
  14. if confighelp then
  15. return
  16. end
  17. local rspamd_logger = require 'rspamd_logger'
  18. local hash = require 'rspamd_cryptobox_hash'
  19. local lua_util = require 'lua_util'
  20. local lua_redis = require 'lua_redis'
  21. local fun = require "fun"
  22. -- A plugin that implements replies check using redis
  23. -- Default port for redis upstreams
  24. local redis_params
  25. local settings = {
  26. action = nil,
  27. expire = 86400, -- 1 day by default
  28. key_prefix = 'rr',
  29. key_size = 20,
  30. message = 'Message is reply to one we originated',
  31. symbol = 'REPLY',
  32. score = -4, -- Default score
  33. use_auth = true,
  34. use_local = true,
  35. cookie = nil,
  36. cookie_key = nil,
  37. cookie_is_pattern = false,
  38. cookie_valid_time = '2w', -- 2 weeks by default
  39. min_message_id = 2, -- minimum length of the message-id header
  40. }
  41. local N = "replies"
  42. local function make_key(goop, sz, prefix)
  43. local h = hash.create()
  44. h:update(goop)
  45. local key
  46. if sz then
  47. key = h:base32():sub(1, sz)
  48. else
  49. key = h:base32()
  50. end
  51. if prefix then
  52. key = prefix .. key
  53. end
  54. return key
  55. end
  56. local function replies_check(task)
  57. local in_reply_to
  58. local function check_recipient(stored_rcpt)
  59. local rcpts = task:get_recipients('mime')
  60. if rcpts then
  61. local filter_predicate = function(input_rcpt)
  62. local real_rcpt_h = make_key(input_rcpt:lower(), 8)
  63. return real_rcpt_h == stored_rcpt
  64. end
  65. if fun.any(filter_predicate, fun.map(function(rcpt)
  66. return rcpt.addr or ''
  67. end, rcpts)) then
  68. lua_util.debugm(N, task, 'reply to %s validated', in_reply_to)
  69. return true
  70. end
  71. rspamd_logger.infox(task, 'ignoring reply to %s as no recipients are matching hash %s',
  72. in_reply_to, stored_rcpt)
  73. else
  74. rspamd_logger.infox(task, 'ignoring reply to %s as recipient cannot be detected for hash %s',
  75. in_reply_to, stored_rcpt)
  76. end
  77. return false
  78. end
  79. local function redis_get_cb(err, data, addr)
  80. if err ~= nil then
  81. rspamd_logger.errx(task, 'redis_get_cb error when reading data from %s: %s', addr:get_addr(), err)
  82. return
  83. end
  84. if data and type(data) == 'string' and check_recipient(data) then
  85. -- Hash was found
  86. task:insert_result(settings['symbol'], 1.0)
  87. if settings['action'] ~= nil then
  88. local ip_addr = task:get_ip()
  89. if (settings.use_auth and
  90. task:get_user()) or
  91. (settings.use_local and ip_addr and ip_addr:is_local()) then
  92. rspamd_logger.infox(task, "not forcing action for local network or authorized user");
  93. else
  94. task:set_pre_result(settings['action'], settings['message'], N)
  95. end
  96. end
  97. end
  98. end
  99. -- If in-reply-to header not present return
  100. in_reply_to = task:get_header_raw('in-reply-to')
  101. if not in_reply_to then
  102. return
  103. end
  104. -- Create hash of in-reply-to and query redis
  105. local key = make_key(in_reply_to, settings.key_size, settings.key_prefix)
  106. local ret = lua_redis.redis_make_request(task,
  107. redis_params, -- connect params
  108. key, -- hash key
  109. false, -- is write
  110. redis_get_cb, --callback
  111. 'GET', -- command
  112. { key } -- arguments
  113. )
  114. if not ret then
  115. rspamd_logger.errx(task, "redis request wasn't scheduled")
  116. end
  117. end
  118. local function replies_set(task)
  119. local function redis_set_cb(err, _, addr)
  120. if err ~= nil then
  121. rspamd_logger.errx(task, 'redis_set_cb error when writing data to %s: %s', addr:get_addr(), err)
  122. end
  123. end
  124. -- If sender is unauthenticated return
  125. local ip = task:get_ip()
  126. if settings.use_auth and task:get_user() then
  127. lua_util.debugm(N, task, 'sender is authenticated')
  128. elseif settings.use_local and (ip and ip:is_local()) then
  129. lua_util.debugm(N, task, 'sender is from local network')
  130. else
  131. return
  132. end
  133. -- If no message-id present return
  134. local msg_id = task:get_header_raw('message-id')
  135. if msg_id == nil or msg_id:len() <= (settings.min_message_id or 2) then
  136. return
  137. end
  138. -- Create hash of message-id and store to redis
  139. local key = make_key(msg_id, settings.key_size, settings.key_prefix)
  140. local sender = task:get_reply_sender()
  141. if sender then
  142. local sender_hash = make_key(sender:lower(), 8)
  143. lua_util.debugm(N, task, 'storing id: %s (%s), reply-to: %s (%s) for replies check',
  144. msg_id, key, sender, sender_hash)
  145. local ret = lua_redis.redis_make_request(task,
  146. redis_params, -- connect params
  147. key, -- hash key
  148. true, -- is write
  149. redis_set_cb, --callback
  150. 'PSETEX', -- command
  151. { key, tostring(math.floor(settings['expire'] * 1000)), sender_hash } -- arguments
  152. )
  153. if not ret then
  154. rspamd_logger.errx(task, "redis request wasn't scheduled")
  155. end
  156. else
  157. rspamd_logger.infox(task, "cannot find reply sender address")
  158. end
  159. end
  160. local function replies_check_cookie(task)
  161. local function cookie_matched(extra, ts)
  162. local dt = task:get_date { format = 'connect', gmt = true }
  163. if dt < ts then
  164. rspamd_logger.infox(task, 'ignore cookie as its date is in future')
  165. return
  166. end
  167. if settings.cookie_valid_time then
  168. if dt - ts > settings.cookie_valid_time then
  169. rspamd_logger.infox(task,
  170. 'ignore cookie as its timestamp is too old: %s (%s current time)',
  171. ts, dt)
  172. return
  173. end
  174. end
  175. if extra then
  176. task:insert_result(settings['symbol'], 1.0,
  177. string.format('cookie:%s:%s', extra, ts))
  178. else
  179. task:insert_result(settings['symbol'], 1.0,
  180. string.format('cookie:%s', ts))
  181. end
  182. if settings['action'] ~= nil then
  183. local ip_addr = task:get_ip()
  184. if (settings.use_auth and
  185. task:get_user()) or
  186. (settings.use_local and ip_addr and ip_addr:is_local()) then
  187. rspamd_logger.infox(task, "not forcing action for local network or authorized user");
  188. else
  189. task:set_pre_result(settings['action'], settings['message'], N)
  190. end
  191. end
  192. end
  193. -- If in-reply-to header not present return
  194. local irt = task:get_header('in-reply-to')
  195. if irt == nil then
  196. return
  197. end
  198. local cr = require "rspamd_cryptobox"
  199. -- Extract user part if needed
  200. local extracted_cookie = irt:match('^%<?([^@]+)@.*$')
  201. if not extracted_cookie then
  202. -- Assume full message id as a cookie
  203. extracted_cookie = irt
  204. end
  205. local dec_cookie, ts = cr.decrypt_cookie(settings.cookie_key, extracted_cookie)
  206. if dec_cookie then
  207. -- We have something that looks like a cookie
  208. if settings.cookie_is_pattern then
  209. local m = dec_cookie:match(settings.cookie)
  210. if m then
  211. cookie_matched(m, ts)
  212. end
  213. else
  214. -- Direct match
  215. if dec_cookie == settings.cookie then
  216. cookie_matched(nil, ts)
  217. end
  218. end
  219. end
  220. end
  221. local opts = rspamd_config:get_all_opt('replies')
  222. if not (opts and type(opts) == 'table') then
  223. rspamd_logger.infox(rspamd_config, 'module is unconfigured')
  224. return
  225. end
  226. if opts then
  227. settings = lua_util.override_defaults(settings, opts)
  228. redis_params = lua_redis.parse_redis_server('replies')
  229. if not redis_params then
  230. if not (settings.cookie and settings.cookie_key) then
  231. rspamd_logger.infox(rspamd_config, 'no servers are specified, disabling module')
  232. lua_util.disable_module(N, "redis")
  233. else
  234. -- Cookies mode
  235. -- Check key sanity:
  236. local pattern = { '^' }
  237. for i = 1, 32 do
  238. pattern[i + 1] = '[a-zA-Z0-9]'
  239. end
  240. pattern[34] = '$'
  241. if not settings.cookie_key:match(table.concat(pattern, '')) then
  242. rspamd_logger.errx(rspamd_config,
  243. 'invalid cookies key: %s, must be 32 hex digits', settings.cookie_key)
  244. lua_util.disable_module(N, "config")
  245. return
  246. end
  247. if settings.cookie_valid_time then
  248. settings.cookie_valid_time = lua_util.parse_time_interval(settings.cookie_valid_time)
  249. end
  250. local id = rspamd_config:register_symbol({
  251. name = 'REPLIES_CHECK',
  252. type = 'prefilter',
  253. callback = replies_check_cookie,
  254. flags = 'nostat',
  255. priority = lua_util.symbols_priorities.medium,
  256. group = "replies"
  257. })
  258. rspamd_config:register_symbol({
  259. name = settings['symbol'],
  260. parent = id,
  261. type = 'virtual',
  262. score = settings.score,
  263. group = "replies",
  264. })
  265. end
  266. else
  267. rspamd_config:register_symbol({
  268. name = 'REPLIES_SET',
  269. type = 'idempotent',
  270. callback = replies_set,
  271. group = 'replies',
  272. flags = 'explicit_disable,ignore_passthrough',
  273. })
  274. local id = rspamd_config:register_symbol({
  275. name = 'REPLIES_CHECK',
  276. type = 'prefilter',
  277. flags = 'nostat',
  278. callback = replies_check,
  279. priority = lua_util.symbols_priorities.medium,
  280. group = "replies"
  281. })
  282. rspamd_config:register_symbol({
  283. name = settings['symbol'],
  284. parent = id,
  285. type = 'virtual',
  286. score = settings.score,
  287. group = "replies",
  288. })
  289. end
  290. end