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.

icap.lua 25KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713
  1. --[[
  2. Copyright (c) 2022, Vsevolod Stakhov <vsevolod@rspamd.com>
  3. Copyright (c) 2019, Carsten Rosenberg <c.rosenberg@heinlein-support.de>
  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. --[[
  15. @module icap
  16. This module contains icap access functions.
  17. Currently tested with
  18. - C-ICAP Squidclamav / echo
  19. - Checkpoint Sandblast
  20. - F-Secure Internet Gatekeeper
  21. - Kaspersky Web Traffic Security
  22. - Kaspersky Scan Engine 2.0
  23. - McAfee Web Gateway 9/10/11
  24. - Sophos Savdi
  25. - Symantec (Rspamd <3.2, >=3.2 untested)
  26. - Trend Micro IWSVA 6.0
  27. - Trend Micro Web Gateway
  28. @TODO
  29. - Preview / Continue
  30. - Reqmod URL's
  31. - Content-Type / Filename
  32. ]] --
  33. --[[
  34. Configuration Notes:
  35. C-ICAP Squidclamav
  36. scheme = "squidclamav";
  37. Checkpoint Sandblast example:
  38. scheme = "sandblast";
  39. ESET Gateway Security / Antivirus for Linux example:
  40. scheme = "scan";
  41. F-Secure Internet Gatekeeper example:
  42. scheme = "respmod";
  43. x_client_header = true;
  44. x_rcpt_header = true;
  45. x_from_header = true;
  46. Kaspersky Web Traffic Security example:
  47. scheme = "av/respmod";
  48. x_client_header = true;
  49. Kaspersky Web Traffic Security (as configured in kavicapd.xml):
  50. scheme = "resp";
  51. x_client_header = true;
  52. McAfee Web Gateway 10/11 (Headers must be activated with personal extra Rules)
  53. scheme = "respmod";
  54. x_client_header = true;
  55. Sophos SAVDI example:
  56. # scheme as configured in savdi.conf (name option in service section)
  57. scheme = "respmod";
  58. Symantec example:
  59. scheme = "avscan";
  60. Trend Micro IWSVA example (X-Virus-ID/X-Infection-Found headers must be activated):
  61. scheme = "avscan";
  62. x_client_header = true;
  63. Trend Micro Web Gateway example (X-Virus-ID/X-Infection-Found headers must be activated):
  64. scheme = "interscan";
  65. x_client_header = true;
  66. ]] --
  67. local lua_util = require "lua_util"
  68. local tcp = require "rspamd_tcp"
  69. local upstream_list = require "rspamd_upstream_list"
  70. local rspamd_logger = require "rspamd_logger"
  71. local common = require "lua_scanners/common"
  72. local rspamd_util = require "rspamd_util"
  73. local rspamd_version = rspamd_version
  74. local N = 'icap'
  75. local function icap_config(opts)
  76. local icap_conf = {
  77. name = N,
  78. scan_mime_parts = true,
  79. scan_all_mime_parts = true,
  80. scan_text_mime = false,
  81. scan_image_mime = false,
  82. scheme = "scan",
  83. default_port = 1344,
  84. ssl = false,
  85. no_ssl_verify = false,
  86. timeout = 10.0,
  87. log_clean = false,
  88. retransmits = 2,
  89. cache_expire = 7200, -- expire redis in one hour
  90. message = '${SCANNER}: threat found with icap scanner: "${VIRUS}"',
  91. detection_category = "virus",
  92. default_score = 1,
  93. action = false,
  94. dynamic_scan = false,
  95. user_agent = "Rspamd",
  96. x_client_header = false,
  97. x_rcpt_header = false,
  98. x_from_header = false,
  99. req_headers_enabled = true,
  100. req_fake_url = "http://127.0.0.1/mail",
  101. http_headers_enabled = true,
  102. use_http_result_header = true,
  103. use_http_3xx_as_threat = false,
  104. use_specific_content_type = false, -- Use content type from a part where possible
  105. }
  106. icap_conf = lua_util.override_defaults(icap_conf, opts)
  107. if not icap_conf.prefix then
  108. icap_conf.prefix = 'rs_' .. icap_conf.name .. '_'
  109. end
  110. if not icap_conf.log_prefix then
  111. icap_conf.log_prefix = icap_conf.name .. ' (' .. icap_conf.type .. ')'
  112. end
  113. if not icap_conf.log_prefix then
  114. if icap_conf.name:lower() == icap_conf.type:lower() then
  115. icap_conf.log_prefix = icap_conf.name
  116. else
  117. icap_conf.log_prefix = icap_conf.name .. ' (' .. icap_conf.type .. ')'
  118. end
  119. end
  120. if not icap_conf.servers then
  121. rspamd_logger.errx(rspamd_config, 'no servers defined')
  122. return nil
  123. end
  124. icap_conf.upstreams = upstream_list.create(rspamd_config,
  125. icap_conf.servers,
  126. icap_conf.default_port)
  127. if icap_conf.upstreams then
  128. lua_util.add_debug_alias('external_services', icap_conf.name)
  129. return icap_conf
  130. end
  131. rspamd_logger.errx(rspamd_config, 'cannot parse servers %s',
  132. icap_conf.servers)
  133. return nil
  134. end
  135. local function icap_check(task, content, digest, rule, maybe_part)
  136. local function icap_check_uncached ()
  137. local upstream = rule.upstreams:get_upstream_round_robin()
  138. local addr = upstream:get_addr()
  139. local retransmits = rule.retransmits
  140. local http_headers = {}
  141. local req_headers = {}
  142. local tcp_options = {}
  143. local threat_table = {}
  144. -- Build extended User Agent
  145. if rule.user_agent == "extended" then
  146. rule.user_agent = string.format("Rspamd/%s-%s (%s/%s)",
  147. rspamd_version('main'),
  148. rspamd_version('id'),
  149. rspamd_util.get_hostname(),
  150. string.sub(task:get_uid(), 1, 6))
  151. end
  152. -- Build the icap queries
  153. local options_request = {
  154. string.format("OPTIONS icap://%s/%s ICAP/1.0\r\n", addr:to_string(), rule.scheme),
  155. string.format('Host: %s\r\n', addr:to_string()),
  156. string.format("User-Agent: %s\r\n", rule.user_agent),
  157. "Connection: keep-alive\r\n",
  158. "Encapsulated: null-body=0\r\n\r\n",
  159. }
  160. if rule.user_agent == "none" then
  161. table.remove(options_request, 3)
  162. end
  163. local respond_headers = {
  164. -- Add main RESPMOD header before any other
  165. string.format('RESPMOD icap://%s/%s ICAP/1.0\r\n', addr:to_string(), rule.scheme),
  166. string.format('Host: %s\r\n', addr:to_string()),
  167. }
  168. local size = tonumber(#content)
  169. local chunked_size = string.format("%x", size)
  170. local function icap_callback(err, conn)
  171. local function icap_requery(err_m, info)
  172. -- retry with another upstream until retransmits exceeds
  173. if retransmits > 0 then
  174. retransmits = retransmits - 1
  175. lua_util.debugm(rule.name, task,
  176. '%s: %s Request Error: %s - retries left: %s',
  177. rule.log_prefix, info, err_m, retransmits)
  178. -- Select a different upstream!
  179. upstream = rule.upstreams:get_upstream_round_robin()
  180. addr = upstream:get_addr()
  181. lua_util.debugm(rule.name, task, '%s: retry IP: %s:%s',
  182. rule.log_prefix, addr, addr:get_port())
  183. tcp_options.host = addr:to_string()
  184. tcp_options.port = addr:get_port()
  185. tcp_options.callback = icap_callback
  186. tcp_options.data = options_request
  187. tcp_options.upstream = upstream
  188. tcp.request(tcp_options)
  189. else
  190. rspamd_logger.errx(task, '%s: failed to scan, maximum retransmits ' ..
  191. 'exceed - error: %s', rule.log_prefix, err_m or '')
  192. common.yield_result(task, rule, string.format('failed - error: %s', err_m),
  193. 0.0, 'fail', maybe_part)
  194. end
  195. end
  196. local function get_req_headers()
  197. local in_client_ip = task:get_from_ip()
  198. local req_hlen = 2
  199. if maybe_part then
  200. table.insert(req_headers,
  201. string.format('GET http://%s/%s HTTP/1.0\r\n', in_client_ip, maybe_part:get_filename()))
  202. if rule.use_specific_content_type then
  203. table.insert(http_headers, string.format('Content-Type: %s/%s\r\n', maybe_part:get_detected_type()))
  204. --else
  205. -- To test: what content type is better for icap servers?
  206. --table.insert(http_headers, 'Content-Type: text/plain\r\n')
  207. end
  208. else
  209. table.insert(req_headers, string.format('GET %s HTTP/1.0\r\n', rule.req_fake_url))
  210. table.insert(http_headers, string.format('Content-Type: application/octet-stream\r\n'))
  211. end
  212. table.insert(req_headers, string.format('Date: %s\r\n', rspamd_util.time_to_string(rspamd_util.get_time())))
  213. if rule.user_agent ~= "none" then
  214. table.insert(req_headers, string.format("User-Agent: %s\r\n", rule.user_agent))
  215. end
  216. for _, h in ipairs(req_headers) do
  217. req_hlen = req_hlen + tonumber(#h)
  218. end
  219. return req_hlen, req_headers
  220. end
  221. local function get_http_headers()
  222. local http_hlen = 2
  223. table.insert(http_headers, 'HTTP/1.0 200 OK\r\n')
  224. table.insert(http_headers, string.format('Date: %s\r\n', rspamd_util.time_to_string(rspamd_util.get_time())))
  225. table.insert(http_headers, string.format('Server: %s\r\n', 'Apache/2.4'))
  226. if rule.user_agent ~= "none" then
  227. table.insert(http_headers, string.format("User-Agent: %s\r\n", rule.user_agent))
  228. end
  229. --table.insert(http_headers, string.format('Content-Type: %s\r\n', 'text/html'))
  230. table.insert(http_headers, string.format('Content-Length: %s\r\n', size))
  231. for _, h in ipairs(http_headers) do
  232. http_hlen = http_hlen + tonumber(#h)
  233. end
  234. return http_hlen, http_headers
  235. end
  236. local function get_respond_query()
  237. local req_hlen = 0
  238. local resp_req_headers
  239. local http_hlen = 0
  240. local resp_http_headers
  241. -- Append all extra headers
  242. if rule.user_agent ~= "none" then
  243. table.insert(respond_headers,
  244. string.format("User-Agent: %s\r\n", rule.user_agent))
  245. end
  246. if rule.req_headers_enabled then
  247. req_hlen, resp_req_headers = get_req_headers()
  248. end
  249. if rule.http_headers_enabled then
  250. http_hlen, resp_http_headers = get_http_headers()
  251. end
  252. if rule.req_headers_enabled and rule.http_headers_enabled then
  253. local res_body_hlen = req_hlen + http_hlen
  254. table.insert(respond_headers,
  255. string.format('Encapsulated: req-hdr=0, res-hdr=%s, res-body=%s\r\n',
  256. req_hlen, res_body_hlen))
  257. elseif rule.http_headers_enabled then
  258. table.insert(respond_headers,
  259. string.format('Encapsulated: res-hdr=0, res-body=%s\r\n',
  260. http_hlen))
  261. else
  262. table.insert(respond_headers, 'Encapsulated: res-body=0\r\n')
  263. end
  264. table.insert(respond_headers, '\r\n')
  265. for _, h in ipairs(resp_req_headers) do
  266. table.insert(respond_headers, h)
  267. end
  268. table.insert(respond_headers, '\r\n')
  269. for _, h in ipairs(resp_http_headers) do
  270. table.insert(respond_headers, h)
  271. end
  272. table.insert(respond_headers, '\r\n')
  273. table.insert(respond_headers, chunked_size .. '\r\n')
  274. table.insert(respond_headers, content)
  275. table.insert(respond_headers, '\r\n0\r\n\r\n')
  276. return respond_headers
  277. end
  278. local function add_respond_header(name, value)
  279. if name and value then
  280. table.insert(respond_headers, string.format('%s: %s\r\n', name, value))
  281. end
  282. end
  283. local function result_header_table(result)
  284. local icap_headers = {}
  285. for s in result:gmatch("[^\r\n]+") do
  286. if string.find(s, '^ICAP') then
  287. icap_headers['icap'] = tostring(s)
  288. elseif string.find(s, '^HTTP') then
  289. icap_headers['http'] = tostring(s)
  290. elseif string.find(s, '[%a%d-+]-:') then
  291. local _, _, key, value = tostring(s):find("([%a%d-+]-):%s?(.+)")
  292. if key ~= nil then
  293. icap_headers[key:lower()] = tostring(value)
  294. end
  295. end
  296. end
  297. lua_util.debugm(rule.name, task, '%s: icap_headers: %s',
  298. rule.log_prefix, icap_headers)
  299. return icap_headers
  300. end
  301. local function threat_table_add(icap_threat, maybe_split)
  302. if maybe_split and string.find(icap_threat, ',') then
  303. local threats = lua_util.str_split(string.gsub(icap_threat, "%s", ""), ',') or {}
  304. for _, v in ipairs(threats) do
  305. table.insert(threat_table, v)
  306. end
  307. else
  308. table.insert(threat_table, icap_threat)
  309. end
  310. return true
  311. end
  312. local function icap_parse_result(headers)
  313. --[[
  314. @ToDo: handle type in response
  315. Generic Strings:
  316. icap: X-Infection-Found: Type=0; Resolution=2; Threat=Troj/DocDl-OYC;
  317. icap: X-Infection-Found: Type=0; Resolution=2; Threat=W97M.Downloader;
  318. Symantec String:
  319. icap: X-Infection-Found: Type=2; Resolution=2; Threat=Container size violation
  320. icap: X-Infection-Found: Type=2; Resolution=2; Threat=Encrypted container violation;
  321. Sophos Strings:
  322. icap: X-Virus-ID: Troj/DocDl-OYC
  323. http: X-Blocked: Virus found during virus scan
  324. http: X-Blocked-By: Sophos Anti-Virus
  325. Kaspersky Web Traffic Security Strings:
  326. icap: X-Virus-ID: HEUR:Backdoor.Java.QRat.gen
  327. icap: X-Response-Info: blocked
  328. icap: X-Virus-ID: no threats
  329. icap: X-Response-Info: blocked
  330. icap: X-Response-Info: passed
  331. http: HTTP/1.1 403 Forbidden
  332. Kaspersky Scan Engine 2.0 (ICAP mode)
  333. icap: X-Virus-ID: EICAR-Test-File
  334. http: HTTP/1.0 403 Forbidden
  335. Trend Micro Strings:
  336. icap: X-Virus-ID: Trojan.W97M.POWLOAD.SMTHF1
  337. icap: X-Infection-Found: Type=0; Resolution=2; Threat=Trojan.W97M.POWLOAD.SMTHF1;
  338. http: HTTP/1.1 403 Forbidden (TMWS Blocked)
  339. http: HTTP/1.1 403 Forbidden
  340. F-Secure Internet Gatekeeper Strings:
  341. icap: X-FSecure-Scan-Result: infected
  342. icap: X-FSecure-Infection-Name: "Malware.W97M/Agent.32584203"
  343. icap: X-FSecure-Infected-Filename: "virus.doc"
  344. ESET File Security for Linux 7.0
  345. icap: X-Infection-Found: Type=0; Resolution=0; Threat=VBA/TrojanDownloader.Agent.JOA;
  346. icap: X-Virus-ID: Trojaner
  347. icap: X-Response-Info: Blocked
  348. McAfee Web Gateway 10/11 (Headers must be activated with personal extra Rules)
  349. icap: X-Virus-ID: EICAR test file
  350. icap: X-Media-Type: text/plain
  351. icap: X-Block-Result: 80
  352. icap: X-Block-Reason: Malware found
  353. icap: X-Block-Reason: Archive not supported
  354. icap: X-Block-Reason: Media Type (Block List)
  355. http: HTTP/1.0 403 VirusFound
  356. C-ICAP Squidclamav
  357. icap/http: X-Infection-Found: Type=0; Resolution=2; Threat={HEX}EICAR.TEST.3.UNOFFICIAL;
  358. icap/http: X-Virus-ID: {HEX}EICAR.TEST.3.UNOFFICIAL
  359. http: HTTP/1.0 307 Temporary Redirect
  360. ]] --
  361. -- Generic ICAP Headers
  362. if headers['x-infection-found'] then
  363. local _, _, icap_type, _, icap_threat = headers['x-infection-found']:find("Type=(.-); Resolution=(.-); Threat=(.-);$")
  364. -- Type=2 is typical for scan error returns
  365. if icap_type and icap_type == '2' then
  366. lua_util.debugm(rule.name, task,
  367. '%s: icap error X-Infection-Found: %s', rule.log_prefix, icap_threat)
  368. common.yield_result(task, rule, icap_threat, 0,
  369. 'fail', maybe_part)
  370. return true
  371. elseif icap_threat ~= nil then
  372. lua_util.debugm(rule.name, task,
  373. '%s: icap X-Infection-Found: %s', rule.log_prefix, icap_threat)
  374. threat_table_add(icap_threat, false)
  375. -- stupid workaround for unuseable x-infection-found header
  376. -- but also x-virus-name set (McAfee Web Gateway 9)
  377. elseif not icap_threat and headers['x-virus-name'] then
  378. threat_table_add(headers['x-virus-name'], true)
  379. else
  380. threat_table_add(headers['x-infection-found'], true)
  381. end
  382. elseif headers['x-virus-name'] and headers['x-virus-name'] ~= "no threats" then
  383. lua_util.debugm(rule.name, task,
  384. '%s: icap X-Virus-Name: %s', rule.log_prefix, headers['x-virus-name'])
  385. threat_table_add(headers['x-virus-name'], true)
  386. elseif headers['x-virus-id'] and headers['x-virus-id'] ~= "no threats" then
  387. lua_util.debugm(rule.name, task,
  388. '%s: icap X-Virus-ID: %s', rule.log_prefix, headers['x-virus-id'])
  389. threat_table_add(headers['x-virus-id'], true)
  390. -- FSecure X-Headers
  391. elseif headers['x-fsecure-scan-result'] and headers['x-fsecure-scan-result'] ~= "clean" then
  392. local infected_filename = ""
  393. local infection_name = "-unknown-"
  394. if headers['x-fsecure-infected-filename'] then
  395. infected_filename = string.gsub(headers['x-fsecure-infected-filename'], '[%s"]', '')
  396. end
  397. if headers['x-fsecure-infection-name'] then
  398. infection_name = string.gsub(headers['x-fsecure-infection-name'], '[%s"]', '')
  399. end
  400. lua_util.debugm(rule.name, task,
  401. '%s: icap X-FSecure-Infection-Name (X-FSecure-Infected-Filename): %s (%s)',
  402. rule.log_prefix, infection_name, infected_filename)
  403. threat_table_add(infection_name, true)
  404. -- McAfee Web Gateway manual extra headers
  405. elseif headers['x-mwg-block-reason'] and headers['x-mwg-block-reason'] ~= "" then
  406. threat_table_add(headers['x-mwg-block-reason'], false)
  407. -- Sophos SAVDI special http headers
  408. elseif headers['x-blocked'] and headers['x-blocked'] ~= "" then
  409. threat_table_add(headers['x-blocked'], false)
  410. elseif headers['x-block-reason'] and headers['x-block-reason'] ~= "" then
  411. threat_table_add(headers['x-block-reason'], false)
  412. -- last try HTTP [4]xx return
  413. elseif headers.http and string.find(headers.http, '^HTTP%/[12]%.. [4]%d%d') then
  414. threat_table_add(
  415. string.format("pseudo-virus (blocked): %s", string.gsub(headers.http, 'HTTP%/[12]%.. ', '')), false)
  416. elseif rule.use_http_3xx_as_threat and
  417. headers.http and
  418. string.find(headers.http, '^HTTP%/[12]%.. [3]%d%d')
  419. then
  420. threat_table_add(
  421. string.format("pseudo-virus (redirect): %s",
  422. string.gsub(headers.http, 'HTTP%/[12]%.. ', '')), false)
  423. end
  424. if #threat_table > 0 then
  425. common.yield_result(task, rule, threat_table, rule.default_score, nil, maybe_part)
  426. common.save_cache(task, digest, rule, threat_table, rule.default_score, maybe_part)
  427. return true
  428. else
  429. return false
  430. end
  431. end
  432. local function icap_r_respond_http_cb(err_m, data, connection)
  433. if err_m or connection == nil then
  434. icap_requery(err_m, "icap_r_respond_http_cb")
  435. else
  436. local result = tostring(data)
  437. local icap_http_headers = result_header_table(result) or {}
  438. -- Find HTTP/[12].x [234]xx response
  439. if icap_http_headers.http and string.find(icap_http_headers.http, 'HTTP%/[12]%.. [234]%d%d') then
  440. local icap_http_header_result = icap_parse_result(icap_http_headers)
  441. if icap_http_header_result then
  442. -- Threat found - close connection
  443. connection:close()
  444. else
  445. common.save_cache(task, digest, rule, 'OK', 0, maybe_part)
  446. common.log_clean(task, rule)
  447. end
  448. else
  449. rspamd_logger.errx(task, '%s: unhandled response |%s|',
  450. rule.log_prefix, string.gsub(result, "\r\n", ", "))
  451. common.yield_result(task, rule, string.format('unhandled icap response: %s', icap_http_headers.icap),
  452. 0.0, 'fail', maybe_part)
  453. end
  454. end
  455. end
  456. local function icap_r_respond_cb(err_m, data, connection)
  457. if err_m or connection == nil then
  458. icap_requery(err_m, "icap_r_respond_cb")
  459. else
  460. local result = tostring(data)
  461. local icap_headers = result_header_table(result) or {}
  462. -- Find ICAP/1.x 2xx response
  463. if icap_headers.icap and string.find(icap_headers.icap, 'ICAP%/1%.. 2%d%d') then
  464. local icap_header_result = icap_parse_result(icap_headers)
  465. if icap_header_result then
  466. -- Threat found - close connection
  467. connection:close()
  468. elseif not icap_header_result
  469. and rule.use_http_result_header
  470. and icap_headers.encapsulated
  471. and not string.find(icap_headers.encapsulated, 'null%-body=0')
  472. then
  473. -- Try to read encapsulated HTTP Headers
  474. lua_util.debugm(rule.name, task, '%s: no ICAP virus header found - try HTTP headers',
  475. rule.log_prefix)
  476. connection:add_read(icap_r_respond_http_cb, '\r\n\r\n')
  477. else
  478. connection:close()
  479. common.save_cache(task, digest, rule, 'OK', 0, maybe_part)
  480. common.log_clean(task, rule)
  481. end
  482. elseif icap_headers.icap and string.find(icap_headers.icap, 'ICAP%/1%.. [45]%d%d') then
  483. -- Find ICAP/1.x 5/4xx response
  484. --[[
  485. Symantec String:
  486. ICAP/1.0 539 Aborted - No AV scanning license
  487. SquidClamAV/C-ICAP:
  488. ICAP/1.0 500 Server error
  489. Eset:
  490. ICAP/1.0 405 Forbidden
  491. TrendMicro:
  492. ICAP/1.0 400 Bad request
  493. McAfee:
  494. ICAP/1.0 418 Bad composition
  495. ]]--
  496. rspamd_logger.errx(task, '%s: ICAP ERROR: %s', rule.log_prefix, icap_headers.icap)
  497. common.yield_result(task, rule, icap_headers.icap, 0.0,
  498. 'fail', maybe_part)
  499. return false
  500. else
  501. rspamd_logger.errx(task, '%s: unhandled response |%s|',
  502. rule.log_prefix, string.gsub(result, "\r\n", ", "))
  503. common.yield_result(task, rule, string.format('unhandled icap response: %s', icap_headers.icap),
  504. 0.0, 'fail', maybe_part)
  505. end
  506. end
  507. end
  508. local function icap_w_respond_cb(err_m, connection)
  509. if err_m or connection == nil then
  510. icap_requery(err_m, "icap_w_respond_cb")
  511. else
  512. connection:add_read(icap_r_respond_cb, '\r\n\r\n')
  513. end
  514. end
  515. local function icap_r_options_cb(err_m, data, connection)
  516. if err_m or connection == nil then
  517. icap_requery(err_m, "icap_r_options_cb")
  518. else
  519. local icap_headers = result_header_table(tostring(data))
  520. if icap_headers.icap and string.find(icap_headers.icap, 'ICAP%/1%.. 2%d%d') then
  521. if icap_headers['methods'] and string.find(icap_headers['methods'], 'RESPMOD') then
  522. -- Allow "204 No Content" responses
  523. -- https://datatracker.ietf.org/doc/html/rfc3507#section-4.6
  524. if icap_headers['allow'] and string.find(icap_headers['allow'], '204') then
  525. add_respond_header('Allow', '204')
  526. end
  527. if rule.x_client_header then
  528. local client = task:get_from_ip()
  529. if client then
  530. add_respond_header('X-Client-IP', client:to_string())
  531. end
  532. end
  533. -- F-Secure extra headers
  534. if icap_headers['server'] and string.find(icap_headers['server'], 'f-secure icap server') then
  535. if rule.x_rcpt_header then
  536. local rcpt_to = task:get_principal_recipient()
  537. if rcpt_to then
  538. add_respond_header('X-Rcpt-To', rcpt_to)
  539. end
  540. end
  541. if rule.x_from_header then
  542. local mail_from = task:get_principal_recipient()
  543. if mail_from and mail_from[1] then
  544. add_respond_header('X-Rcpt-To', mail_from[1].addr)
  545. end
  546. end
  547. end
  548. if icap_headers.connection and icap_headers.connection:lower() == 'close' then
  549. lua_util.debugm(rule.name, task, '%s: OPTIONS request Connection: %s - using new connection',
  550. rule.log_prefix, icap_headers.connection)
  551. connection:close()
  552. tcp_options.callback = icap_w_respond_cb
  553. tcp_options.data = get_respond_query()
  554. tcp.request(tcp_options)
  555. else
  556. connection:add_write(icap_w_respond_cb, get_respond_query())
  557. end
  558. else
  559. rspamd_logger.errx(task, '%s: RESPMOD method not advertised: Methods: %s',
  560. rule.log_prefix, icap_headers['methods'])
  561. common.yield_result(task, rule, 'NO RESPMOD', 0.0,
  562. 'fail', maybe_part)
  563. end
  564. else
  565. rspamd_logger.errx(task, '%s: OPTIONS query failed: %s',
  566. rule.log_prefix, icap_headers.icap or "-")
  567. common.yield_result(task, rule, 'OPTIONS query failed', 0.0,
  568. 'fail', maybe_part)
  569. end
  570. end
  571. end
  572. if err or conn == nil then
  573. icap_requery(err, "options_request")
  574. else
  575. conn:add_read(icap_r_options_cb, '\r\n\r\n')
  576. end
  577. end
  578. tcp_options.task = task
  579. tcp_options.stop_pattern = '\r\n'
  580. tcp_options.read = false
  581. tcp_options.timeout = rule.timeout
  582. tcp_options.callback = icap_callback
  583. tcp_options.data = options_request
  584. if rule.ssl then
  585. tcp_options.ssl = true
  586. if rule.no_ssl_verify then
  587. tcp_options.no_ssl_verify = true
  588. end
  589. end
  590. tcp_options.host = addr:to_string()
  591. tcp_options.port = addr:get_port()
  592. tcp_options.upstream = upstream
  593. tcp.request(tcp_options)
  594. end
  595. if common.condition_check_and_continue(task, content, rule, digest,
  596. icap_check_uncached, maybe_part) then
  597. return
  598. else
  599. icap_check_uncached()
  600. end
  601. end
  602. return {
  603. type = { N, 'virus', 'virus', 'scanner' },
  604. description = 'generic icap antivirus',
  605. configure = icap_config,
  606. check = icap_check,
  607. name = N
  608. }