├── .gitignore ├── LICENSE ├── README.md ├── appveyor.yml ├── misc ├── deps.bat └── ipsecspylog.bat ├── rockspecs ├── lua-cjson-2.1.0-1.rockspec ├── luuid-20120501-2.rockspec ├── luuid-20120509-2.rockspec └── spylog-scm-0.rockspec └── src ├── action ├── init.lua ├── lib │ └── spylog │ │ ├── actiondb.lua │ │ └── actions │ │ ├── chain.lua │ │ ├── growl.lua │ │ ├── mail.lua │ │ └── spawn.lua └── main.lua ├── config ├── actions │ ├── advfirewall.lua │ ├── badips.lua │ ├── growl.lua │ ├── ipsec.lua │ └── mail.lua ├── filters │ ├── freeswitch.lua │ ├── fusionpbx.lua │ ├── radmin.lua │ ├── rdp-nxlog.lua │ ├── rdp.lua │ ├── syslog_http_request.lua │ └── tshark.lua ├── jails │ ├── access.lua │ └── voip.lua ├── sources │ ├── freeswitch.lua │ ├── nxlog.lua │ └── tshark.lua └── spylog.lua ├── filter ├── init.lua ├── lib │ └── spylog │ │ ├── eventlog.lua │ │ ├── filemon.lua │ │ ├── filter │ │ ├── manager.lua │ │ └── regex.lua │ │ ├── monitor │ │ ├── esl.lua │ │ ├── eventlog.lua │ │ ├── file.lua │ │ ├── net.lua │ │ ├── process.lua │ │ ├── syslog.lua │ │ └── trap.lua │ │ ├── syslog.lua │ │ └── trap.lua └── main.lua ├── jail ├── init.lua ├── lib │ └── spylog │ │ └── TimeCounters.lua └── main.lua ├── lib └── spylog │ ├── args.lua │ ├── cfilter.lua │ ├── cfilter │ ├── acl.lua │ ├── base.lua │ ├── geoip.lua │ ├── list.lua │ ├── prefix.lua │ └── regex.lua │ ├── config.lua │ ├── exit.lua │ ├── iputil.lua │ ├── log.lua │ ├── spawn.lua │ ├── var.lua │ └── version.lua └── spylog ├── init.lua └── main.lua /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # logs from debug runs 3 | 4 | src/spylog/data/ 5 | src/spylog/logs/ 6 | src/action/data/ 7 | src/action/logs/ 8 | src/filter/logs/ 9 | src/jail/logs/ 10 | 11 | # Binaries 12 | 13 | *.exe 14 | *.dll 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2017 Alexey Melnichuk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SpyLog 2 | 3 | ## Execute actions based on log records 4 | 5 | [![SpyLog-x86-0.0.2.exe](https://img.shields.io/badge/0.0.2-x86-blue.svg)](https://github.com/moteus/lua-spylog/releases/download/v0.0.2/SpyLog-x86-0.0.2.exe) 6 | [![SpyLog-x64-0.0.2.exe](https://img.shields.io/badge/0.0.2-x64-blue.svg)](https://github.com/moteus/lua-spylog/releases/download/v0.0.2/SpyLog-x64-0.0.2.exe) 7 | 8 | ----------------------------------------------------------- 9 | 10 | The main goal of this project is provide [fail2ban](http://www.fail2ban.org) functionality to Windows. 11 | 12 | ## Install/Start 13 | The `lua-spylog` consist of three services. 14 | 15 | * filter - read logs from sources and extract date(optional) and IP and send them to `jail` service. 16 | * jail - read messages from `filter` service and support time couter. If some counter is reached to 17 | `maxretry` then jail send message to `action` service. 18 | * action - read messages from `jail` service and support queue of actions to be done. When it recv new 19 | message it push 2 new action to queue (ban and unban). Queue is persistent. 20 | 21 | All services can be run as separate process or as thread in one multithreaded process. 22 | To run spylog as Windows service you can use [LuaService](https://github.com/moteus/luaservice). 23 | 24 | For Windows there exists installer which allows install SpyLog and all dependencies. You can download 25 | it form [Releases](https://github.com/moteus/lua-spylog/releases) page. 26 | 27 | ## Configuration 28 | 29 | ### Detect auth fail on FreeSWITCH system 30 | ```Lua 31 | -- config/sources/freeswitch.lua 32 | SOURCE{"freeswitch", 33 | "esl:ClueCon@127.0.0.1:8021", 34 | level = 'WARNING'; 35 | } 36 | ``` 37 | ```Lua 38 | -- config/filters/freeswitch.lua 39 | FILTER{ "freeswitch-auth-fail"; 40 | enabled = true; 41 | source = "freeswitch"; 42 | failregex = { 43 | "^(%d%d%d%d%-%d%d%-%d%d %d%d:%d%d:%d%d%.%d+) %[WARNING%] sofia_reg.c:%d+ SIP auth failure %([A-Z]+%) on sofia profile %'[^']+%' for %[.-%] from ip ([0-9.]+)%s*$"; 44 | "^(%d%d%d%d%-%d%d%-%d%d %d%d:%d%d:%d%d%.%d+) %[WARNING%] sofia.c:%d+ IP ([0-9.]+) Rejected by acl \"[^\"]*\"%s*$"; 45 | } 46 | }; 47 | ``` 48 | ```Lua 49 | -- config/jails/freeswitch.lua 50 | JAIL{"voip-auth-fail"; 51 | enabled = true; 52 | filter = {"freeswitch-auth-fail"}; 53 | findtime = 600; 54 | maxretry = 3; 55 | bantime = 3600 * 24; 56 | action = {"mail", "growl", "ipsec"}; 57 | } 58 | ``` 59 | 60 | ### Supported sources 61 | * Text log file 62 | * UDP raw server 63 | * SysLog UDP server (rfc3164 and rfc5424) 64 | * SNMP trap UDP server (allows handle Windows event logs) 65 | * EventLog (based on event trap) allows additional filters based on source names. 66 | * FreeSWITCH ESL TCP connection 67 | * TCP raw connection 68 | * Process stdout and/or stderr 69 | 70 | ### Filters 71 | 72 | #### Named captures 73 | By default filter names first capure as `date` and second one as `host`. 74 | If there only one capture then `date` set as current timestamp and capture names as `host`. 75 | It is possible to assign names to captures using `capture` array. 76 | ```Lua 77 | FILTER{'nginx-404', 78 | capture = {'host', 'date'}; -- we have to swap `date` and `host` 79 | failregex = '^([0-9.]+) %- %- %[(.-)%].-GET.-HTTP.- 404'; 80 | } 81 | ``` 82 | Also it possible add any other captures. They will be send to jails as whell. 83 | 84 | #### Ignore regex 85 | Filters support `ignoreregex` field to exclude records which already matched by `failregex` 86 | 87 | #### Exclude IP 88 | Filters support `exclude` array which allows exclude some IP and networks. 89 | 90 | ### Jails 91 | Each jail is just array of counters with some expire time. 92 | 93 | #### Counter types 94 | Currently supports this counter types 95 | 96 | * `incremental` increment to one for each filter message 97 | * `accumulate` get increment value from filter message. 98 | Can be used e.g. to calculate total calls duration in some VOIP system. 99 | * `fixed` just return value from filter message. 100 | Can be used e.g. to monitor max call duration for calls in some VOIP system. 101 | 102 | By default `increment` type uses. 103 | 104 | #### Counter control values 105 | Each counter do count for some value (like `counter[id] = counter[id] + value`). 106 | To specify `id` field you can use `capture` field. By default it is `host`. 107 | To specify `value` you can use `value` field. There no default value for this. 108 | E.g. in voip system it may be need monitor each account and block them . 109 | 110 | ```Lua 111 | JAIL{ 112 | ... 113 | counter = { 114 | type = 'accumulate'; 115 | capture = 'account'; -- count total duration for each account 116 | value = 'duration'; -- what value use to increment. 117 | }; 118 | } 119 | ``` 120 | 121 | #### Capture filters 122 | It is also possible add some additional filter to `filters` and `jails`. 123 | 124 | Example 1. Add `black` list for user names for RDP service. 125 | ```Lua 126 | JAIL{"rdp-bad-user-access"; -- e.g. can ban after first attempt 127 | -- apply this jail only for specific user list 128 | cfilter = {"list", 129 | type = "allow", 130 | capture = "user", 131 | nocase = true, 132 | filter = { "admin", "guest", "user", "root"}; 133 | }; 134 | } 135 | ``` 136 | 137 | Example 2. Counts attempts to call only to some specific area codes. 138 | ```Lua 139 | JAIL{ 140 | -- count only calls to Cuba and Albania and exclude '192.168.123.22' host 141 | cfilter = { 142 | {'prefix', -- filter type 143 | type = 'allow', -- count if match 144 | capture = 'number', -- capture name to filter 145 | filter = { -- filter rules 146 | '53', -- Cuba 147 | '355', -- Albania 148 | } 149 | }; 150 | 151 | {'acl', -- filter type 152 | type = 'deny', -- count if not match 153 | capture = 'host', -- capture name to filter 154 | filter = { -- filter rules 155 | '192.168.123.22', 156 | } 157 | }; 158 | } 159 | } 160 | ``` 161 | 162 | Example 3. Apply jail to some countries only. 163 | ```Lua 164 | JAIL{"rdp-bad-country-access"; -- e.g. can ban after first attempt 165 | -- apply this jail for all counties except Russia and North America 166 | cfilter = {"geoip", 167 | type = "deny", 168 | filter = { 'ru', continent = {'na'} }; 169 | }; 170 | } 171 | ``` 172 | 173 | Each capture filter should have name as first element, `capture` and `filter` fields. 174 | Currently support `prefix`, `acl`, `regex`, `list` and `geoip` filters. 175 | `capture` field specify what value from capture should be used in this fileter. 176 | `filter` is set of rules had specific format for each type of filter. 177 | `prefix` filter should have `filter` field as array of prefixes of file name. 178 | `acl` filter should have `filter` field as array of IP and/or CIDR. 179 | `regex` filter should have `filter` field as string/array of strings. 180 | `list` filter should have `list` field as string/array of strings. 181 | 182 | 183 | ## Dependencies 184 | - [bit32](https://luarocks.org/modules/siffiejoe/bit32) 185 | - [date](https://luarocks.org/modules/tieske/date) 186 | - [lluv](https://luarocks.org/modules/moteus/lluv) 187 | - [lluv-poll-zmq](https://luarocks.org/modules/moteus/lluv-poll-zmq) 188 | - [lpeg](https://luarocks.org/modules/gvvaughan/lpeg) 189 | - [Lrexlib-PCRE](https://luarocks.org/modules/rrt/lrexlib-pcre) 190 | - [lua-cjson](https://luarocks.org/modules/luarocks/lua-cjson) 191 | - [lua-llthreads2](https://luarocks.org/modules/moteus/lua-llthreads2) 192 | - [lua-log](https://luarocks.org/modules/moteus/lua-log) 193 | - [lua-path](https://luarocks.org/modules/moteus/lua-path) 194 | - [Lua-Sqlite3](https://luarocks.org/modules/moteus/sqlite3) 195 | - [LuaFileSystem](https://luarocks.org/modules/hisham/luafilesystem) 196 | - [luuid](https://luarocks.org/modules/luarocks/luuid) 197 | - [lzmq](https://luarocks.org/modules/moteus/lzmq) 198 | - [StackTracePlus](https://luarocks.org/modules/ignacio/stacktraceplus) 199 | - [LuaService](https://luarocks.org/modules/moteus/luaservice) 200 | - [environ](https://luarocks.org/modules/moteus/environ) 201 | 202 | ### To support `mail` action 203 | - [lluv-ssl](https://luarocks.org/modules/moteus/lluv-ssl) 204 | - [sendmail](https://luarocks.org/modules/moteus/sendmail) 205 | - [try](https://luarocks.org/modules/moteus/try) 206 | 207 | ### To support `growl` action 208 | - [gntp](https://luarocks.org/modules/moteus/gntp) 209 | - [openssl](https://luarocks.org/modules/zhaozg/openssl) 210 | 211 | ### To support `esl` source type 212 | - [lluv-esl](https://luarocks.org/modules/moteus/lluv-esl) 213 | 214 | ### To support `prefix` capture filter 215 | - [prefix_tree](https://luarocks.org/modules/moteus/prefix_tree) 216 | 217 | ### To support `geoip` capture filter 218 | - [mmdblua](https://luarocks.org/modules/daurnimator/mmdblua) 219 | - [compat53](https://luarocks.org/modules/siffiejoe/compat53) 220 | - [lua-lru](https://luarocks.org/modules/starius/lua-lru) (optional) 221 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 0.0.1.{build} 2 | 3 | # Start builds on tags only (GitHub and BitBucket) 4 | skip_non_tags: true 5 | 6 | init: 7 | - git config --global core.autocrlf true 8 | 9 | before_build: 10 | - choco install -y curl InnoSetup 11 | - set PATH="C:\Program Files\7-Zip";%PATH% 12 | - set PATH="C:\Program Files (x86)\Inno Setup 5";%PATH% 13 | - set PATH=C:\Python27\Scripts;%PATH% 14 | - git clone https://github.com/moteus/lua-spylog-installer installer 15 | 16 | build_script: 17 | - cd .\installer\win 18 | - iscc spylog.iss /O"%APPVEYOR_BUILD_FOLDER%" /DArch=x86 /DLuaVer=5.1 /DSpyLogGit=..\.. 19 | - iscc spylog.iss /O"%APPVEYOR_BUILD_FOLDER%" /DArch=x64 /DLuaVer=5.1 /DSpyLogGit=..\.. 20 | 21 | artifacts: 22 | - path: SpyLog-x86*.exe 23 | name: SpyLog-x86 24 | 25 | - path: SpyLog-x64*.exe 26 | name: SpyLog-x64 27 | 28 | deploy: 29 | - provider: GitHub 30 | auth_token: 31 | secure: quhtxEmz8RrBAwBB5rZ675IUuhqjiDxTNouM5rEnUTeT7Gj3Lr4qPxuRcF60dqzW 32 | artifact: SpyLog-x86,SpyLog-x64 33 | draft: false 34 | prerelease: true 35 | on: 36 | appveyor_repo_tag: true 37 | -------------------------------------------------------------------------------- /misc/deps.bat: -------------------------------------------------------------------------------- 1 | ::================================================================== 2 | :: 3 | :: Install all dependencies on Windows using LuaRocks 4 | :: It requires that you already have all external deps 5 | :: (like libzmq, libuv etc.) installed in your system 6 | :: and LuaRocks can use them to install Lua modules. 7 | :: 8 | :: Tested with https://github.com/moteus/lua-windows-environment 9 | :: 10 | :: > luaenv x86 5.1 && deps 5.1 11 | :: > luaenv x86 5.2 && deps 5.2 12 | :: > luaenv x86 5.3 && deps 5.3 13 | :: 14 | ::================================================================== 15 | 16 | @echo off && setlocal 17 | 18 | set LUA_VER=%1 19 | set TREE=%2 20 | set ROOT= 21 | 22 | if exist ..\README.md if exist ..\src set ROOT=.. 23 | 24 | if exist .\README.md if exist .\src set ROOT=. 25 | 26 | if "%ROOT%" == "" ( 27 | echo Please run this file from project root directory 28 | EXIT /B 1 29 | ) 30 | 31 | if "%LUA_VER%" == "" (set LUA_VER=5.1) 32 | 33 | if "%TREE%" == "" (set TREE=spylog-%LUA_VER%) 34 | 35 | set LR=luarocks-%LUA_VER% 36 | 37 | if "%LUA_VER%" == "5.1" (set UUID_VER=20120501) else (set UUID_VER=20120509) 38 | 39 | ::================================================================== 40 | :: Custom rockspecs for build with MSVC 41 | ::================================================================== 42 | 43 | call %LR% show lua-cjson --tree %TREE% || call %LR% --tree %TREE% install %ROOT%\rockspecs\lua-cjson-2.1.0-1.rockspec 44 | call %LR% show luuid --tree %TREE% || call %LR% --tree %TREE% install %ROOT%\rockspecs\luuid-%UUID_VER%-2.rockspec 45 | 46 | ::================================================================== 47 | :: Not released yet 48 | ::================================================================== 49 | 50 | :: install deps for LuaService by hand from main server 51 | call %LR% show luasocket --tree %TREE% || call %LR% --tree %TREE% install luasocket 52 | call %LR% show luafilesystem --tree %TREE% || call %LR% --tree %TREE% install luafilesystem 53 | :: install LuaService form dev server 54 | call %LR% show luaservice --tree %TREE% || call %LR% --tree %TREE% install luaservice --server=http://luarocks.org/dev 55 | 56 | :: install deps for lluv-esl by hand from main server 57 | call %LR% show eventemitter --tree %TREE% || call %LR% --tree %TREE% install eventemitter 58 | call %LR% show lluv --tree %TREE% || call %LR% --tree %TREE% install lluv 59 | call %LR% show luaexpat --tree %TREE% || call %LR% --tree %TREE% install luaexpat 60 | :: install lluv-esl form dev server 61 | call %LR% show lluv-esl --tree %TREE% || call %LR% --tree %TREE% install lluv-esl --server=http://luarocks.org/dev 62 | 63 | :: install openssl form dev server 64 | call %LR% show openssl --tree %TREE% || call %LR% --tree %TREE% install openssl --server=http://luarocks.org/dev 65 | 66 | ::================================================================== 67 | :: install rest deps 68 | ::================================================================== 69 | 70 | call %LR% --tree %TREE% --only-deps install %ROOT%\rockspecs\spylog-scm-0.rockspec -------------------------------------------------------------------------------- /misc/ipsecspylog.bat: -------------------------------------------------------------------------------- 1 | ::========================================================= 2 | :: Basic way to manage IPSec SpyLog filters 3 | :: Install/Uninstall default policy and filters 4 | :: Create/Assign filters to policy 5 | :: Clean all host in filter 6 | :: Add/Remove host to specific filter 7 | :: List of IP for specific filter 8 | ::========================================================= 9 | 10 | @echo off && setlocal 11 | 12 | set DummyIP=192.168.251.136 13 | set SpyLogActionName=SpyLogBlock 14 | set SpyLogPolicyName=SpyLogBlock 15 | set SpyLogFilterName=SpyLogBlock 16 | 17 | if "%1"=="" goto :usage 18 | if "%1"=="-help" goto :usage 19 | 20 | if not "%1"=="install" if not "%1"=="uninstall" if not "%1"=="filter" if not "%1"=="host" ( 21 | echo ERROR: Unknown action/object: %1 22 | goto :usage 23 | ) 24 | 25 | :: what need to do (install, uninstall, filter, host) 26 | SET object=%1 27 | SET policy= 28 | SET filter= 29 | SET host= 30 | SET port= 31 | SET protocol= 32 | SET mask= 33 | SET skip_policy=false 34 | SET skip_filter=false 35 | SHIFT 36 | 37 | if "%object%" == "filter" goto :filter_args 38 | if "%object%" == "host" goto :filter_ip_args 39 | 40 | ::--------------------------------------------------------- 41 | :: decode args for install/uninstall 42 | ::--------------------------------------------------------- 43 | :install_args 44 | IF NOT "%1"=="" ( 45 | IF "%1"=="-policy" ( 46 | SET policy=%2 47 | SHIFT && SHIFT 48 | GOTO :install_args 49 | ) 50 | IF "%1"=="-filter" ( 51 | SET filter=%2 52 | SHIFT && SHIFT 53 | GOTO :install_args 54 | ) 55 | IF "%1"=="-skip-policy" ( 56 | SET skip_policy=true 57 | SHIFT 58 | GOTO :install_args 59 | ) 60 | IF "%1"=="-skip-filter" ( 61 | SET skip_filter=true 62 | SHIFT 63 | GOTO :install_args 64 | ) 65 | echo ERROR: Unknown key for %object% command: %1 66 | goto :usage 67 | ) 68 | 69 | if "%object%" == "install" goto :install 70 | if "%object%" == "uninstall" goto :uninstall 71 | 72 | ::--------------------------------------------------------- 73 | :: decode args for manage filter 74 | ::--------------------------------------------------------- 75 | :filter_args 76 | if not "%1"=="add" if not "%1"=="remove" if not "%1"=="list" if not "%1"=="clean" ( 77 | echo ERROR: Unknown action for filter object: %1 78 | goto :usage 79 | ) 80 | SET action=%1 81 | SHIFT 82 | 83 | SET filter=%1 84 | if "%filter:~0,1%"=="-" ( 85 | SET filter= 86 | ) 87 | if not "%filter%"=="" ( 88 | SHIFT 89 | ) 90 | 91 | :filter_args_loop 92 | IF NOT "%1"=="" ( 93 | IF "%1"=="-policy" ( 94 | SET policy=%2 95 | SHIFT && SHIFT 96 | GOTO :filter_args_loop 97 | ) 98 | IF "%1"=="-filter" ( 99 | SET filter=%2 100 | SHIFT && SHIFT 101 | GOTO :filter_args_loop 102 | ) 103 | echo ERROR: Unknown key for filter command: %1 104 | goto :usage 105 | ) 106 | 107 | if "%action%" == "add" goto :add_filter 108 | if "%action%" == "remove" goto :remove_filter 109 | if "%action%" == "list" goto :list_filter 110 | if "%action%" == "clean" goto :clean_filter 111 | 112 | ::--------------------------------------------------------- 113 | :: decode args for add/remove ip to filter 114 | ::--------------------------------------------------------- 115 | :filter_ip_args 116 | if not "%1"=="add" if not "%1"=="remove" ( 117 | echo ERROR: Unknown action for host object: %1 118 | goto :usage 119 | ) 120 | SET action=%1 121 | SHIFT 122 | 123 | SET host=%1 124 | if "%host:~0,1%"=="-" ( 125 | SET host= 126 | ) 127 | if not "%host%"=="" ( 128 | SHIFT 129 | ) 130 | 131 | 132 | :filter_ip_args_loop 133 | IF NOT "%1"=="" ( 134 | IF "%1"=="-host" ( 135 | SET host=%2 136 | SHIFT && SHIFT 137 | GOTO :filter_ip_args_loop 138 | ) 139 | IF "%1"=="-filter" ( 140 | SET filter=%2 141 | SHIFT && SHIFT 142 | GOTO :filter_ip_args_loop 143 | ) 144 | IF "%1"=="-protocol" ( 145 | SET protocol=%2 146 | SHIFT && SHIFT 147 | GOTO :filter_ip_args_loop 148 | ) 149 | IF "%1"=="-port" ( 150 | SET port=%2 151 | SHIFT && SHIFT 152 | GOTO :filter_ip_args_loop 153 | ) 154 | IF "%1"=="-mask" ( 155 | SET mask=%2 156 | SHIFT && SHIFT 157 | GOTO :filter_ip_args_loop 158 | ) 159 | echo ERROR: Unknown key for host command: %1 160 | goto :usage 161 | ) 162 | 163 | :: if you whant use port then you have to define protocol 164 | if not "%port%"=="" if "%protocol%"=="" ( 165 | echo ERROR: no protocol, but port defined 166 | goto :usage 167 | ) 168 | 169 | :: if you define protocol you can set port to `0` that means any 170 | if "%port%"=="" if not "%protocol%"=="" ( 171 | SET port=0 172 | ) 173 | 174 | :: only this host 175 | if "%mask%"=="" ( 176 | SET mask=32 177 | ) 178 | 179 | if "%action%" == "add" goto :add_filter_ip 180 | if "%action%" == "remove" goto :remove_filter_ip 181 | 182 | :usage 183 | echo ipsecspylog install^|uninstall [-skip-policy] [-skip-filter] 184 | echo ipsecspylog filter add^|remove [[-filter] ^] [-policy ^] 185 | echo ipsecspylog filter list^|clean [[-filter] ^] 186 | echo ipsecspylog host add^|remove [[-host] ^] [-mask ^] [-protocol ^ [-port ^]] [-filter ^] 187 | 188 | goto :eof 189 | 190 | :install 191 | call:CreateAction 192 | if "%skip_policy%" == "false" call:CreatePolicy %policy% 193 | if "%skip_policy%" == "false" if "%skip_filter%" == "false" call:CreateFilter %filter% %policy% 194 | 195 | goto :eof 196 | 197 | :uninstall 198 | if "%skip_policy%" == "false" call:RemovePolicy %policy% 199 | if "%skip_filter%" == "false" call:RemoveFilter %filter% %policy% 200 | call:RemoveAction 201 | 202 | goto :eof 203 | 204 | :add_filter 205 | call:CreateFilter %filter% %policy% 206 | 207 | goto :eof 208 | 209 | :remove_filter 210 | call:RemoveFilter %filter% %policy% 211 | 212 | goto :eof 213 | 214 | :clean_filter 215 | call:RemoveFilter %filter% %policy% 216 | call:CreateFilter %filter% %policy% 217 | 218 | goto :eof 219 | 220 | :list_filter 221 | call:ListFilter %filter% 222 | 223 | goto :eof 224 | 225 | :add_filter_ip 226 | if "%port%"=="" ( 227 | call:AddFilterIP %host% %mask% %filter% 228 | ) 229 | if not "%port%"=="" ( 230 | call:AddFilterIPPort %host% %mask% %protocol% %port% %filter% 231 | ) 232 | 233 | goto :eof 234 | 235 | :remove_filter_ip 236 | if "%port%"=="" ( 237 | call:RemoveFilterIP %host% %mask% %filter% 238 | ) 239 | if not "%port%"=="" ( 240 | call:RemoveFilterIPPort %host% %mask% %protocol% %port% %filter% 241 | ) 242 | 243 | goto :eof 244 | 245 | endlocal 246 | 247 | 248 | :CreateAction 249 | setlocal 250 | set name=%~1 251 | set action=%~2 252 | if "%name%" == "" set name=%SpyLogActionName% 253 | if "%action%" == "" set action=block 254 | netsh ipsec static add filteraction name=%name% action=%action% 255 | endlocal 256 | goto :eof 257 | 258 | 259 | :RemoveAction 260 | setlocal 261 | set name=%~1 262 | if "%name%" == "" set name=%SpyLogActionName% 263 | netsh ipsec static delete filteraction name=%name% 264 | endlocal 265 | goto :eof 266 | 267 | 268 | :CreatePolicy 269 | setlocal 270 | set policy=%~1 271 | if "%policy%" == "" set policy=%SpyLogPolicyName% 272 | netsh ipsec static add policy name=%policy% assign=yes activatedefaultrule=no 273 | endlocal 274 | goto :eof 275 | 276 | 277 | :RemovePolicy 278 | setlocal 279 | set policy=%~1 280 | if "%policy%" == "" set policy=%SpyLogPolicyName% 281 | netsh ipsec static delete policy name=%policy% 282 | endlocal 283 | goto :eof 284 | 285 | 286 | :CreateFilter 287 | setlocal 288 | set filter=%~1 289 | set policy=%~2 290 | set action=%~3 291 | if "%filter%" == "" set filter=%SpyLogFilterName% 292 | if "%policy%" == "" set policy=%SpyLogPolicyName% 293 | if "%action%" == "" set action=%SpyLogActionName% 294 | 295 | set rule=%policy%-%filter% 296 | 297 | netsh ipsec static add filter filterlist=%filter% srcaddr=%DummyIP% dstaddr=me 298 | netsh ipsec static add rule name=%rule% policy=%policy% filterlist=%filter% filteraction=%action% 299 | netsh ipsec static delete filter filterlist=%filter% srcaddr=%DummyIP% dstaddr=Me 300 | endlocal 301 | goto :eof 302 | 303 | 304 | :RemoveFilter 305 | setlocal 306 | set filter=%~1 307 | set policy=%~2 308 | if "%filter%" == "" set filter=%SpyLogFilterName% 309 | if "%policy%" == "" set policy=%SpyLogPolicyName% 310 | 311 | set rule=%policy%-%filter% 312 | 313 | netsh ipsec static delete rule name=%rule% policy=%policy% 314 | netsh ipsec static delete filterlist name=%filter% 315 | endlocal 316 | goto :eof 317 | 318 | 319 | :ListFilter 320 | setlocal 321 | set filter=%~1 322 | if "%filter%" == "" set filter=%SpyLogFilterName% 323 | 324 | netsh ipsec static show filterlist %filter% level=verbose format=table 325 | endlocal 326 | goto :eof 327 | 328 | 329 | :AddFilterIP 330 | setlocal 331 | set host=%~1 332 | set mask=%~2 333 | set filter=%~3 334 | if "%filter%" == "" set filter=%SpyLogFilterName% 335 | 336 | netsh ipsec static add filter filterlist=%filter% srcaddr=%host% srcmask=%mask% dstaddr=me 337 | endlocal 338 | goto :eof 339 | 340 | :AddFilterIPPort 341 | setlocal 342 | set host=%~1 343 | set mask=%~2 344 | set protocol=%~3 345 | set port=%~4 346 | set filter=%~5 347 | if "%filter%" == "" set filter=%SpyLogFilterName% 348 | 349 | netsh ipsec static add filter filterlist=%filter% srcaddr=%host% srcmask=%mask% protocol=%protocol% dstport=%port% dstaddr=me 350 | endlocal 351 | goto :eof 352 | 353 | :RemoveFilterIP 354 | setlocal 355 | set host=%~1 356 | set mask=%~2 357 | set filter=%~3 358 | if "%filter%" == "" set filter=%SpyLogFilterName% 359 | 360 | netsh ipsec static delete filter filterlist=%filter% srcaddr=%host% srcmask=%mask% dstaddr=me 361 | endlocal 362 | goto :eof 363 | 364 | :RemoveFilterIPPort 365 | setlocal 366 | set host=%~1 367 | set mask=%~2 368 | set protocol=%~3 369 | set port=%~4 370 | set filter=%~5 371 | if "%filter%" == "" set filter=%SpyLogFilterName% 372 | 373 | netsh ipsec static add filter filterlist=%filter% srcaddr=%host% srcmask=%mask% protocol=%protocol% dstport=%port% dstaddr=me 374 | endlocal 375 | goto :eof 376 | -------------------------------------------------------------------------------- /rockspecs/lua-cjson-2.1.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-cjson" 2 | version = "2.1.0-1" 3 | 4 | source = { 5 | url = "http://www.kyne.com.au/~mark/software/download/lua-cjson-2.1.0.zip", 6 | } 7 | 8 | description = { 9 | summary = "A fast JSON encoding/parsing module", 10 | detailed = [[ 11 | The Lua CJSON module provides JSON support for Lua. It features: 12 | - Fast, standards compliant encoding/parsing routines 13 | - Full support for JSON with UTF-8, including decoding surrogate pairs 14 | - Optional run-time support for common exceptions to the JSON specification 15 | (infinity, NaN,..) 16 | - No dependencies on other libraries 17 | ]], 18 | homepage = "http://www.kyne.com.au/~mark/software/lua-cjson.php", 19 | license = "MIT" 20 | } 21 | 22 | dependencies = { 23 | "lua >= 5.1" 24 | } 25 | 26 | build = { 27 | type = "builtin", 28 | modules = { 29 | cjson = { 30 | sources = { "lua_cjson.c", "strbuf.c", "fpconv.c" }, 31 | defines = { 32 | -- LuaRocks does not support platform specific configuration for Solaris. 33 | -- Uncomment the line below on Solaris platforms if required. 34 | -- "USE_INTERNAL_ISINF" 35 | } 36 | } 37 | }, 38 | install = { 39 | lua = { 40 | ["cjson.util"] = "lua/cjson/util.lua" 41 | }, 42 | bin = { 43 | json2lua = "lua/json2lua.lua", 44 | lua2json = "lua/lua2json.lua" 45 | } 46 | }, 47 | -- Override default build options (per platform) 48 | platforms = { 49 | win32 = { modules = { cjson = { defines = { 50 | "DISABLE_INVALID_NUMBERS" 51 | } } } } 52 | }, 53 | copy_directories = { "tests" }, 54 | patches = { 55 | ["pr1.diff"] = [[--- a/fpconv.c 56 | +++ b/fpconv.c 57 | @@ -35,6 +35,10 @@ 58 | 59 | #include "fpconv.h" 60 | 61 | +#ifdef _MSC_VER 62 | +#define snprintf _snprintf 63 | +#endif 64 | + 65 | /* Lua CJSON assumes the locale is the same for all threads within a 66 | * process and doesn't change after initialisation. 67 | * 68 | @@ -124,7 +128,7 @@ double fpconv_strtod(const char *nptr, char **endptr) 69 | /* Duplicate number into buffer */ 70 | if (buflen >= FPCONV_G_FMT_BUFSIZE) { 71 | /* Handle unusually large numbers */ 72 | - buf = malloc(buflen + 1); 73 | + buf = (char *)malloc(buflen + 1); 74 | if (!buf) { 75 | fprintf(stderr, "Out of memory"); 76 | abort(); 77 | @@ -196,10 +200,12 @@ int fpconv_g_fmt(char *str, double num, int precision) 78 | return len; 79 | } 80 | 81 | +#ifndef USE_INTERNAL_FPCONV 82 | void fpconv_init() 83 | { 84 | fpconv_update_locale(); 85 | } 86 | +#endif 87 | 88 | /* vi:ai et sw=4 ts=4: 89 | */ 90 | diff --git a/fpconv.h b/fpconv.h 91 | index 0124908..b3e2c3c 100644 92 | --- a/fpconv.h 93 | +++ b/fpconv.h 94 | @@ -6,6 +6,10 @@ 95 | * -1.7976931348623e+308 */ 96 | # define FPCONV_G_FMT_BUFSIZE 32 97 | 98 | +#ifdef _MSC_VER 99 | +#define inline 100 | +#endif 101 | + 102 | #ifdef USE_INTERNAL_FPCONV 103 | static inline void fpconv_init() 104 | { 105 | diff --git a/lua_cjson.c b/lua_cjson.c 106 | index c14a1c5..5ede8df 100644 107 | --- a/lua_cjson.c 108 | +++ b/lua_cjson.c 109 | @@ -40,7 +40,13 @@ 110 | #include 111 | #include 112 | #include 113 | + 114 | +#ifdef __cplusplus 115 | +#include 116 | +#else 117 | #include 118 | +#endif 119 | + 120 | #include 121 | 122 | #include "strbuf.h" 123 | @@ -59,6 +65,14 @@ 124 | #define isinf(x) (!isnan(x) && isnan((x) - (x))) 125 | #endif 126 | 127 | +#ifdef _MSC_VER 128 | +#define snprintf _snprintf 129 | +#define strncasecmp _strnicmp 130 | +#include 131 | +#define isnan(x) _isnan(x) 132 | +#define isinf(x) (!isnan(x) && isnan((x) - (x))) 133 | +#endif 134 | + 135 | #define DEFAULT_SPARSE_CONVERT 0 136 | #define DEFAULT_SPARSE_RATIO 2 137 | #define DEFAULT_SPARSE_SAFE 10 138 | @@ -193,7 +207,7 @@ static json_config_t *json_fetch_config(lua_State *l) 139 | { 140 | json_config_t *cfg; 141 | 142 | - cfg = lua_touserdata(l, lua_upvalueindex(1)); 143 | + cfg = (json_config_t *)lua_touserdata(l, lua_upvalueindex(1)); 144 | if (!cfg) 145 | luaL_error(l, "BUG: Unable to fetch CJSON configuration"); 146 | 147 | @@ -360,7 +374,7 @@ static int json_destroy_config(lua_State *l) 148 | { 149 | json_config_t *cfg; 150 | 151 | - cfg = lua_touserdata(l, 1); 152 | + cfg = (json_config_t *)lua_touserdata(l, 1); 153 | if (cfg) 154 | strbuf_free(&cfg->encode_buf); 155 | cfg = NULL; 156 | @@ -373,7 +387,7 @@ static void json_create_config(lua_State *l) 157 | json_config_t *cfg; 158 | int i; 159 | 160 | - cfg = lua_newuserdata(l, sizeof(*cfg)); 161 | + cfg = (json_config_t *)lua_newuserdata(l, sizeof(*cfg)); 162 | 163 | /* Create GC method to clean up strbuf */ 164 | lua_newtable(l); 165 | @@ -1407,7 +1421,13 @@ static int lua_cjson_safe_new(lua_State *l) 166 | return 1; 167 | } 168 | 169 | -int luaopen_cjson(lua_State *l) 170 | +#ifdef _MSC_VER 171 | +#define EXPORT_API __declspec(dllexport) 172 | +#else 173 | +#define EXPORT_API 174 | +#endif 175 | + 176 | +int EXPORT_API luaopen_cjson(lua_State *l) 177 | { 178 | lua_cjson_new(l); 179 | 180 | @@ -1421,7 +1441,7 @@ int luaopen_cjson(lua_State *l) 181 | return 1; 182 | } 183 | 184 | -int luaopen_cjson_safe(lua_State *l) 185 | +int EXPORT_API luaopen_cjson_safe(lua_State *l) 186 | { 187 | lua_cjson_safe_new(l); 188 | 189 | diff --git a/strbuf.c b/strbuf.c 190 | index f0f7f4b..147f27a 100644 191 | --- a/strbuf.c 192 | +++ b/strbuf.c 193 | @@ -58,7 +58,7 @@ void strbuf_init(strbuf_t *s, int len) 194 | s->reallocs = 0; 195 | s->debug = 0; 196 | 197 | - s->buf = malloc(size); 198 | + s->buf = (char *)malloc(size); 199 | if (!s->buf) 200 | die("Out of memory"); 201 | 202 | @@ -69,7 +69,7 @@ strbuf_t *strbuf_new(int len) 203 | { 204 | strbuf_t *s; 205 | 206 | - s = malloc(sizeof(strbuf_t)); 207 | + s = (strbuf_t *)malloc(sizeof(strbuf_t)); 208 | if (!s) 209 | die("Out of memory"); 210 | 211 | @@ -173,7 +173,7 @@ void strbuf_resize(strbuf_t *s, int len) 212 | } 213 | 214 | s->size = newsize; 215 | - s->buf = realloc(s->buf, s->size); 216 | + s->buf = (char *)realloc(s->buf, s->size); 217 | if (!s->buf) 218 | die("Out of memory"); 219 | s->reallocs++; 220 | @@ -221,12 +221,12 @@ void strbuf_append_fmt(strbuf_t *s, int len, const char *fmt, ...) 221 | void strbuf_append_fmt_retry(strbuf_t *s, const char *fmt, ...) 222 | { 223 | va_list arg; 224 | - int fmt_len, try; 225 | + int fmt_len, attempt; 226 | int empty_len; 227 | 228 | /* If the first attempt to append fails, resize the buffer appropriately 229 | * and try again */ 230 | - for (try = 0; ; try++) { 231 | + for (attempt = 0; ; attempt++) { 232 | va_start(arg, fmt); 233 | /* Append the new formatted string */ 234 | /* fmt_len is the length of the string required, excluding the 235 | @@ -238,7 +238,7 @@ void strbuf_append_fmt_retry(strbuf_t *s, const char *fmt, ...) 236 | 237 | if (fmt_len <= empty_len) 238 | break; /* SUCCESS */ 239 | - if (try > 0) 240 | + if (attempt > 0) 241 | die("BUG: length of formatted string changed"); 242 | 243 | strbuf_resize(s, s->length + fmt_len); 244 | diff --git a/strbuf.h b/strbuf.h 245 | index d861108..05b4878 100644 246 | --- a/strbuf.h 247 | +++ b/strbuf.h 248 | @@ -48,6 +48,10 @@ typedef struct { 249 | #define STRBUF_DEFAULT_INCREMENT -2 250 | #endif 251 | 252 | +#ifdef _MSC_VER 253 | +#define inline 254 | +#endif 255 | + 256 | /* Initialise */ 257 | extern strbuf_t *strbuf_new(int len); 258 | extern void strbuf_init(strbuf_t *s, int len);]]; 259 | } 260 | } 261 | 262 | -- vi:ai et sw=4 ts=4: 263 | -------------------------------------------------------------------------------- /rockspecs/luuid-20120501-2.rockspec: -------------------------------------------------------------------------------- 1 | package = "luuid" 2 | version = "20120501-2" 3 | source = { 4 | url = "http://www.tecgraf.puc-rio.br/~lhf/ftp/lua/5.1/luuid.tar.gz", 5 | md5 = "28273187c1f8176e98f959889d6abdbc", 6 | dir = "uuid" 7 | } 8 | description = { 9 | summary = "A library for UUID generation", 10 | detailed = [[ 11 | A library for generating universally unique identifiers based on 12 | libuuid, which is part of e2fsprogs. 13 | ]], 14 | homepage = "http://www.tecgraf.puc-rio.br/~lhf/ftp/lua/#luuid", 15 | license = "Public domain" 16 | } 17 | dependencies = { 18 | "lua ~> 5.1" 19 | } 20 | external_dependencies = { 21 | platforms = { 22 | unix = { 23 | LIBUUID = { 24 | header = "uuid/uuid.h", 25 | library = "libuuid.so" 26 | } 27 | } 28 | } 29 | } 30 | build = { 31 | type = "builtin", 32 | platforms = { 33 | win32 = { 34 | modules = { 35 | uuid = { 36 | libraries = { 37 | "rpcrt4", 38 | }, 39 | sources = { 40 | "luuid.c", 41 | "wuuid.c", 42 | } 43 | } 44 | } 45 | }, 46 | unix = { 47 | modules = { 48 | uuid = { 49 | libraries = { 50 | "uuid", 51 | }, 52 | sources = { 53 | "luuid.c", 54 | } 55 | } 56 | } 57 | } 58 | }; 59 | patches = { 60 | ["wuuid.diff"] = [[--- wuuid.h Tue Sep 25 02:25:11 2007 61 | +++ wuuid.h Sun Aug 21 00:36:43 2016 62 | @@ -19,7 +19,9 @@ 63 | #include 64 | #include 65 | #endif 66 | +#ifndef _MSC_VER 67 | #include 68 | +#endif 69 | 70 | /* 71 | * Fix declaration of uuid_t (possibly made in included]] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /rockspecs/luuid-20120509-2.rockspec: -------------------------------------------------------------------------------- 1 | package = "luuid" 2 | version = "20120509-2" 3 | source = { 4 | url = "http://www.tecgraf.puc-rio.br/~lhf/ftp/lua/5.2/luuid.tar.gz", 5 | md5 = "cd6c758f163b41b27a76b3d57cf730fd", 6 | dir = "uuid" 7 | } 8 | description = { 9 | summary = "A library for UUID generation", 10 | detailed = [[ 11 | A library for generating universally unique identifiers based on 12 | libuuid, which is part of e2fsprogs. 13 | ]], 14 | homepage = "http://www.tecgraf.puc-rio.br/~lhf/ftp/lua/#luuid", 15 | license = "Public domain" 16 | } 17 | dependencies = { 18 | "lua >= 5.2, < 5.4" 19 | } 20 | external_dependencies = { 21 | platforms = { 22 | unix = { 23 | LIBUUID = { 24 | header = "uuid/uuid.h", 25 | library = "libuuid.so" 26 | } 27 | } 28 | } 29 | } 30 | build = { 31 | type = "builtin", 32 | platforms = { 33 | win32 = { 34 | modules = { 35 | uuid = { 36 | libraries = { 37 | "rpcrt4", 38 | }, 39 | sources = { 40 | "luuid.c", 41 | "wuuid.c", 42 | } 43 | } 44 | } 45 | }, 46 | unix = { 47 | modules = { 48 | uuid = { 49 | libraries = { 50 | "uuid", 51 | }, 52 | sources = { 53 | "luuid.c", 54 | } 55 | } 56 | } 57 | } 58 | }; 59 | patches = { 60 | ["wuuid.diff"] = [[--- wuuid.h Tue Sep 25 02:25:11 2007 61 | +++ wuuid.h Sun Aug 21 00:36:43 2016 62 | @@ -19,7 +19,9 @@ 63 | #include 64 | #include 65 | #endif 66 | +#ifndef _MSC_VER 67 | #include 68 | +#endif 69 | 70 | /* 71 | * Fix declaration of uuid_t (possibly made in included]] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /rockspecs/spylog-scm-0.rockspec: -------------------------------------------------------------------------------- 1 | package="spylog" 2 | version="scm-0" 3 | 4 | -- for now this rockspec installs only dependencies but not spylog itself 5 | source = { 6 | url = "https://github.com/moteus/lua-spylog/archive/master.zip", 7 | dir = "lua-spylog-master/src", 8 | } 9 | 10 | description = { 11 | summary = "Execute actions based on log recods", 12 | detailed = [[ 13 | The main goal of this project is provide fail2ban functionality to Windows. 14 | ]], 15 | license = "MIT/X11", 16 | homepage = "https://github.com/moteus/lua-spylog" 17 | } 18 | 19 | dependencies = { 20 | "lua >= 5.1, <5.4", 21 | "bit32", 22 | "date", 23 | "lluv", 24 | "lluv-poll-zmq", 25 | "lpeg", 26 | "lrexlib-pcre", 27 | "lua-llthreads2", 28 | "lua-log > 0.1.5", 29 | "lua-path", 30 | "luafilesystem", 31 | "lzmq", 32 | "stacktraceplus", 33 | "lluv-ssl", 34 | "sendmail", 35 | "try", 36 | "prefix_tree", 37 | "gntp", 38 | "sqlite3", 39 | "mmdblua", 40 | "lua-lru", 41 | "environ", 42 | 43 | -- need install before by hand 44 | "lua-cjson", -- custom rockspec on windows/msvc 45 | "luuid", -- custom rockspec on windows/msvc 46 | "luaservice", -- not released yet 47 | "openssl", -- not released yet 48 | "lluv-esl", -- not released yet 49 | } 50 | 51 | build = { 52 | type = "builtin"; 53 | modules = {}; 54 | } 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/action/init.lua: -------------------------------------------------------------------------------- 1 | -- Configuration file for LuaService 2 | 3 | return { 4 | tracelevel = 7, 5 | name = "spylog_action", 6 | display_name = "SpyLog - Action", 7 | script = "main.lua", 8 | lua_cpath = '!\\..\\lib\\?.dll', 9 | lua_path = '!\\..\\lib\\?.lua;' .. 10 | '!\\..\\lib\\?\\init.lua;' .. 11 | '!\\lib\\?.lua'; 12 | } 13 | -------------------------------------------------------------------------------- /src/action/lib/spylog/actiondb.lua: -------------------------------------------------------------------------------- 1 | local config = require "spylog.config" 2 | local Args = require "spylog.args" 3 | local log = require "spylog.log" 4 | local var = require "spylog.var" 5 | local ut = require "lluv.utils" 6 | local sqlite = require "sqlite3" 7 | local path = require "path" 8 | local uuid = require "uuid" 9 | local date = require "date" 10 | local json = require "cjson" 11 | 12 | local dt = os.date("%Y-%m-%d %H:%M:%S") 13 | assert(dt == date(dt):fmt("%F %T")) 14 | 15 | local ActionDB = ut.class() do 16 | 17 | function ActionDB:__init(fileName) 18 | self._fileName = fileName or ":memory:" 19 | self._db = assert(sqlite.open(self._fileName)) 20 | self:init() 21 | return self 22 | end 23 | 24 | function ActionDB:init() 25 | assert(self._db:exec( 26 | "BEGIN TRANSACTION;" .. 27 | "CREATE TABLE IF NOT EXISTS actions (" .. 28 | "action_uuid not null," .. 29 | "action_time not null," .. 30 | "action_name not null," .. 31 | "action_jail not null," .. 32 | "action_type not null," .. 33 | "action_host not null," .. 34 | "action_opts not null," .. 35 | "action_full not null," .. 36 | "action_uniq null," .. 37 | "CONSTRAINT pk_actions PRIMARY KEY(action_uuid)" .. 38 | ");" .. 39 | "END TRANSACTION")) 40 | end 41 | 42 | function ActionDB:clear() 43 | assert(self._db:exec( 44 | "BEGIN TRANSACTION;" .. 45 | "DROP TABLE IF EXISTS actions;" .. 46 | "END TRANSACTION")) 47 | self:init() 48 | end 49 | 50 | function ActionDB:_find_command(action_type, unique) 51 | local stmt = assert(self._db:prepare( 52 | "select action_uuid, action_time " .. 53 | "from actions ".. 54 | "where action_uniq=? and action_type=? " .. 55 | "order by action_time " .. ((action_type == 'ban') and "desc " or "asc ") .. 56 | "limit 1" 57 | )) 58 | stmt:bind( 59 | unique, 60 | action_type 61 | ) 62 | local row = stmt:first_row() 63 | stmt:close() 64 | return row 65 | end 66 | 67 | function ActionDB:_add_command(unique, action, action_type, options) 68 | local active_action 69 | 70 | local context, action_name = action, action.action 71 | if action.parameters then 72 | context = var.combine{action, action.parameters} 73 | end 74 | 75 | local log_prefix = string.format("[%s][%s][%s]", action.jail, action.action, action_type) 76 | 77 | if unique then -- control duplicate 78 | unique = Args.apply_tags(unique, context) 79 | active_action = self:_find_command(action_type, unique) 80 | end 81 | 82 | if active_action then 83 | -- we already have one action in queue so we need only move it on early stage 84 | local flag = (date(active_action.action_time) > date(action.date)) 85 | 86 | log.debug("%s %s > %s == %s (%s)", log_prefix, active_action.action_time, action.date, flag and 'true' or 'false', action_type) 87 | 88 | if action_type == 'unban' then flag = not flag end 89 | 90 | if flag then 91 | log.info("%s reset time for active action from %s to %s", log_prefix, active_action.action_time, action.date) 92 | local stmt = assert(self._db:prepare("update actions set action_time=? where action_uuid=?;")) 93 | assert(stmt:bind( 94 | action.date, 95 | active_action.action_uuid 96 | )) 97 | assert(stmt:exec()) 98 | stmt:close() 99 | else 100 | log.info("%s reuse active action at %s", log_prefix, active_action.action_time) 101 | end 102 | 103 | return 104 | end 105 | 106 | action_args = Args.apply_tags(action_args or '', context) 107 | 108 | -- not prepared command so we create new one 109 | local stmt = assert(self._db:prepare( 110 | "INSERT INTO actions(action_uuid,action_time,action_name,action_jail,action_type,".. 111 | "action_host,action_opts,action_full,action_uniq)".. 112 | "VALUES (?,?,?,?,?,?,?,?,?)" 113 | )) 114 | 115 | assert(stmt:bind( 116 | action.uuid, 117 | action.date, 118 | action_name, 119 | action.jail, 120 | action_type, 121 | action.host, 122 | json.encode(options or {}), 123 | json.encode(action), 124 | unique 125 | )) 126 | 127 | log.info("%s schedule action at %s", log_prefix, action.date) 128 | 129 | assert(stmt:exec()) 130 | stmt:close() 131 | end 132 | 133 | local function is_table(t) 134 | return type(t) == 'table' and t 135 | end 136 | 137 | local function build_action_params(typ, action, command) 138 | -- 139 | -- if parameters has bun or unban specific params then we can not use `action.parameters` 140 | -- e.g. we have 141 | -- action.parameters = { 142 | -- a = 10; 143 | -- unban = { 144 | -- a = 20; 145 | -- } 146 | -- } 147 | -- and we build parameters to `ban` action. we do not need pass `unban` parameters to it. 148 | -- Also we can use bun/unban params to disable some action e.g. 149 | -- action.parameters = {unban=false} -- just prevent run unban action at all. 150 | -- 151 | 152 | local parameters 153 | 154 | -- export parameters from request 155 | if action.parameters and ( 156 | (action.parameters.ban ~= nil) or (action.parameters.unban ~= nil) 157 | ) then 158 | parameters = is_table(action.parameters[typ]) or {} 159 | 160 | for k, v in pairs(action.parameters) do 161 | if k ~= 'ban' and k ~= 'unban' and parameters[k] == nil then 162 | parameters[k] = v 163 | end 164 | end 165 | else 166 | parameters = action.parameters 167 | end 168 | 169 | -- apply default parameters 170 | if command.parameters then 171 | if not parameters then 172 | parameters = command.parameters 173 | else 174 | for k, v in pairs(command.parameters) do 175 | if parameters[k] == nil then 176 | parameters[k] = v 177 | end 178 | end 179 | end 180 | end 181 | 182 | return parameters 183 | end 184 | 185 | function ActionDB:add(action) 186 | local action_name = action.action 187 | 188 | local command = config.ACTIONS[action_name] 189 | 190 | if not command then 191 | log.alert('[%s] unknown action %s', action.jail, action_name) 192 | return 193 | end 194 | 195 | if command.ban and not (action.parameters and action.parameters.ban == false) then 196 | local parameters = build_action_params('ban', action, command) 197 | 198 | local unique = command.ban.unique or command.unique 199 | local options = command.ban.options or command.options 200 | action.ban_uuid = nil 201 | action.uuid = uuid.new() 202 | action.date = date(action.date):fmt("%F %T") 203 | action.cmd = command.ban 204 | action.parameters, parameters = parameters, action.parameters 205 | 206 | self:_add_command(unique, action, 'ban', options) 207 | 208 | action.parameters = parameters 209 | end 210 | 211 | if action.bantime and (action.bantime >= 0) and command.unban 212 | and not (action.parameters and action.parameters.unban == false) 213 | then 214 | local parameters = build_action_params('unban', action, command) 215 | 216 | local unique = command.unban.unique or command.unique 217 | local options = command.unban.options or command.options 218 | action.ban_uuid = action.uuid 219 | action.uuid = uuid.new() 220 | action.date = date(action.date):addseconds(action.bantime):fmt("%F %T") 221 | action.cmd = command.unban 222 | action.parameters, parameters = parameters, action.parameters 223 | 224 | self:_add_command(unique, action, 'unban', options) 225 | 226 | action.parameters = parameters 227 | end 228 | 229 | return 230 | end 231 | 232 | function ActionDB:next() 233 | local stmt = assert(self._db:prepare( 234 | "select action_uuid as uuid, action_time as date, action_name as action, action_jail as jail, " .. 235 | "action_type as type, action_host as host, " .. 236 | "action_opts as options, action_full as action " .. 237 | "from actions ".. 238 | "where action_time<=? " .. 239 | "order by action_time " .. 240 | "limit 1" 241 | )) 242 | stmt:bind(os.date("%Y-%m-%d %H:%M:%S")) 243 | local row = stmt:first_row() 244 | stmt:close() 245 | 246 | if row then 247 | row.options = json.decode(row.options) 248 | row.action = json.decode(row.action) 249 | end 250 | 251 | return row 252 | end 253 | 254 | function ActionDB:remove(row) 255 | return self._db:exec( 256 | "delete from actions ".. 257 | "where action_uuid='" .. row.uuid .. "'" 258 | ) 259 | end 260 | 261 | function ActionDB:close() 262 | self.db:close() 263 | end 264 | 265 | end 266 | 267 | --[=[ 268 | do 269 | local pp = require "pp" 270 | 271 | local db = ActionDB.new() 272 | local action1 = cjson.decode[[{"host":"192.168.123.102","action":"ipsec","jail":"freeswitch-auth-request","date":"2015-09-17 13:36:15.626250","bantime":60,"filter":"freeswitch-auth-request"}]] 273 | local action2 = cjson.decode[[{"host":"192.168.123.102","action":"ipsec","jail":"freeswitch-auth-request","date":"2015-09-17 12:36:15.626250","bantime":10800,"filter":"freeswitch-auth-request"}]] 274 | local action3 = cjson.decode[[{"host":"192.168.123.102","action":"ipsec","jail":"freeswitch-auth-request","date":"2015-09-17 14:36:15.626250","bantime":60,"filter":"freeswitch-auth-request"}]] 275 | 276 | db:add(action1) 277 | db:add(action2) 278 | db:add(action3) 279 | 280 | print("-------------------------------------------------") 281 | 282 | local row = db:next() 283 | while row do 284 | pp(row) 285 | db:remove(row) 286 | row = db:next() 287 | end 288 | 289 | return 290 | 291 | end 292 | 293 | --]=] 294 | 295 | return ActionDB -------------------------------------------------------------------------------- /src/action/lib/spylog/actions/chain.lua: -------------------------------------------------------------------------------- 1 | local uv = require "lluv" 2 | local spawn = require "spylog.spawn" 3 | local Args = require "spylog.args" 4 | local var = require "spylog.var" 5 | local log = require "spylog.log" 6 | 7 | return function(task, cb) 8 | local action, options = task.action, task.options 9 | local context, command = action, action.cmd[2] 10 | local parameters = action.parameters or command.parameters 11 | 12 | if action.parameters then context = var.combine{action, action.parameters, command.parameters} 13 | elseif command.parameters then context = var.combine{action, command.parameters} end 14 | 15 | local log_header = string.format("[%s][%s][%s]", action.jail, action.action, task.type) 16 | 17 | local commands = command 18 | if type(commands) == 'string' then commands = {commands} end 19 | 20 | if type(commands) ~= 'table' or #commands == 0 then 21 | log.warning('%s no commands to execute', log_header) 22 | return uv.defer(cb, task, 0) 23 | end 24 | 25 | for i = 1, #commands do 26 | if type(commands[i]) == 'string' then 27 | commands[i] = {commands[i]} 28 | end 29 | 30 | local command = commands[i] 31 | 32 | local cmd, args, tail = Args.decode_command(command, context) 33 | 34 | if not cmd then 35 | log.error("%s Can not parse argument string: %s", log_header, args) 36 | return uv.defer(cb, task, false, args) 37 | end 38 | 39 | if tail then 40 | log.warning("%s Unused command arguments: %q", log_header, tail) 41 | end 42 | 43 | command[1], command[2] = cmd, args 44 | 45 | log.debug("%s[%d] prepare to execute: %s %s", log_header, i, cmd, Args.build(args)) 46 | end 47 | 48 | local last_error, last_command 49 | spawn.chain(commands, options.timeout, function(i, typ, err, status, signal) 50 | if typ == 'done' then 51 | return uv.defer(cb, task, not last_error, last_error) 52 | end 53 | 54 | if typ == 'exit' then 55 | last_command, last_error, last_status = i, err, status 56 | log.debug("%s[%d] chain command exit: %s", log_header, i, tostring(err or status)) 57 | return 58 | end 59 | 60 | return log.trace("%s[%d] chain command output: [%s] %s", log_header, i, typ, tostring(err or status)) 61 | end) 62 | end 63 | -------------------------------------------------------------------------------- /src/action/lib/spylog/actions/growl.lua: -------------------------------------------------------------------------------- 1 | local log = require "spylog.log" 2 | local var = require "spylog.var" 3 | local Args = require "spylog.args" 4 | local uv = require "lluv" 5 | local ut = require "lluv.utils" 6 | local GNTP = require "gntp" 7 | 8 | local app = GNTP.Application.new{'SpyLog', 9 | notifications = { 10 | { 'BAN', 11 | -- title = 'Ban', 12 | -- display = 'Ban', 13 | enabled = true, 14 | }; 15 | { 'UNBAN', 16 | -- title = 'Unban', 17 | -- display = 'Unban', 18 | enabled = true, 19 | }; 20 | } 21 | } 22 | 23 | local reged = {} 24 | 25 | local function decode_growl_address(url) 26 | local auth, address = ut.split_first(url, '@', true) 27 | if not address then address, auth = auth end 28 | local port 29 | address, port = ut.split_first(address, ':', true) 30 | port = port and tonumber(port) 31 | if not port then port = '23053' end 32 | local pass, hash, enc 33 | if auth then pass, hash, enc = ut.usplit(auth, ':', true) end 34 | return address, tostring(port), pass, hash, enc 35 | end 36 | 37 | local function is_growl_ok(msg) 38 | if msg:status() == '-ERROR' then 39 | return nil, string.format("[GROWL] %s (%s)", 40 | msg:header('Error-Description') or '----', 41 | msg:header('Error-Code') or '----' 42 | ) 43 | end 44 | 45 | return true 46 | end 47 | 48 | return function(task, cb) 49 | local action, options = task.action, task.options 50 | local context, command = action, action.cmd[2] 51 | local parameters = action.parameters or command.parameters 52 | 53 | if action.parameters then context = var.combine{action, action.parameters, command.parameters} 54 | elseif command.parameters then context = var.combine{action, command.parameters} end 55 | 56 | local log_header = string.format("[%s][%s][%s]", action.jail, action.action, task.type) 57 | 58 | log.debug('%s execute start', log_header) 59 | 60 | local args, tail = Args.decode(command, context) 61 | 62 | if not args then 63 | log.error("%s Can not parse argument string: %q", log_header, tail) 64 | return uv.defer(cb, task, nil, tail) 65 | end 66 | 67 | if tail then 68 | log.warning("%s unused command arguments: %q", log_header, tail) 69 | end 70 | 71 | local subject = parameters and parameters.fullsubj or args[1] 72 | local message = parameters and parameters.fullmsg or args[2] 73 | local priority = parameters and parameters.priority 74 | local icon = parameters and parameters.icon 75 | local sticky = parameters and parameters.sticky 76 | local notify_type = string.upper(task.type) 77 | 78 | local count, growl_err 79 | 80 | local function send_notify(growl, address) 81 | growl:notify(notify_type, { 82 | title = subject, 83 | text = message, 84 | priority = priority, 85 | sticky = sticky, 86 | icon = icon, 87 | }, function(self, err, msg) 88 | if not err then local ok ok, err = is_growl_ok(msg) end 89 | 90 | count = count - 1 91 | growl_err = growl_err or err 92 | 93 | if err then 94 | log.error('%s can not send notify to %s: %s', log_header, address, tostring(err)) 95 | end 96 | 97 | if count > 0 then return end 98 | 99 | if growl_err then 100 | uv.defer(cb, task, false, growl_err) 101 | else 102 | uv.defer(cb, task, true) 103 | end 104 | 105 | log.debug('%s execute done', log_header) 106 | end) 107 | end 108 | 109 | local function send_register(growl, address) 110 | growl:register(function(self, err, msg) 111 | if not err then local ok ok, err = is_growl_ok(msg) end 112 | 113 | if err then 114 | log.error('%s can not register on %s: %s', log_header, count, address, tostring(err)) 115 | 116 | count = count - 1 117 | 118 | if count == 0 then 119 | uv.defer(cb, task, false, err) 120 | log.debug('%s execute done', log_header) 121 | else growl_err = err end 122 | 123 | return 124 | end 125 | 126 | reged[address] = true 127 | 128 | send_notify(growl, address) 129 | end) 130 | end 131 | 132 | local function notify(address, port, password, hash, encrypt) 133 | address = address or '127.0.0.1'; 134 | 135 | local growl = GNTP.Connector.lluv(app, { 136 | host = address; 137 | port = port or '23053'; 138 | pass = password; 139 | encrypt = encrypt; 140 | hash = hash; 141 | }) 142 | 143 | log.debug('%s notify %s: [%s] %s', log_header, address, notify_type, subject) 144 | 145 | if not reged[address] then return send_register(growl, address) end 146 | return send_notify(growl, address) 147 | end 148 | 149 | local dest = parameters and parameters.dest 150 | 151 | if dest and string.find(dest, '[;,]') then 152 | dest = ut.split(dest, '[;,]') 153 | count = #dest 154 | for i = 1, #dest do 155 | local address, port, password, hash, encrypt = decode_growl_address(dest[i]) 156 | if address and #address > 0 then 157 | notify(address, port, password, hash, encrypt) 158 | else 159 | log.error('%s Invalid growl destination: %s', log_header, dest[i]) 160 | count = count - 1 161 | end 162 | if count == 0 then 163 | uv.defer(cb, task, false, 'Invalid destinations') 164 | end 165 | end 166 | return 167 | end 168 | 169 | count = 1 170 | 171 | local address, port, password, hash, encrypt 172 | if type(dest) == 'string' then 173 | address, port, password, hash, encrypt = decode_growl_address(dest) 174 | if (not address) or #address == 0 then 175 | log.error('%s Invalid growl destination: %s', log_header, dest) 176 | return uv.defer(cb, task, false, 'Invalid destinations') 177 | end 178 | elseif options then 179 | address = options.address 180 | port = options.port 181 | password = options.password 182 | hash = options.hash 183 | encrypt = options.encrypt 184 | end 185 | 186 | notify(address, port, password, hash, encrypt) 187 | end 188 | -------------------------------------------------------------------------------- /src/action/lib/spylog/actions/mail.lua: -------------------------------------------------------------------------------- 1 | if (_VERSION == "Lua 5.1") and (not coroutine.coco) then 2 | -- Lua 5.1 does not support yield accross 3 | -- C function so we use `try.co` module to 4 | -- replace default implementation of LuaSocket 5 | -- protect functionality. 6 | 7 | local socket = require "socket" 8 | local try = require "try.co" 9 | 10 | socket.newtry = try.new 11 | socket.protect = try.protect 12 | socket.try = try.new() 13 | end 14 | 15 | local uv = require "lluv" 16 | local ut = require "lluv.utils" 17 | local socket = require "lluv.luasocket" 18 | socket.ssl = require "lluv.ssl.luasocket".ssl 19 | local ssl = require "lluv.ssl" 20 | local sendmail = require "sendmail" 21 | local Args = require "spylog.args" 22 | local log = require "spylog.log" 23 | local var = require "spylog.var" 24 | 25 | local function sok_create() 26 | local co = coroutine.running() 27 | return function(...) return socket.tcp(...):attach(co) end 28 | end 29 | 30 | local function ssl_create() 31 | local co = coroutine.running() 32 | return function(...) return socket.ssl(...):attach(co) end 33 | end 34 | 35 | local SSL_CONTEXT = {} 36 | 37 | local function CTX(opt) 38 | if not opt then return end 39 | 40 | local ctx = SSL_CONTEXT[opt] 41 | if not ctx then 42 | ctx = ssl.context(opt) 43 | SSL_CONTEXT[opt] = ctx 44 | end 45 | 46 | return ctx 47 | end 48 | 49 | return function(task, cb) 50 | local action, options = task.action, task.options 51 | local context, command = action, action.cmd[2] 52 | local parameters = action.parameters or command.parameters 53 | 54 | if action.parameters then context = var.combine{action, action.parameters, command.parameters} 55 | elseif command.parameters then context = var.combine{action, command.parameters} end 56 | 57 | local log_header = string.format("[%s][%s][%s]", action.jail, action.action, task.type) 58 | 59 | local args, tail = Args.decode(command, context) 60 | 61 | if not args then 62 | log.error("%s Can not parse argument string: %q", log_header, tail) 63 | return uv.defer(cb, task, nil, tail) 64 | end 65 | 66 | if tail then 67 | log.warning("%s unused command arguments: %q", log_header, tail) 68 | end 69 | 70 | local subject = parameters and parameters.fullsubj or args[1] 71 | local message = parameters and parameters.fullmsg or args[2] 72 | local charset = parameters and parameters.charset or options.charset 73 | 74 | ut.corun(function() 75 | local ok, err = sendmail{ 76 | server = { 77 | address = options.server.address; 78 | user = options.server.user; 79 | password = options.server.password; 80 | ssl = CTX(options.server.ssl); 81 | create = options.server.ssl and ssl_create() or sok_create(); 82 | }, 83 | 84 | from = { 85 | title = parameters and parameters.sendername or options.from.title; 86 | address = parameters and parameters.sender or options.from.address; 87 | }, 88 | 89 | to = { 90 | title = parameters and parameters.destname or options.to.title; 91 | address = parameters and parameters.dest or options.to.address; 92 | }, 93 | 94 | message = { 95 | subject = {subject, charset = charset}, 96 | text = {message, charset = charset}, 97 | } 98 | } 99 | 100 | uv.defer(cb, task, ok, err) 101 | end) 102 | end 103 | -------------------------------------------------------------------------------- /src/action/lib/spylog/actions/spawn.lua: -------------------------------------------------------------------------------- 1 | local uv = require "lluv" 2 | local spawn = require "spylog.spawn" 3 | local Args = require "spylog.args" 4 | local log = require "spylog.log" 5 | local var = require "spylog.var" 6 | 7 | return function(task, cb) 8 | local action, options = task.action, task.options 9 | local context, command = action, action.cmd 10 | local parameters = action.parameters or command.parameters 11 | 12 | if action.parameters then context = var.combine{action, action.parameters, command.parameters} 13 | elseif command.parameters then context = var.combine{action, command.parameters} end 14 | 15 | local log_header = string.format("[%s][%s][%s]", action.jail, action.action, task.type) 16 | 17 | local cmd, args, tail = Args.decode_command(command, context) 18 | 19 | if not cmd then 20 | log.error("%s Can not parse argument string: %s", log_header, args) 21 | return uv.defer(cb, task, false, args) 22 | end 23 | 24 | if tail then 25 | log.warning("%s Unused command arguments: %q", log_header, tail) 26 | end 27 | 28 | if string.sub(cmd, 1, 1) == '@' then 29 | cmd = table.remove(args, 1) 30 | end 31 | 32 | log.debug("%s prepare to execute: %s %s", log_header, cmd, Args.build(args)) 33 | 34 | spawn(cmd, args, options.timeout, function(typ, err, status, signal) 35 | if typ == 'exit' then 36 | if not err then 37 | if not (command.ignore_status or status == 0) then 38 | err = spawn.estatus(status, signal) 39 | end 40 | end 41 | return uv.defer(cb, task, not err, err) 42 | end 43 | return log.trace("%s command output: [%s] %s", log_header, typ, tostring(err or status)) 44 | end) 45 | end 46 | -------------------------------------------------------------------------------- /src/action/main.lua: -------------------------------------------------------------------------------- 1 | local SERVICE = require "LuaService" 2 | local config = require "spylog.config" 3 | config.LOG.prefix = "[action] " 4 | ------------------------------------------------- 5 | 6 | local log = require "spylog.log" 7 | local version = require "spylog.version" 8 | local uv = require "lluv" 9 | uv.poll_zmq = require "lluv.poll_zmq" 10 | local zthreads = require "lzmq.threads" 11 | local ztimer = require "lzmq.timer" 12 | local ut = require "lluv.utils" 13 | local path = require "path" 14 | local cjson = require "cjson.safe" 15 | local stp = require "StackTracePlus" 16 | local ActionDB = require "spylog.actiondb" 17 | local spawn = require "spylog.actions.spawn" 18 | local exit = require "spylog.exit" 19 | 20 | log.info('Starting %s version %s. %s', version._NAME, version._VERSION, version._COPYRIGHT) 21 | 22 | log.debug("config.LOG.multithread: %s", tostring(config.LOG.multithread)) 23 | 24 | local EXICUTERS = {} 25 | 26 | local db_path = path.normalize(path.join(SERVICE.PATH, "data")) 27 | path.mkdir(db_path) 28 | 29 | local actions = ActionDB.new( 30 | path.join(db_path, "action.db") 31 | ) 32 | 33 | local ACTION_POLL, action_timer = 10000 34 | 35 | local sub, err = zthreads.context():socket("SUB",{ 36 | [config.CONNECTIONS.ACTION.JAIL.type] = config.CONNECTIONS.ACTION.JAIL.address; 37 | subscribe = ""; 38 | }) 39 | 40 | if not sub then 41 | log.fatal("Can not start action interface: %s", tostring(err)) 42 | ztimer.sleep(500) 43 | return SERVICE.exit() 44 | end 45 | 46 | local function do_action(task, cb) 47 | local action = task.action 48 | local cmd = action.cmd[1] 49 | 50 | local log_prefix = string.format("[%s][%s][%s]", action.jail, action.action, task.type) 51 | 52 | log.info("%s start action", log_prefix, action.date) 53 | 54 | local exicuter 55 | if cmd:sub(1, 1) == '@' then 56 | exicuter = EXICUTERS[cmd] 57 | if not exicuter then 58 | log.alert("%s action module not loaded", log_prefix) 59 | actions:remove(task) 60 | return uv.defer(cb) 61 | end 62 | else 63 | exicuter = spawn 64 | end 65 | 66 | exicuter(task, function(task, ok, err) 67 | actions:remove(task) 68 | uv.defer(cb) 69 | 70 | if not ok then 71 | log.error('%s execute action error: %s', log_prefix, tostring(err or 'unknown')) 72 | else 73 | log.info('%s execute action success', log_prefix) 74 | end 75 | end) 76 | end 77 | 78 | local function next_action() 79 | local action = actions:next() 80 | if not action then 81 | return action_timer:again(ACTION_POLL) 82 | end 83 | 84 | do_action(action, next_action) 85 | end 86 | 87 | uv.poll_zmq(sub):start(function(handle, err, pipe) 88 | if err then 89 | log.fatal("poll: %s", tostring(err)) 90 | return uv.stop() 91 | end 92 | 93 | local msg, err = sub:recvx() 94 | if not msg then 95 | if err:name() ~= 'EAGAIN' then 96 | log.fatal("recv msg: %s", tostring(err)) 97 | uv.stop() 98 | end 99 | return 100 | end 101 | 102 | log.trace("%s", msg) 103 | 104 | local task = cjson.decode(msg) 105 | if not (task and task.action and task.date) then 106 | log.error("invalid msg: %q", msg:sub(128)) 107 | return 108 | end 109 | 110 | if type(task.action) ~= 'table' then 111 | log.error("invalid action format: %q", msg:sub(128)) 112 | return 113 | end 114 | 115 | local task_actions = task.action 116 | for i = 1, #task_actions do 117 | local action = task_actions[i] 118 | if type(action) ~= 'table' then 119 | log.error("invalid action format: %q", msg:sub(128)) 120 | return 121 | end 122 | end 123 | 124 | -- ugly hack. `actions.add` method change date field. 125 | -- so we have reset this field each time 126 | local date = task.date 127 | 128 | for i = 1, #task_actions do 129 | local action = task_actions[i] 130 | 131 | -- build task table to each action 132 | task.date = date 133 | task.action = action[1] 134 | task.parameters = action[2] 135 | actions:add(task) 136 | end 137 | 138 | if action_timer:active() then 139 | action_timer:again(1) 140 | end 141 | end) 142 | 143 | action_timer = uv.timer():start(0, ACTION_POLL, function() 144 | action_timer:stop() 145 | next_action() 146 | end) 147 | 148 | exit.start_monitor(...) 149 | 150 | local function init_service() 151 | 152 | local function append_executer(cmd) 153 | if cmd:sub(1, 1) == '@' and not EXICUTERS[cmd] then 154 | local ok, executer = pcall(require, (cmd:sub(2))) 155 | assert(ok, ("Can not load action module `%s`: %s"):format(cmd:sub(2), tostring(executer))) 156 | EXICUTERS[cmd] = executer 157 | end 158 | end 159 | 160 | for name, cmd in pairs(config.ACTIONS) do 161 | if type(cmd.ban ) == 'string' then cmd.ban = {cmd.ban } end 162 | if type(cmd.unban) == 'string' then cmd.unban = {cmd.unban} end 163 | end 164 | 165 | for name, cmd in pairs(config.ACTIONS) do 166 | if cmd.ban then append_executer(cmd.ban[1]) end 167 | if cmd.unban then append_executer(cmd.unban[1]) end 168 | log.info("Add new action: %s", tostring(name)) 169 | end 170 | 171 | end 172 | 173 | local ok, err = pcall(init_service) 174 | if not ok then 175 | log.fatal("Can not load actions: %s", tostring(err)) 176 | ztimer.sleep(500) 177 | return SERVICE.exit() 178 | end 179 | 180 | local ok, err = pcall(uv.run, stp.stacktrace) 181 | 182 | if not ok then 183 | log.alert(err) 184 | end 185 | 186 | log.info("Service stopped") 187 | 188 | ztimer.sleep(500) 189 | 190 | SERVICE.exit() 191 | -------------------------------------------------------------------------------- /src/config/actions/advfirewall.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- https://technet.microsoft.com/en-us/library/dd734783(v=ws.10).aspx 3 | -- protocol = { any | Integer | icmpv4 | icmpv6 | icmpv4:type,code | icmpv6:type,code | tcp | udp } 4 | -- Windows 7 does not support multiple protocol 5 | -- 6 | -- port = { any | Integer | rpc | rpc-epmap | teredo | [ ,... ] } 7 | -- 8 | -- You have to set `protocol` if you whant ban specific port. 9 | -- 10 | -- Example ban ports 5060 and 5080 for tcp and udp 11 | -- action = { 12 | -- {"advfirewall", {port = "5060,5080"; protocol = "udp"}}; 13 | -- {"advfirewall", {port = "5060,5080"; protocol = "tcp"}}; 14 | -- } 15 | -- 16 | 17 | local unban = 'netsh advfirewall firewall delete rule name="SpyLog "' 18 | local ban = 'netsh advfirewall firewall add rule dir=in interface=any action=block ' .. 19 | 'name="SpyLog " description=" " ' .. 20 | 'remoteip="/" localport="" protocol="" '; 21 | 22 | local param = { 23 | net = '32'; 24 | port = 'any'; 25 | protocol = 'any'; 26 | service = 'any'; 27 | } 28 | 29 | ACTION{"advfirewall", 30 | ban = ban; 31 | unban = unban; 32 | parameters = param; 33 | options = { timeout = 10000 }; 34 | } 35 | 36 | -- `program` parameter has no default value and can not be empty 37 | ACTION{"advfirewall-program", 38 | ban = ban .. ' program=""'; 39 | unban = unban; 40 | parameters = param; 41 | options = { timeout = 10000 }; 42 | } 43 | 44 | -- `service=any` means that rule will be apply only to services but not to regular application 45 | ACTION{"advfirewall-service", 46 | ban = ban .. ' service=""'; 47 | unban = unban; 48 | parameters = param; 49 | options = { timeout = 10000 }; 50 | } 51 | 52 | ACTION{"advfirewall-program-service", 53 | ban = ban .. ' program="" service=""'; 54 | unban = unban; 55 | parameters = param; 56 | options = { timeout = 10000 }; 57 | } 58 | -------------------------------------------------------------------------------- /src/config/actions/badips.lua: -------------------------------------------------------------------------------- 1 | -- https://www.badips.com 2 | -- to set Key you need call `curl http://www.badips.com/set/key/` 3 | -- in jail e.g. `action = {'ipsec', {'badips', {category='sip'}}}` 4 | 5 | ACTION{"badips", 6 | ban = 'curl --fail --user-agent "SpyLog" https://www.badips.com/add//'; 7 | 8 | unique = 'badips '; 9 | 10 | parameters = { 11 | category = 'sip'; 12 | }; 13 | 14 | options = { 15 | timeout = 10000; 16 | }; 17 | 18 | }; 19 | -------------------------------------------------------------------------------- /src/config/actions/growl.lua: -------------------------------------------------------------------------------- 1 | ACTION{"growl", 2 | ban = { "@spylog.actions.growl", [["SpyLog ban " " 3 | Filter: 4 | Jail: 5 | Host: 6 | "]]}; 7 | 8 | unban = { "@spylog.actions.growl", [["SpyLog unban " " 9 | Filter: 10 | Jail: 11 | Host: 12 | "]]}; 13 | 14 | options = { 15 | address = "127.0.0.1"; 16 | -- port = "23053"; 17 | -- password = "secret"; 18 | -- encrypt = "AES"; 19 | -- hash = "SHA256"; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/config/actions/ipsec.lua: -------------------------------------------------------------------------------- 1 | -- Start action: 2 | -- netsh ipsec static delete policy name=SpyLogBlock 3 | -- netsh ipsec static add filteraction name=SpyLogBlock action=block 4 | -- netsh ipsec static add filter filterlist=SpyLogBlock srcaddr=192.168.192.100 dstaddr=me 5 | -- netsh ipsec static add policy name=SpyLogBlock assign=yes activatedefaultrule=no 6 | -- netsh ipsec static add rule name=SpyLogBlock policy=SpyLogBlock filterlist=SpyLogBlock filteraction=SpyLogBlock 7 | -- netsh ipsec static delete filter filterlist=SpyLogBlock srcaddr=192.168.192.100 dstaddr=Me 8 | -- Stop action: 9 | -- netsh ipsec static delete policy name=SpyLogBlock 10 | local args = 'filterlist= srcaddr= srcmask= protocol= dstport= dstaddr=me' 11 | ACTION{"ipsec", 12 | ban = 'netsh ipsec static add filter ' .. args .. ' description=" "'; 13 | 14 | unban = 'netsh ipsec static delete filter ' .. args; 15 | 16 | unique = "netsh ipsec " .. args; 17 | 18 | parameters = { 19 | filterlist = 'SpyLogBlock'; 20 | net = '32'; 21 | protocol = 'ANY'; 22 | port = '0'; 23 | }; 24 | 25 | options = { 26 | timeout = 10000; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/config/actions/mail.lua: -------------------------------------------------------------------------------- 1 | ACTION{"mail", 2 | ban = { "@spylog.actions.mail", [["SpyLog ban " " 3 | Date: 4 | Filter: 5 | Jail: 6 | Host: 7 | 8 | "]]}; 9 | 10 | unban = { "@spylog.actions.mail", [["SpyLog unban " " 11 | Date: 12 | Filter: 13 | Jail: 14 | Host: 15 | 16 | "]]}; 17 | 18 | options = { 19 | server = { 20 | address = "smtp.domain.local"; 21 | user = "spylog@domain.local"; 22 | password = "secret"; 23 | -- ssl = {verify = {"none"}}; 24 | }, 25 | 26 | from = { 27 | title = "SpyLog Service"; 28 | address = "spylog@domain.local"; 29 | }, 30 | 31 | to = { 32 | title = "Dear Admin"; 33 | address = "admin@domain.local"; 34 | }, 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /src/config/filters/freeswitch.lua: -------------------------------------------------------------------------------- 1 | FILTER{ "freeswitch-ip-request"; 2 | enabled = false; 3 | source = "freeswitch"; 4 | exclude = WHITE_IP; 5 | hint = "[WARNING]"; 6 | engine = 'pcre'; 7 | failregex = { 8 | [[^(\d\d\d\d\-\d\d\-\d\d \d\d:\d\d:\d\d\.\d+) \[WARNING\] sofia_reg.c:\d+ SIP auth (?:challenge|failure) \([A-Z]+\) on sofia profile \'[^']+\' for \[.*?@\d+\.\d+\.\d+\.\d+\] from ip ([0-9.]+)\s*$]] 9 | } 10 | } 11 | 12 | FILTER{ "freeswitch-auth-request"; 13 | enabled = false; 14 | source = "freeswitch"; 15 | exclude = WHITE_IP; 16 | hint = "[WARNING]"; 17 | failregex = { 18 | "^(%d%d%d%d%-%d%d%-%d%d %d%d:%d%d:%d%d%.%d+) %[WARNING%] sofia_reg.c:%d+ SIP auth challenge %([A-Z]+%) on sofia profile %'[^']+%' for %[.-%] from ip ([0-9.]+)%s*$"; 19 | } 20 | }; 21 | 22 | FILTER{ "freeswitch-auth-fail"; 23 | enabled = false; 24 | source = "freeswitch"; 25 | exclude = WHITE_IP; 26 | hint = "[WARNING]"; 27 | failregex = { 28 | "^(%d%d%d%d%-%d%d%-%d%d %d%d:%d%d:%d%d%.%d+) %[WARNING%] sofia_reg.c:%d+ SIP auth failure %([A-Z]+%) on sofia profile %'[^']+%' for %[.-%] from ip ([0-9.]+)%s*$"; 29 | "^(%d%d%d%d%-%d%d%-%d%d %d%d:%d%d:%d%d%.%d+) %[WARNING%] sofia.c:%d+ IP ([0-9.]+) Rejected by acl \"[^\"]*\"%s*$"; 30 | } 31 | }; 32 | 33 | -------------------------------------------------------------------------------- /src/config/filters/fusionpbx.lua: -------------------------------------------------------------------------------- 1 | FILTER{ "fusionpbx-fail-access"; 2 | enabled = false; 3 | source = "eventlog:udp://127.0.0.1"; 4 | exclude = WHITE_IP; 5 | events = {'PHP-*', 3}; 6 | failregex = { 7 | "FusionPBX %[([0-9.]+)%] authentication failed"; 8 | "FusionPBX %[([0-9.]+)%] provision attempt bad password for"; 9 | } 10 | }; 11 | 12 | -------------------------------------------------------------------------------- /src/config/filters/radmin.lua: -------------------------------------------------------------------------------- 1 | FILTER{ "radmin-fail-access"; 2 | enabled = false; 3 | -- radmin 2.2 4 | source = "file:c:/logfile.txt"; 5 | -- radmin 3.0 6 | -- source = "file:C:/WINDOWS/system32/rserver30/Radm_log.htm"; 7 | exclude = WHITE_IP; 8 | hint = "Password is incorrect"; 9 | failregex = { 10 | -- radmin 2.2 11 | "^[%d+%.<>: ]+Connection from ([%d+%.]+) : Password is incorrect or error occurs"; 12 | -- radmin 3.0 13 | "^<%d+> RServer3 [^()]+%(([%d%.]+)%).-Password is incorrect or error occurs"; 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/config/filters/rdp-nxlog.lua: -------------------------------------------------------------------------------- 1 | -- When use NTLM auth event 4625 has no IP address 2 | -- so we should use different event but this event can not 3 | -- be forwarded by SNMP so we have to use some external tool. 4 | -- 5 | -- Forward eventlogs using nxlog (http://nxlog.co) 6 | -- 7 | -- 8 | -- Module im_msvistalog 9 | -- SavePos TRUE 10 | -- ReadFromLast TRUE 11 | -- Channel "Microsoft-Windows-RemoteDesktopServices-RdpCoreTS/Operational" 12 | -- 13 | -- 14 | -- 15 | -- 16 | -- 17 | -- 18 | -- 19 | -- 20 | -- 21 | -- 22 | -- Module om_udp 23 | -- Host 127.0.0.1 24 | -- Port 614 25 | -- Exec $raw_event = "EventID: " + $EventID + "; " + $Message; 26 | -- 27 | -- 28 | -- 29 | -- Path eventlog => spylog 30 | -- 31 | -- 32 | FILTER{ "rdp-fail-access-140-nxlog"; 33 | enabled = false; 34 | source = "nxlog"; 35 | exclude = WHITE_IP; 36 | hint = "EventID: 140;"; 37 | failregex = { 38 | "^EventID: 140; A connection from the client computer with an IP address of ([%d%.:]+)"; 39 | -- UTF8 40 | "^EventID: 140; Не удалось подключить клиентский компьютер с IP%-адресом ([%d%.:]+)"; 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /src/config/filters/rdp.lua: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moteus/lua-spylog/6c46407f78ada33110e8e463d3e1bbf8d017bf3b/src/config/filters/rdp.lua -------------------------------------------------------------------------------- /src/config/filters/syslog_http_request.lua: -------------------------------------------------------------------------------- 1 | -- Filter to logs generated by Kiwi Syslog generator 2 | FILTER{ "syslog_http_request"; 3 | enabled = false; 4 | source = "syslog:udp://127.0.0.1"; 5 | exclude = WHITE_IP; 6 | failregex = { 7 | "Test user connected to website http://([0-9.]-)/index.html"; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/config/filters/tshark.lua: -------------------------------------------------------------------------------- 1 | FILTER{ "tshark-sip-options"; 2 | enabled = false; 3 | source = 'tshark-sip-options'; 4 | exclude = WHITE_IP; 5 | failregex = '^(%d%d%d%d%-%d%d%-%d%d %d%d:%d%d:%d%d%.%d+)\t([0-9.]+)'; 6 | } 7 | 8 | FILTER{ "tshark-sip-ip-request"; 9 | enabled = false; 10 | source = 'tshark-sip-request'; 11 | exclude = WHITE_IP; 12 | engine = 'pcre'; 13 | failregex = [[^(\d\d\d\d\-\d\d\-\d\d \d\d:\d\d:\d\d\.\d+)\t([0-9.]+)\t(?:REGISTER|INVITE|OPTIONS)\t[0-9.]+\t\d+\.\d+\.\d+\.\d+\t]] 14 | } 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/config/jails/access.lua: -------------------------------------------------------------------------------- 1 | local SECOND = 1 2 | local MINUTE = 60 * SECOND 3 | local HOUR = 60 * MINUTE 4 | local DAY = 24 * HOUR 5 | 6 | JAIL{"fail-access"; 7 | enabled = false; 8 | filter = {"rdp-fail-access", "fusionpbx-fail-access", "radmin-fail-access"}; 9 | findtime = 5 * MINUTE; 10 | maxretry = 4; 11 | bantime = 10 * MINUTE + 28 * SECOND; 12 | action = "ipsec"; 13 | } 14 | 15 | JAIL{"rdp-bad-user-access"; 16 | enabled = false; 17 | filter = "rdp-fail-access"; 18 | findtime = 5 * MINUTE; 19 | maxretry = 1; 20 | bantime = 1 * DAY + 10 * MINUTE + 28 * SECOND; 21 | action = "ipsec"; 22 | cfilter = {"list", 23 | type = "allow", 24 | capture = "user", 25 | nocase = true, 26 | filter = { 27 | "administrator"; 28 | "guest"; 29 | "user"; 30 | "root"; 31 | }; 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/config/jails/voip.lua: -------------------------------------------------------------------------------- 1 | local SECOND = 1 2 | local MINUTE = 60 * SECOND 3 | local HOUR = 60 * MINUTE 4 | local DAY = 24 * HOUR 5 | 6 | JAIL{"voip-auth-request"; 7 | enabled = false; 8 | filter = {"freeswitch-auth-request"}; 9 | findtime = 1 * MINUTE; 10 | maxretry = 10; 11 | bantime = 24 * HOUR; 12 | action = {"ipsec", {"mail", {unban=false}}}; 13 | } 14 | 15 | JAIL{"voip-auth-fail"; 16 | enabled = false; 17 | filter = {"freeswitch-auth-fail"}; 18 | findtime = 10 * MINUTE; 19 | maxretry = 3; 20 | bantime = 24 * HOUR; 21 | action = {"ipsec", {"mail", {unban=false}}}; 22 | } 23 | 24 | JAIL{"voip-ip-request"; 25 | enabled = false; 26 | filter = {"freeswitch-ip-request"}; 27 | findtime = 1 * MINUTE; 28 | maxretry = 1; 29 | bantime = 7 * DAY; 30 | action = {"ipsec", {"mail", {unban=false}}}; 31 | } 32 | -------------------------------------------------------------------------------- /src/config/sources/freeswitch.lua: -------------------------------------------------------------------------------- 1 | -- based on log file 2 | -- SOURCE{"freeswitch", 3 | -- "file:c:/FreeSWITCH/log/freeswitch.log", 4 | -- poll = 30, max_line = 20 * 1024; 5 | -- } 6 | 7 | -- based on ESL 8 | SOURCE{"freeswitch", 9 | "esl:ClueCon@127.0.0.1:8021", 10 | level = 'WARNING'; 11 | } 12 | -------------------------------------------------------------------------------- /src/config/sources/nxlog.lua: -------------------------------------------------------------------------------- 1 | -- Forward logs using nxlog (http://nxlog.co) 2 | 3 | SOURCE{ "nxlog", 4 | "net:udp://127.0.0.1:614", 5 | } 6 | 7 | -- Counfigure NXLog to forward logs from system EventLog. 8 | -- 9 | -- 1. Output module 10 | -- 11 | -- 12 | -- Module om_udp 13 | -- Host 127.0.0.1 14 | -- Port 614 15 | -- Exec $raw_event = "EventID: " + $EventID + "; " + $Message; 16 | -- 17 | -- 18 | -- 2. Configure soureces 19 | -- 20 | -- 2.1 On Win2k3 configure Auth fail events 21 | -- 22 | -- 23 | -- Module im_mseventlog 24 | -- Sources Security 25 | -- SavePos TRUE 26 | -- ReadFromLast TRUE 27 | -- Exec if $EventID NOT IN (529, 4625) drop(); 28 | -- 29 | -- 30 | -- 2.2 On Win2k3 configure PHP log events 31 | -- 32 | -- 33 | -- Module im_mseventlog 34 | -- Sources Application 35 | -- SavePos TRUE 36 | -- ReadFromLast TRUE 37 | -- Exec if $EventID != 3 drop(); 38 | -- Exec if $SourceName !~ /^PHP/ drop(); 39 | -- 40 | -- 41 | -- 2.3 on Win >= Vista configure access log 42 | -- When use NTLM auth event 4625 has no IP address 43 | -- 44 | -- 45 | -- Module im_msvistalog 46 | -- SavePos TRUE 47 | -- ReadFromLast TRUE 48 | -- Channel "Microsoft-Windows-RemoteDesktopServices-RdpCoreTS/Operational" 49 | -- 50 | -- 51 | -- 52 | -- 53 | -- 54 | -- 55 | -- 56 | -- 57 | -- 58 | -- 3. Configure route 59 | -- 60 | -- 61 | -- Path winlogon => spylog 62 | -- 63 | -- 64 | -- 65 | -- Path phplog => spylog 66 | -- 67 | -------------------------------------------------------------------------------- /src/config/sources/tshark.lua: -------------------------------------------------------------------------------- 1 | local tshark = 'process:C:/Program Files/Wireshark/tshark.exe' 2 | local args = '-l -i 6 -f "port 5060" ' 3 | .. '-o gui.column.format:Time,%Yt -T fields ' 4 | .. '-e _ws.col.Time -e ip.src -e sip.Method ' 5 | .. '-e ip.dst -e sip.r-uri.host -e sip.r-uri.user ' 6 | 7 | SOURCE{"tshark-sip-request", tshark; 8 | args = args .. '-Y "sip.Request-Line != """""'; 9 | restart = 5; 10 | monitor = 'stdout'; 11 | eol = {'\r\n', false}; 12 | -- env = {}; 13 | -- max_line = 4096 14 | } 15 | 16 | SOURCE{"tshark-sip-options", tshark; 17 | args = args .. '-Y "sip.Method == ""OPTIONS"""'; 18 | restart = 5; 19 | monitor = 'stdout'; 20 | eol = {'\r\n', false}; 21 | -- env = {}; 22 | -- max_line = 4096 23 | } 24 | -------------------------------------------------------------------------------- /src/config/spylog.lua: -------------------------------------------------------------------------------- 1 | WHITE_IP{ 2 | "192.168.1.11"; 3 | "192.168.2.0/24"; 4 | } 5 | 6 | FILTER{ 7 | } 8 | 9 | JAIL{ 10 | purge_interval = 10; 11 | 12 | -- Defaults for all jails 13 | default = { 14 | -- parameters to actions 15 | parameters = { 16 | }; 17 | }; 18 | } 19 | 20 | ACTION{ 21 | } 22 | 23 | LOG{ 24 | level = "trace"; 25 | file = { 26 | log_dir = "./logs", 27 | log_name = "event.log", 28 | max_size = 10 * 1024 * 1024, 29 | close_file = false, 30 | flush_interval = 1, 31 | reuse = true, 32 | }, 33 | zmq = "tcp://127.0.0.1:6060" 34 | }; 35 | 36 | CONNECT{ FILTER = { 37 | JAIL = { 38 | type = 'bind'; 39 | address = 'tcp://127.0.0.1:5555'; 40 | }; 41 | } 42 | } 43 | 44 | CONNECT{ JAIL = { 45 | FILTER = { 46 | type = 'connect'; 47 | address = 'tcp://127.0.0.1:5555'; 48 | }; 49 | ACTION = { 50 | type = 'bind'; 51 | address = 'tcp://127.0.0.1:5556'; 52 | }; 53 | } 54 | } 55 | 56 | CONNECT{ ACTION = { 57 | JAIL = { 58 | type = 'connect'; 59 | address = 'tcp://127.0.0.1:5556'; 60 | }; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/filter/init.lua: -------------------------------------------------------------------------------- 1 | -- Configuration file for LuaService 2 | 3 | return { 4 | tracelevel = 7, 5 | name = "spylog_filter", 6 | display_name = "SpyLog - Filter", 7 | script = "main.lua", 8 | lua_cpath = '!\\..\\lib\\?.dll', 9 | lua_path = '!\\..\\lib\\?.lua;' .. 10 | '!\\..\\lib\\?\\init.lua;' .. 11 | '!\\lib\\?.lua'; 12 | } 13 | -------------------------------------------------------------------------------- /src/filter/lib/spylog/eventlog.lua: -------------------------------------------------------------------------------- 1 | -- Decode MS EventLog traps. 2 | 3 | local bit = require "bit32" 4 | 5 | local EventLog = {} 6 | 7 | local EventLogOID = '1.3.6.1.4.1.311.1.13.1.' 8 | 9 | local EventLogTextOID = EventLogOID .. '9999.1' 10 | 11 | local EventLogTextOID_ = EventLogTextOID .. '.' 12 | 13 | local eventlog_fields = { 14 | [ '1' ] = 'text'; 15 | [ '2' ] = 'userId'; 16 | [ '3' ] = 'system'; 17 | [ '4' ] = 'type'; 18 | [ '5' ] = 'category'; 19 | -- [ '6' ] = 'var1'; 20 | -- [ '7' ] = 'var2'; 21 | -- [ '8' ] = 'var3'; 22 | -- [ '9' ] = 'var4'; 23 | -- [ '10' ] = 'var5'; 24 | -- [ '11' ] = 'var6'; 25 | -- [ '12' ] = 'var7'; 26 | -- [ '13' ] = 'var8'; 27 | -- [ '14' ] = 'var9'; 28 | -- [ '15' ] = 'var10'; 29 | -- [ '16' ] = 'var11'; 30 | -- [ '17' ] = 'var12'; 31 | -- [ '18' ] = 'var13'; 32 | -- [ '19' ] = 'var14'; 33 | -- [ '20' ] = 'var15'; 34 | } 35 | 36 | local eventlog_severity = { 37 | [0] = 'Success'; 38 | [1] = 'Informational'; 39 | [2] = 'Warning'; 40 | [3] = 'Error'; 41 | } 42 | 43 | local eventlog_types = { 44 | ['1' ] = 'Error'; 45 | ['2' ] = 'Warning'; 46 | ['4' ] = 'Informational'; 47 | ['8' ] = 'Success Audit'; 48 | ['16'] = 'Failure Audit'; 49 | } 50 | 51 | local function decode_event_log_oid(str) 52 | if string.find(str, EventLogOID, nil, true) ~= 1 then 53 | return 54 | end 55 | 56 | str = string.sub(str, #EventLogOID + 1) 57 | 58 | local id, data = string.match(str, '^(%d+)(%.[%d%.]+)$') 59 | 60 | if id == '9999' then 61 | id, data = string.match(data, '^%.(%d+)(%.[%d%.]+)$') 62 | local name = eventlog_fields[id] 63 | if name then return name, data end 64 | else 65 | -- https://support.microsoft.com/en-us/kb/318464 66 | local len = tonumber(id) 67 | local pat = "^(" .. ("%.%d+"):rep(len) .. ")(.*)$" 68 | str, data = string.match(data, pat) 69 | if str then 70 | str = string.gsub(str, "(%.)(%d+)", function(_, ch) 71 | return string.char(tonumber(ch)) 72 | end) 73 | return 'source', str, data 74 | end 75 | end 76 | end 77 | 78 | local function Specific2EventID(v) 79 | -- https://support.microsoft.com/en-us/kb/160969 80 | -- Low 16 bits is Event ID. 81 | -- Hi 2 bits is default severity. 82 | -- 83 | -- But other bits stil unknown. 84 | -- Also MS send this value as signed integer so it 85 | -- not match to value from evntwin `Trap Specific ID` 86 | -- E.g. Trap Specific ID `2147483651` converts to `-2147483645` 87 | -- 88 | 89 | local id = bit.band(0xFFFF, v) 90 | local severity = eventlog_severity[bit.rshift(v, 30)] 91 | 92 | return id, severity 93 | end 94 | 95 | EventLog.trap2event = function(t) 96 | if not t.enterprise then return end 97 | 98 | local name, value = decode_event_log_oid(t.enterprise) 99 | if name ~= 'source' then return end 100 | 101 | -- In threory we also should test t.generic==6. 102 | -- In other cases it not valid. 103 | local EventID, Severity = Specific2EventID(t.specific) 104 | 105 | local event = { 106 | id = EventID; 107 | severity = Severity; 108 | source = value; 109 | agent = t.agent; 110 | time = t.time; 111 | -- do not use 112 | -- _community = t.community; 113 | -- _generic = t.generic; 114 | -- _specific = t.specific; 115 | } 116 | 117 | for i = 1, #t.data do 118 | local name, rest = decode_event_log_oid(t.data[i][1]) 119 | if name then event[name] = t.data[i][2] end 120 | end 121 | 122 | -- e.g. for 529 123 | -- event.severity='Success' 124 | -- event.type='Failure Audit' 125 | -- so I think `type` is more accurate 126 | 127 | event.type = event.type and eventlog_types[event.type] or event.type 128 | 129 | return event 130 | end 131 | 132 | local function SourceFilter(source) 133 | local equal = string.sub(source, -1) ~= '*' 134 | if not equal then source = string.sub(source, 1, -2) end 135 | 136 | if not equal then 137 | source = "^" .. string.gsub(EventLogOID, "%.", "%%.") .. "%d+" .. string.gsub(source, '.', function(ch) 138 | return '%.' .. tostring(string.byte(ch)) 139 | end) .. "%.[%.%d]+$" 140 | else 141 | source = EventLogOID .. string.format("%d", #source) .. string.gsub(source, '.', function(ch) 142 | return '.' .. tostring(string.byte(ch)) 143 | end) 144 | end 145 | 146 | if equal then 147 | return function(t) return t.enterprise == source end 148 | end 149 | 150 | return function(t) return string.find(t.enterprise, source) end 151 | end 152 | 153 | local function EventIDFilter(id) 154 | local set = {} 155 | if type(id) ~= 'table' then set[id] = true else 156 | for k, v in ipairs(id) do set[v] = true end 157 | end 158 | return function(t) return set[bit.band(0xFFFF, t.specific)] end 159 | end 160 | 161 | local function SeverityFilter(id) 162 | local set = {} 163 | if type(id) ~= 'table' then set[id] = true else 164 | for k, v in ipairs(id) do set[v] = true end 165 | end 166 | return function(t) return set[bit.rshift(t.specific, 32)] end 167 | end 168 | 169 | EventLog.BuildFilter = function(t) 170 | if type(t[1]) == 'string' then t = {t} end 171 | 172 | local f = {} for _, v in ipairs(t) do 173 | assert(type(v[1]) == 'string', 'source name required') 174 | 175 | local source = SourceFilter(v[1]) 176 | local id = v[2] and EventIDFilter(v[2]) 177 | 178 | if not id then f[#f+1] = function(t) return source(t) and id(t) end 179 | else f[#f+1] = source end 180 | end 181 | 182 | return function(t) 183 | for i = 1, #f do 184 | if f[i](t) then return true end 185 | end 186 | end 187 | end 188 | 189 | EventLog.trap2text = function(t) 190 | for i = 1, #t.data do 191 | local oid = t.data[i][1] 192 | if (string.find(oid, EventLogTextOID_, nil, true) == 1) or (EventLogTextOID == oid) then 193 | return t.data[i][2] 194 | end 195 | end 196 | end 197 | 198 | return EventLog -------------------------------------------------------------------------------- /src/filter/lib/spylog/filemon.lua: -------------------------------------------------------------------------------- 1 | local uv = require "lluv" 2 | local ut = require "lluv.utils" 3 | local path = require "path" 4 | 5 | local EOF = uv.error("LIBUV", uv.EOF); 6 | 7 | local AppendFileMonitor = ut.class() do 8 | 9 | local DEFAULT_BUFFER_SIZE = 4096 10 | 11 | local POLL_CREATE_FILE_INTERVAL = 5000 12 | 13 | function AppendFileMonitor:__init(opt) 14 | opt = opt or {} 15 | self._fname = nil 16 | self._file = nil 17 | self._event = nil 18 | self._buffer = uv.buffer(opt.buffer_size or DEFAULT_BUFFER_SIZE) 19 | self._offset = nil 20 | self._reading = nil 21 | self._opening = nil 22 | if opt.skeep == true or opt.skeep == nil then 23 | self._skeep = nil 24 | else 25 | self._skeep = true -- true mean do not skeep 26 | end 27 | 28 | if opt.poll then 29 | assert(type(opt.poll) == 'number', 'poll should be number but got: ' .. type(opt.poll)) 30 | assert(opt.poll >= 1, 'poll should be at least one second') 31 | self._poll_interval = opt.poll * 1000 32 | end 33 | 34 | self._on_read_proxy = function(...) return self:_on_read(...) end 35 | 36 | return self 37 | end 38 | 39 | function AppendFileMonitor:_do_read() 40 | if (not self._reading) and (self._file) then 41 | self._reading = true 42 | self._file:read(self._buffer, self._offset, self._on_read_proxy) 43 | end 44 | end 45 | 46 | function AppendFileMonitor:_do_open() 47 | if self._opening then return end 48 | 49 | self._opening = true 50 | 51 | if self._skeep then 52 | self._skeep = path.size(self._fname) or 0 53 | end 54 | 55 | uv.fs_open(self._fname, "r", function(file, err, path) 56 | self._opening = false 57 | self:_on_open(err, file) 58 | end) 59 | end 60 | 61 | function AppendFileMonitor:_on_read(file, err, buf, size) 62 | self._reading = false 63 | 64 | if err and err:name() ~= 'EOF' then self:_on_error(err) end 65 | 66 | if err or size == 0 then return end 67 | 68 | self._offset = self._offset + size 69 | local data = buf:to_s(size) 70 | 71 | self:_do_read() 72 | 73 | self:_cb(nil, data, self._offset) 74 | end 75 | 76 | function AppendFileMonitor:_on_open(err, file) 77 | self._offset = self._skeep or 0 78 | self._skeep = false -- only once 79 | 80 | if err then return self:_on_error(err) end 81 | 82 | self._file = file 83 | 84 | self:_do_read() 85 | end 86 | 87 | function AppendFileMonitor:_on_rename() 88 | if self._file then 89 | self._file:close() 90 | self._file = nil 91 | end 92 | end 93 | 94 | function AppendFileMonitor:_on_change() 95 | self:_do_read() 96 | end 97 | 98 | function AppendFileMonitor:_on_error(err) 99 | self:_cb(err) 100 | end 101 | 102 | function AppendFileMonitor:open(fname, cb) 103 | self._fname = fname 104 | self._cb = cb 105 | 106 | if (not self._file) and path.exists(fname) then 107 | -- We skip existed content of first file. 108 | -- e.g. log already has data for a week and we just start filter service. 109 | if self._skeep == nil then 110 | self._skeep = true 111 | end 112 | self:_do_open() 113 | end 114 | 115 | self._event = uv.fs_event() 116 | 117 | local timer 118 | 119 | local function on_event(_, err, _, ev) 120 | -- This function may be called in case of error start. 121 | -- E.g. if file does not exists we get `ENOENT` error. 122 | if err then 123 | -- If timer exists that means we still try do first successful start 124 | if timer then 125 | -- we have to call `stop` in other case we get `EINVAL` 126 | -- when we call start again 127 | self._event:stop() 128 | timer:again(POLL_CREATE_FILE_INTERVAL) 129 | if err:name() == 'ENOENT' then 130 | self._skeep = false 131 | end 132 | end 133 | 134 | -- ignore `ENOENT` if we do not start yet 135 | if not (timer and err:name() == 'ENOENT') then 136 | self:_on_error(err) 137 | end 138 | return 139 | end 140 | 141 | timer = nil 142 | 143 | if ev == uv.RENAME then 144 | return self:_on_rename() 145 | end 146 | 147 | if self._file then 148 | if ev == uv.CHANGE then self:_on_change() end 149 | return 150 | end 151 | 152 | if path.exists(fname) then 153 | return self:_do_open() 154 | end 155 | end 156 | 157 | timer = uv.timer():start(function() 158 | timer:stop() 159 | self._event:start(fname, on_event) 160 | end) 161 | 162 | if self._poll_interval then 163 | self._poll_event = uv.fs_poll():start(fname, self._poll_interval, function()end) 164 | end 165 | 166 | end 167 | 168 | function AppendFileMonitor:close(cb) 169 | self:stop(function() 170 | self._file:close() 171 | if self._poll_event then 172 | self._poll_event:close() 173 | end 174 | self._event:close(function() 175 | cb(self) 176 | end) 177 | end) 178 | end 179 | 180 | end 181 | 182 | return AppendFileMonitor 183 | -------------------------------------------------------------------------------- /src/filter/lib/spylog/filter/manager.lua: -------------------------------------------------------------------------------- 1 | local config = require "spylog.config" 2 | local log = require "spylog.log" 3 | local path = require "path" 4 | local uv = require "lluv" 5 | local ut = require "lluv.utils" 6 | local EventLog = require "spylog.eventlog" 7 | 8 | local function append(t, v) 9 | t[#t + 1] = v 10 | return t 11 | end 12 | 13 | local Source = ut.class() do 14 | 15 | local function apply_filter(jail, filters, filter, ...) 16 | for i = 1, #filters do 17 | local capture = filter(filters[i], ...) 18 | if capture then 19 | jail(filters[i], capture) 20 | if filters[i].stop then 21 | break 22 | end 23 | end 24 | end 25 | end 26 | 27 | -- @static 28 | function Source.decode_source(source) 29 | local source_name 30 | if type(source) == 'string' then 31 | source_name = config.SOURCES and config.SOURCES[source] and source 32 | source = config.SOURCES and config.SOURCES[source] or source 33 | end 34 | 35 | local source_string = (type(source) == 'table') and source[1] or source 36 | 37 | assert(type(source_string) == 'string', "source string required") 38 | 39 | local source_type, source_info = ut.split_first(source_string, ':', true) 40 | 41 | assert(source_info, string.format('invalid source string: %s', source_string)) 42 | 43 | if source_type == 'file' then 44 | source_info = path.fullpath(source_info) 45 | source_string = source_type..":"..source_info 46 | if type(source) == 'table' then source[1] = source_string end 47 | end 48 | 49 | source_name = source_name or source_string 50 | 51 | return source_name, source_string, source_type, source_info, (type(source) == 'table') and source or nil 52 | end 53 | 54 | function Source:__init(source) 55 | local source_name, source_string, source_type, source_info, source_opt = Source.decode_source(source) 56 | 57 | if source_name == source_string then 58 | log.info("create new source: %s", source_name) 59 | else 60 | log.info("create new source: %s/%s", source_name, source_string) 61 | end 62 | 63 | self._name = source_name 64 | self._string = source_string 65 | self._type = source_type 66 | self._info = source_info 67 | self._opt = source_opt 68 | if self._opt then 69 | self._opt.__name = source_name 70 | self._opt.__type = source_type 71 | end 72 | self._filters = {} 73 | 74 | return self 75 | end 76 | 77 | function Source:start(jail) 78 | if not self._m then 79 | log.info("start source monitor for: `%s`", self._string) 80 | 81 | local m = require ("spylog.monitor." .. self._type) 82 | local filters = self._filters 83 | m.monitor(self._info, self._opt, function(...) 84 | apply_filter(jail, filters, m.filter, ...) 85 | end) 86 | 87 | self._m = m 88 | end 89 | end 90 | 91 | function Source:add(filter) 92 | if self._type == 'trap' then 93 | if type(filter.trap) ~= 'table' then 94 | filter.trap = {[filter.trap] = true} 95 | else 96 | for i =1, #filter.trap do 97 | filter.trap[filter.trap[i]] = true 98 | end 99 | end 100 | end 101 | 102 | if self._type == 'eventlog' then 103 | assert(type(filter.events) == 'table', 'No events list for eventlog filter') 104 | filter.events = EventLog.BuildFilter(filter.events) 105 | end 106 | 107 | log.info("attach filter `%s` to source `%s`", filter.name, self._name) 108 | 109 | append(self._filters, filter) 110 | end 111 | 112 | end 113 | 114 | local FilterManager = ut.class() do 115 | 116 | function FilterManager:__init() 117 | self._sources = {} 118 | 119 | return self 120 | end 121 | 122 | function FilterManager:source(filter) 123 | local source_name = Source.decode_source(filter.source) 124 | 125 | local s = self._sources[source_name] 126 | if not s then 127 | s = Source.new(filter.source) 128 | self._sources[source_name] = s 129 | end 130 | 131 | return s 132 | end 133 | 134 | function FilterManager:add(filter) 135 | local source = self:source(filter) 136 | source:add(filter) 137 | return self 138 | end 139 | 140 | function FilterManager:start(jail) 141 | local n = 0 142 | for _, source in pairs(self._sources) do 143 | source:start(jail) 144 | n = n + 1 145 | end 146 | return n 147 | end 148 | 149 | end 150 | 151 | return FilterManager -------------------------------------------------------------------------------- /src/filter/lib/spylog/filter/regex.lua: -------------------------------------------------------------------------------- 1 | local iputil = require "spylog.iputil" 2 | local log = require "spylog.log" 3 | 4 | local ENGINES = { 5 | default = function(filter) 6 | local failregex = filter.failregex 7 | if type(failregex) == 'string' then 8 | failregex = {failregex} 9 | end 10 | 11 | local function rmatch(i, t, ...) 12 | if ... then return i-1, ... end 13 | if failregex[i] then 14 | return rmatch(i+1, t, string.match(t, failregex[i])) 15 | end 16 | end 17 | 18 | local match = function(t) 19 | if (not filter.hint) or string.find(t, filter.hint, nil, true) then 20 | return rmatch(1, t) 21 | end 22 | end 23 | 24 | if filter.ignoreregex then 25 | local ignoreregex = filter.ignoreregex 26 | if type(ignoreregex) == 'string' then 27 | ignoreregex = {ignoreregex} 28 | end 29 | 30 | local function ignore(t, rid, dt, ip, ...) 31 | if dt then 32 | for i = 1, #ignoreregex do 33 | if string.find(t, ignoreregex[i]) then 34 | log.debug("[%s] match `%s` but excluded by ignoreregex", filter.name, ip) 35 | return 36 | end 37 | end 38 | end 39 | return rid, dt, ip, ... 40 | end 41 | 42 | local pass = match 43 | match = function(t) 44 | return ignore(t, pass(t)) 45 | end 46 | end 47 | 48 | return match 49 | end; 50 | 51 | pcre = function(filter) 52 | local rex = require "rex_pcre" 53 | local failregex = {} 54 | 55 | if type(filter.failregex) == "string" then 56 | failregex[1] = assert(rex.new(filter.failregex)) 57 | else 58 | for i = 1, #filter.failregex do 59 | failregex[i] = assert(rex.new(filter.failregex[i])) 60 | end 61 | end 62 | 63 | local function rmatch(i, t, ...) 64 | if ... then return i-1, ... end 65 | if failregex[i] then 66 | return rmatch(i+1, t, failregex[i]:match(t)) 67 | end 68 | end 69 | 70 | local match = function(t) 71 | if (not filter.hint) or string.find(t, filter.hint, nil, true) then 72 | return rmatch(1, t) 73 | end 74 | end 75 | 76 | if filter.ignoreregex then 77 | local ignoreregex = {} 78 | 79 | if type(filter.ignoreregex) == "string" then 80 | ignoreregex[1] = assert(rex.new(filter.ignoreregex)) 81 | else 82 | for i = 1, #filter.ignoreregex do 83 | ignoreregex[i] = assert(rex.new(filter.ignoreregex[i])) 84 | end 85 | end 86 | 87 | local function ignore(t, rid, dt, ip, ...) 88 | if rid then 89 | for i = 1, #ignoreregex do 90 | if ignoreregex[i]:find(t) then 91 | log.debug("[%s] match `%s` but excluded by ignoreregex", filter.name, ip or dt) 92 | return 93 | end 94 | end 95 | end 96 | return rid, dt, ip, ... 97 | end 98 | 99 | local pass = match 100 | match = function(t) 101 | return ignore(t, pass(t)) 102 | end 103 | end 104 | 105 | return match 106 | end; 107 | } 108 | 109 | local defaul_result = function(captures, result, cidr, rule_id, date, host) 110 | if not rule_id then return end 111 | 112 | if not host then host, date = date, os.date("%Y-%m-%d %H:%M:%S") end 113 | 114 | if iputil.find_cidr(host, cidr) then 115 | log.debug("[%s] match `%s` but excluded by cidr", result.filter, host) 116 | return 117 | end 118 | 119 | result.date, result.host = date, host 120 | 121 | return result 122 | end 123 | 124 | local capture_result = function(captures, result, cidr, rule_id, ...) 125 | if not rule_id then return end 126 | 127 | local capture = captures[rule_id] 128 | result = result[rule_id] 129 | result.date = nil 130 | 131 | for i = 1, #capture do 132 | local name = capture[i] 133 | result[name] = select(i, ...) 134 | end 135 | 136 | if not result.date then result.date = os.date("%Y-%m-%d %H:%M:%S") end 137 | 138 | if result.host and iputil.find_cidr(result.host, cidr) then 139 | log.debug("[%s] match `%s` but excluded by cidr", result.filter, result.host) 140 | return 141 | end 142 | 143 | return result 144 | end 145 | 146 | -- Build `match` function wich returns either table with captures or nil. 147 | -- `match` function may return same table with different content 148 | -- required and not redefined filds is `date` and `filter` 149 | -- `filter` field can not be captured and set based on fileter name only. 150 | local function build_rex_filter(filter) 151 | local engine = ENGINES.default 152 | 153 | local search_fn 154 | 155 | if filter.engine then 156 | if type(filter.engine) == 'string' then 157 | engine = assert(ENGINES[filter.engine], "Unknown engine: " .. filter.engine) 158 | elseif type(filter.engine) == 'function' then 159 | engine = filter.engine 160 | end 161 | end 162 | 163 | -- build function wich do capture data from log string 164 | search_fn = assert(engine and engine(filter), "Internal error while build filter: " .. filter.name .. " engine: " .. tostring(filter.engine or 'default') ) 165 | 166 | -- build index to exclude IP 167 | local exclude_cidr = iputil.load_cidrs(filter.exclude or {}) 168 | 169 | -- check if we use named captures 170 | local captures 171 | if filter.capture then 172 | captures = filter.capture 173 | 174 | if type(captures[1]) ~= 'table' then 175 | captures = {captures} 176 | end 177 | 178 | local failregex = type(filter.failregex) == 'string' and {filter.failregex} or filter.failregex 179 | 180 | for i = 1, #captures do assert(failregex[i], '[' .. filter.name .. ']' .. 'No regex for capture #' .. i) end 181 | for i = 1, #failregex do assert(captures[i], '[' .. filter.name .. ']' .. 'No capture for regex #' .. i) end 182 | end 183 | 184 | local result, tmp 185 | if captures then 186 | result, tmp = capture_result, {} 187 | for i = 1, #captures do tmp[#tmp + 1] = {filter = filter.name} end 188 | else 189 | result, tmp = defaul_result, {filter = filter.name} 190 | end 191 | 192 | 193 | return function(t) 194 | return result(captures, tmp, exclude_cidr, search_fn(t)) 195 | end 196 | end 197 | 198 | return build_rex_filter -------------------------------------------------------------------------------- /src/filter/lib/spylog/monitor/esl.lua: -------------------------------------------------------------------------------- 1 | local uv = require "lluv" 2 | local ut = require "lluv.utils" 3 | local Esl = require "lluv.esl" 4 | local log = require "spylog.log" 5 | 6 | local FS_LOG_LEVELS_NAMES = { 7 | ['0'] = "CONSOLE"; 8 | ['1'] = "ALERT"; 9 | ['2'] = "CRIT"; 10 | ['3'] = "ERR"; 11 | ['4'] = "WARNING"; 12 | ['5'] = "NOTICE"; 13 | ['6'] = "INFO"; 14 | ['7'] = "DEBUG"; 15 | } 16 | 17 | local FS_LOG_LEVELS = { 18 | CONSOLE = '0'; 19 | ALERT = '1'; 20 | CRIT = '2'; 21 | ERR = '3'; 22 | WARNING = '4'; 23 | NOTICE = '5'; 24 | INFO = '6'; 25 | DEBUG = '7'; 26 | } 27 | 28 | local function esl_monitor(endpoint, opt, cb) 29 | local auth, address, port = ut.split_first(endpoint, "@", true) 30 | if not address then 31 | auth, address = 'ClueCon', auth 32 | end 33 | 34 | address, port = ut.split_first(address, ":", true) 35 | port = tonumber(port) or 8021 36 | 37 | local log_header = string.format('[esl] [%s:%d]', address, port) 38 | 39 | local reconnect_timeout = (opt and opt.reconnect or 30) 40 | local level, level_name = string.upper(tostring(opt and opt.level or 'WARNING')) 41 | level = FS_LOG_LEVELS[level] or level 42 | level_name = assert(FS_LOG_LEVELS_NAMES[level], 'Unknon log level: ' .. level) 43 | 44 | local esl = Esl.Connection{address, port, auth, 45 | reconnect = reconnect_timeout; no_execute_result = true; no_bgapi = true; 46 | } 47 | 48 | esl:on('esl::reconnect', function(self, eventName) 49 | log.info("%s connected", log_header) 50 | 51 | self:log(level_name, function(self, err, event) 52 | if err then 53 | log.error('%s log command: %s', log_header, tostring(err)) 54 | return 55 | end 56 | 57 | local ok, status, msg = event:getReply() 58 | log.info('%s log command: %s %s', log_header, tostring(status), tostring(msg)) 59 | end) 60 | end) 61 | 62 | esl:on('esl::disconnect', function(self, eventName, err) 63 | log.info("%s disconnected: %s", log_header, tostring(err)) 64 | end) 65 | 66 | esl:on('esl::event::LOG', function(self, eventName, event) 67 | cb(event) 68 | end) 69 | 70 | esl:on('esl::error::**', function(self, eventName, err) 71 | log.error('%s %s: %s', log_header, eventName, tostring(err)) 72 | end) 73 | 74 | esl:on('esl::close', function(self, eventName, err) 75 | log.debug('%s %s: %s', log_header, eventName, tostring(err)) 76 | end) 77 | 78 | esl:open() 79 | end 80 | 81 | local function esl_filter(filter, event) 82 | -- local file = event:getHeader('Log-File') 83 | -- local line = event:getHeader('Log-Line') 84 | -- local func = event:getHeader('Log-Func') 85 | local level = event:getHeader('Log-Level') 86 | -- local udata = event:getHeader('User-Data') 87 | -- local chann = event:getHeader('Text-Channel') 88 | 89 | -- this is console 90 | if level == '0' then return end 91 | 92 | local msg = event:getBody() 93 | 94 | return filter.match(msg) 95 | end 96 | 97 | return { 98 | monitor = esl_monitor; 99 | filter = esl_filter; 100 | } 101 | -------------------------------------------------------------------------------- /src/filter/lib/spylog/monitor/eventlog.lua: -------------------------------------------------------------------------------- 1 | local ut = require "lluv.utils" 2 | local log = require "spylog.log" 3 | local EventLog = require "spylog.eventlog" 4 | local trap = require "spylog.monitor.trap" 5 | 6 | local function trap_monitor(endpoint, opt, cb, log_header) 7 | local proto, address, port = ut.split_first(endpoint, "://", true) 8 | assert(proto == 'udp') 9 | 10 | address, port = ut.split_first(address,":", true) 11 | port = tonumber(port) or 162 12 | 13 | endpoint = string.format('%s://%s:%d', proto, address, port) 14 | 15 | local log_header = log_header or string.format('[eventlog/%s] [%s:%d]', proto, address, port) 16 | 17 | return trap.monitor(endpoint, opt, cb, log_header) 18 | end 19 | 20 | local function trap_filter(filter, t) 21 | local msg = filter.events(t) and EventLog.trap2text(t) 22 | if type(msg) == 'string' then 23 | return filter.match(msg) 24 | end 25 | end 26 | 27 | return { 28 | monitor = trap_monitor; 29 | filter = trap_filter; 30 | } -------------------------------------------------------------------------------- /src/filter/lib/spylog/monitor/file.lua: -------------------------------------------------------------------------------- 1 | local uv = require "lluv" 2 | local ut = require "lluv.utils" 3 | local date = require "date" 4 | local log = require "spylog.log" 5 | local filemon = require "spylog.filemon" 6 | 7 | local MAX_LINE_LENGTH = 4096 8 | 9 | local unpack = unpack or table.unpack 10 | 11 | local function file_monitor(fname, opt, cb) 12 | local log_header = string.format('[file:%s]', fname) 13 | 14 | local max_line = opt and opt.max_line or MAX_LINE_LENGTH 15 | local eol = opt and opt.eol or {'\r*\n', true} 16 | if type(eol) == 'string' then eol = {eol, false} end 17 | local buffer = ut.Buffer.new(unpack(eol, 1, 2)) 18 | local monitor = filemon.new(opt) 19 | 20 | monitor:open(fname, function(self, err, data) 21 | if err then 22 | return log.error("%s READ FILE: %s", log_header, tostring(err)) 23 | end 24 | 25 | buffer:append(data) 26 | while true do 27 | local line = buffer:read_line() 28 | if not line then 29 | if buffer.size and (buffer:size() > max_line) then 30 | log.alert('%s get too long line: %d `%s...`', log_header, buffer:size(), buffer:read_n(256)) 31 | buffer:reset() 32 | end 33 | break 34 | end 35 | cb(line) 36 | end 37 | end) 38 | end 39 | 40 | local function file_filter(filter, t) 41 | return filter.match(t) 42 | end 43 | 44 | return { 45 | monitor = file_monitor; 46 | filter = file_filter; 47 | } 48 | -------------------------------------------------------------------------------- /src/filter/lib/spylog/monitor/net.lua: -------------------------------------------------------------------------------- 1 | local uv = require "lluv" 2 | local ut = require "lluv.utils" 3 | local log = require "spylog.log" 4 | 5 | local MAX_LINE_LENGTH = 4096 6 | 7 | local function tcp_cli_monitor(proto, address, opt, cb, log_header) 8 | local address, port = ut.split_first(address, ":", true) 9 | port = assert(tonumber(port), 'port is required') 10 | 11 | local log_header = log_header or string.format('[net/%s] [%s:%d]', proto, address, port) 12 | 13 | local eol = opt and opt.eol or '\r\n' 14 | local reconnect_timeout = (opt and opt.reconnect or 30) * 1000 15 | local max_line = opt and opt.max_line or MAX_LINE_LENGTH 16 | 17 | local reconnect_timer = uv.timer(0) 18 | 19 | local function connect() 20 | log.info("%s connecting ...", log_header) 21 | uv.tcp():connect(address, port, function(self, err) 22 | if err then 23 | self:close() 24 | log.error("%s can not connect: %s", log_header, tostring(err)) 25 | return reconnect_timer:again(reconnect_timeout) 26 | end 27 | 28 | local buffer = ut.Buffer.new(eol) 29 | 30 | self:start_read(function(self, err, data) 31 | if err then 32 | self:close() 33 | log.error("%s recv error: %s", log_header, tostring(err)) 34 | return reconnect_timer:again(reconnect_timeout) 35 | end 36 | 37 | buffer:append(data) 38 | while true do 39 | local line = buffer:read_line() 40 | if not line then 41 | if buffer.size and (buffer:size() > max_line) then 42 | log.alert('%s get too long line: %d `%s...`', log_header, buffer:size(), buffer:read_n(256)) 43 | buffer:reset() 44 | end 45 | break 46 | end 47 | cb(line) 48 | end 49 | end) 50 | 51 | log.info("%s connected", log_header) 52 | end) 53 | end 54 | 55 | reconnect_timer:start(function(self) 56 | self:stop() 57 | connect() 58 | end) 59 | end 60 | 61 | local function udp_srv_monitor(proto, address, opt, cb, log_header) 62 | local address, port = ut.split_first(address, ":", true) 63 | port = assert(tonumber(port), 'port is required') 64 | 65 | local log_header = log_header or string.format('[net/%s] [%s:%d]', proto, address, port) 66 | 67 | uv.udp():bind(address, port, function(self, err) 68 | if err then 69 | self:close() 70 | return log.fatal("%s can not bind: %s", log_header, tostring(err)) 71 | end 72 | 73 | log.info("%s started", log_header) 74 | 75 | self:start_recv(function(self, err, data, host, port) 76 | if err then 77 | return log.error("%s recv: %s", log_header, tostring(err)) 78 | end 79 | 80 | cb(data) 81 | end) 82 | end) 83 | 84 | log.info("%s starting ...", log_header) 85 | end 86 | 87 | local function net_monitor(endpoint, opt, cb, log_header) 88 | local proto, address = ut.split_first(endpoint, "://", true) 89 | 90 | if proto == 'tcp' then 91 | return tcp_cli_monitor(proto, address, opt, cb, log_header) 92 | end 93 | 94 | if proto == 'udp' then 95 | return udp_srv_monitor(proto, address, opt, cb, log_header) 96 | end 97 | 98 | log.fatal('[net] unknown protocol: %', proto) 99 | 100 | assert(false) 101 | end 102 | 103 | local function net_filter(filter, msg) 104 | return filter.match(msg) 105 | end 106 | 107 | return { 108 | monitor = net_monitor; 109 | filter = net_filter; 110 | } 111 | -------------------------------------------------------------------------------- /src/filter/lib/spylog/monitor/process.lua: -------------------------------------------------------------------------------- 1 | local uv = require "lluv" 2 | local ut = require "lluv.utils" 3 | local spawn = require "spylog.spawn".spawn_ex 4 | local log = require "spylog.log" 5 | local Args = require "spylog.args" 6 | local path = require "path" 7 | local environ = require "environ.process" 8 | 9 | local MAX_LINE_LENGTH = 4096 10 | 11 | local function build_cb(buffer, cb) 12 | return function(data) 13 | buffer:append(data) 14 | while true do 15 | local str = buffer:read("*l") 16 | if not str then break end 17 | cb(str) 18 | end 19 | end 20 | end 21 | 22 | local function process_monitor(cmd, opt, cb) 23 | local opt = opt or {} 24 | 25 | local proto = 'process' 26 | 27 | local args, tail = opt.args 28 | if args then 29 | if type(args) == 'string' then 30 | args, tail = Args.split(args) 31 | 32 | if not args then 33 | log.error("[process][%s] Can not parse argument string: %s", cmd, opt.args) 34 | return false 35 | end 36 | end 37 | else 38 | local tmp_cmd = cmd 39 | cmd, args, tail = Args.split_command(cmd) 40 | if not cmd then 41 | log.error("[process] Can not parse command string: %s", cmd) 42 | end 43 | end 44 | 45 | cmd = path.normalize(cmd) 46 | 47 | local source_name = opt.__name or (cmd and path.basename(cmd)) or 'unknown' 48 | local log_header = '[' .. proto .. ']' ..'[' .. source_name .. ']' 49 | 50 | if tail and #tail > 0 then 51 | log.warning("%s Unused command arguments: %q", log_header, tail) 52 | end 53 | 54 | local env 55 | if opt.env then 56 | env = {} 57 | for k, v in pairs(opt.env) do 58 | if type(k) == 'number' then 59 | env[#env + 1] = v .. '=' .. environ.getenv(v) 60 | else 61 | env[#env + 1] = k .. '=' .. environ.expand(v) 62 | end 63 | end 64 | end 65 | 66 | local eol 67 | if type(opt.eol) == 'table' then 68 | eol = opt.eol 69 | else 70 | eol = {opt.eol} 71 | end 72 | 73 | local restart = opt.restart or 10 74 | if not tonumber(restart) then 75 | log.warning("%s restart timeout have to be a number but got: %q", log_header, tostring(restart)) 76 | restart = 10 77 | end 78 | 79 | restart = tonumber(restart) 80 | 81 | if restart < 1 then 82 | log.warning("%s restart timout have to be greater than 1 but got: %d", log_header, restart) 83 | end 84 | 85 | restart = restart * 1000 86 | 87 | local max_line = opt and opt.max_line or MAX_LINE_LENGTH 88 | if not tonumber(max_line) then 89 | log.warning("%s max_line option have to be a number but got: %q", log_header, tostring(restart)) 90 | max_line = MAX_LINE_LENGTH 91 | end 92 | 93 | max_line = tonumber(max_line) 94 | 95 | if max_line < MAX_LINE_LENGTH then 96 | log.warning("%s too short `max_line` option: %d, use default one: %d", log_header, max_line, MAX_LINE_LENGTH) 97 | end 98 | 99 | local monitor = {} 100 | if type(opt.monitor) == 'string' then 101 | monitor[opt.monitor] = 'true' 102 | elseif type(opt.monitor) == 'table' then 103 | for i = 1, #opt.monitor do 104 | monitor[ opt.monitor[i] ] = true 105 | end 106 | end 107 | if not (monitor.sdtout or monitor.sdterr) then 108 | monitor.stdout = true 109 | end 110 | 111 | local stdout_buffer, on_stdout, stderr_buffer, on_stderr 112 | if monitor.stdout then 113 | stdout_buffer = ut.Buffer.new(eol[1], eol[2]) 114 | on_stdout = build_cb(stdout_buffer, cb) 115 | end 116 | 117 | if monitor.stderr then 118 | stderr_buffer = ut.Buffer.new(eol[1], eol[2]) 119 | on_stderr = build_cb(stderr_buffer, cb) 120 | end 121 | 122 | local restart_timer = uv.timer() 123 | 124 | local function start() 125 | log.info('%s creating process: %s %s', log_header, cmd, (args and table.concat(args, ' ') or '')) 126 | 127 | if stdout_buffer then stdout_buffer:reset() end 128 | if stderr_buffer then stderr_buffer:reset() end 129 | 130 | local pid 131 | local process, err = spawn(cmd, args, env, nil, 132 | function(typ, err, status, signal) 133 | if typ == 'exit' then 134 | restart_timer:again(restart) 135 | log.warning('%s process with pid %s exit %s; status: %s; signal: %s', log_header, tostring(pid), tostring(err), tostring(status), tostring(signal)) 136 | end 137 | end, 138 | on_stdout, on_stderr 139 | ) 140 | 141 | if not process then 142 | log.error('%s can not spawn process: %s', log_header, tostring(err)) 143 | restart_timer:again(restart) 144 | end 145 | 146 | pid = err 147 | log.info('%s created process with pid %s', log_header, tostring(pid)) 148 | end 149 | 150 | restart_timer:start(function(self) 151 | self:stop() 152 | start() 153 | end) 154 | end 155 | 156 | local function process_filter(filter, t) 157 | return filter.match(t) 158 | end 159 | 160 | return { 161 | monitor = process_monitor; 162 | filter = process_filter; 163 | } 164 | -------------------------------------------------------------------------------- /src/filter/lib/spylog/monitor/syslog.lua: -------------------------------------------------------------------------------- 1 | local ut = require "lluv.utils" 2 | local log = require "spylog.log" 3 | local syslog = require "spylog.syslog" 4 | local net = require "spylog.monitor.net" 5 | 6 | local function decode(fmt, ...) 7 | local pri, ver, ts, host, app, procid, msgid, sdata, msg 8 | if fmt == 'rfc3164' then 9 | local mon, day, time 10 | pri, mon, day, time, host, msg = ... 11 | elseif fmt == 'rfc5424' then 12 | pri, ver, ts, host, app, procid, msgid, sdata, msg = ... 13 | end 14 | return pri, msg 15 | end 16 | 17 | local function syslog_monitor(endpoint, opt, cb) 18 | local proto, address, port = ut.split_first(endpoint,"://", true) 19 | assert(proto == 'udp') 20 | 21 | address, port = ut.split_first(address, ":", true) 22 | port = tonumber(port) or 514 23 | 24 | local log_header = string.format('[syslog/%s] [%s:%d]', proto, address, port) 25 | 26 | return net.monitor(string.format('%s://%s:%d', proto, address, port), opt, 27 | function(data) 28 | local pri, msg = decode(syslog.unpack(data)) 29 | if not pri then 30 | return log.warning("%s recv non syslog: %q", log_header, data) 31 | end 32 | 33 | cb(pri, msg) 34 | end, log_header 35 | ) 36 | end 37 | 38 | local function syslog_filter(filter, pri, msg) 39 | return filter.match(msg) 40 | end 41 | 42 | return { 43 | monitor = syslog_monitor; 44 | filter = syslog_filter; 45 | } 46 | 47 | -------------------------------------------------------------------------------- /src/filter/lib/spylog/monitor/trap.lua: -------------------------------------------------------------------------------- 1 | local ut = require "lluv.utils" 2 | local log = require "spylog.log" 3 | local trap = require "spylog.trap" 4 | local net = require "spylog.monitor.net" 5 | 6 | local LVL_TRACE = require "log".LVL.TRACE 7 | 8 | local function append(t, v) t[#t+1] = v return t end 9 | 10 | local active_monitors = {} 11 | 12 | local function trap_monitor(endpoint, opt, cb, log_header) 13 | local proto, address, port = ut.split_first(endpoint, "://", true) 14 | assert(proto == 'udp') 15 | 16 | address, port = ut.split_first(address,":", true) 17 | port = tonumber(port) or 162 18 | 19 | endpoint = string.format('%s://%s:%d', proto, address, port) 20 | 21 | local log_header = log_header or string.format('[trap/%s] [%s:%d]', proto, address, port) 22 | 23 | local active_monitor = active_monitors[endpoint] 24 | 25 | if not active_monitor then 26 | active_monitor = {} 27 | active_monitors[endpoint] = active_monitor 28 | 29 | net.monitor(endpoint, opt, 30 | function(data) 31 | local t = trap.decode(data) 32 | if not t then 33 | return log.warning("%s recv non trap: %q", log_header, trap.bin2hex(data)) 34 | end 35 | 36 | if log.lvl() >= LVL_TRACE then 37 | log.trace('%s %s', log_header, trap.bin2hex(data)) 38 | end 39 | 40 | for i = 1, #active_monitor do 41 | active_monitor[i](t) 42 | end 43 | end, log_header 44 | ) 45 | end 46 | 47 | append(active_monitor, cb) 48 | end 49 | 50 | local function trap_filter(filter, t) 51 | if not (filter.trap[t.specific] or filter.trap[t.enterprise]) then return end 52 | 53 | for i = 1, #t.data do 54 | local msg = t.data[i][2] 55 | if type(msg) == 'string' then 56 | local capture = filter.match(msg) 57 | if capture then return capture end 58 | end 59 | end 60 | end 61 | 62 | return { 63 | monitor = trap_monitor; 64 | filter = trap_filter; 65 | } -------------------------------------------------------------------------------- /src/filter/lib/spylog/syslog.lua: -------------------------------------------------------------------------------- 1 | local month_rfc3164 = { 2 | "Jan", "Feb", "Mar", "Apr", "May", "Jun", 3 | "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", 4 | Jan=true, Feb=true, Mar=true, Apr=true, May=true, Jun=true, 5 | Jul=true, Aug=true, Sep=true, Oct=true, Nov=true, Dec=true 6 | } 7 | 8 | local header_pat_rfc5424 = 9 | "^<(%d+)>%s*" .. -- PRI 10 | "(%d+)%s" .. -- VERSION 11 | "(%S-)%s+" .. -- TIMESTAMP 12 | "(%S-)%s+" .. -- HOSTNAME 13 | "(%S-)%s+" .. -- APP-NAME 14 | "(%S-)%s+" .. -- PROCID 15 | "(%S-)%s+" .. -- MSGID 16 | "" 17 | 18 | local function syslog_msg_rfc3164(msg) 19 | local b, e, pri = msg:find("^<(%d+)>%s*") 20 | if not pri then return end 21 | 22 | msg = msg:sub(e+1) 23 | local b, e, mon, day, time = msg:find("^(...)%s(..)%s(..:..:..)%s+") 24 | if b and month_rfc3164[mon] then 25 | msg = msg:sub(e+1) 26 | else -- invalid date or no date so we MUST set it byself 27 | local now = os.date("*t") 28 | mon = month_rfc3164[now.month] 29 | day = now.day < 10 and (" " .. now.day) or tostring(now.day) 30 | time = string.format("%.2d:%.2d:%.2d", now.hour, now.min, now.sec) 31 | end 32 | 33 | local host, msg = msg:match("^(%S+)%s+(.*)$") 34 | 35 | return "rfc3164", pri, mon, day, time, host, msg 36 | end 37 | 38 | local function syslog_msg(msg) 39 | local _, hend, pri, ver, ts, host, app, procid, msgid = msg:find(header_pat_rfc5424) 40 | 41 | if not pri then return syslog_msg_rfc3164(msg) end 42 | 43 | msg = msg:sub(hend+1) 44 | local sdata 45 | if msg:sub(1, 1) == "-" then 46 | sdata = "-" 47 | msg = msg:sub(3) 48 | else 49 | if msg:sub(1,1) ~= "[" then return end 50 | 51 | local b, e, elem 52 | sdata = {} 53 | while true do 54 | b, e, elem = msg:find("(%b[])", e) 55 | 56 | if not elem then return end 57 | 58 | sdata[#sdata + 1] = elem:sub(2,-2) 59 | e = e + 1 60 | 61 | if msg:sub(e, e) ~= '[' then break end 62 | end 63 | msg = msg:sub(e + 1) 64 | end 65 | 66 | return "rfc5424", pri, ver, ts, host, app, procid, msgid, sdata, msg 67 | end 68 | 69 | return { 70 | unpack = syslog_msg 71 | } -------------------------------------------------------------------------------- /src/filter/lib/spylog/trap.lua: -------------------------------------------------------------------------------- 1 | local ut = require "lluv.utils" 2 | local bit = require "bit32" 3 | 4 | local trap_generic = { 5 | [0] = 'coldStart', 6 | [1] = 'warmStart', 7 | [2] = 'linkDown', 8 | [3] = 'linkUp', 9 | [4] = 'authenticationFailure', 10 | [5] = 'egpNeighborLoss', 11 | [6] = 'enterpriseSpecific' 12 | } 13 | 14 | local function bin2hex(str) 15 | local t = {string.byte(str, 1, #str)} 16 | for i = 1, #t do t[i] = string.format('%.2X', t[i]) end 17 | return table.concat(t) 18 | end 19 | 20 | local function hex2bin(str) 21 | str = str:gsub("(..)", function(ch) 22 | local a = tonumber(ch, 16) 23 | return string.char(a) 24 | end) 25 | return str 26 | end 27 | 28 | -- some libs convert to signed integer 29 | local unsigned_bit = (0xFFFFFFFF == bit.bor(0xFFFFFFFF, 0x0)) and 4 or 2 30 | 31 | local function to_unsigned(v) 32 | local n = #v 33 | 34 | if n == 1 then 35 | return (string.byte(v)) 36 | end 37 | 38 | local value = 0 39 | if n <= unsigned_bit then 40 | for i = 1, n do 41 | value = bit.bor(bit.lshift(value, 8), string.byte(v, i)) 42 | end 43 | else 44 | for i = 1, n do 45 | value = value + (256 ^ (n-i)) * string.byte(v, i) 46 | end 47 | end 48 | return value 49 | end 50 | 51 | local function to_signed(v) 52 | local sign = (bit.band(string.byte(v, 1), 0x80) == 0) 53 | value = to_unsigned(v) 54 | if not sign then 55 | value = value - 256^#v 56 | end 57 | return value 58 | end 59 | 60 | -- assert(0xAB == to_unsigned(hex2bin"AB")) 61 | -- assert(0xABCD == to_unsigned(hex2bin"ABCD")) 62 | -- assert(0xABCDEF == to_unsigned(hex2bin"ABCDEF")) 63 | -- assert(0xFFABCDEF == to_unsigned(hex2bin"FFABCDEF")) 64 | -- assert(1098099060735 == to_unsigned(hex2bin"FFABCDEFFF")) 65 | 66 | -- assert( 0 == to_signed(hex2bin"00")) 67 | -- assert( 127 == to_signed(hex2bin"7F")) 68 | -- assert( 128 == to_signed(hex2bin"0080")) 69 | -- assert( 256 == to_signed(hex2bin"0100")) 70 | -- assert(-128 == to_signed(hex2bin"80")) 71 | -- assert(-129 == to_signed(hex2bin"FF7F")) 72 | 73 | local BinIter = ut.class() do 74 | 75 | function BinIter:__init(s) 76 | assert(type(s) == "string") 77 | 78 | self._s = s 79 | self._i = 1 80 | return self 81 | end 82 | 83 | function BinIter:rest() 84 | if self._i > #self._s then return 0 end 85 | return #self._s - self._i + 1 86 | end 87 | 88 | function BinIter:peek_char(n) 89 | n = n or 1 90 | return self._s:sub(self._i, self._i + n - 1) 91 | end 92 | 93 | function BinIter:read_char(n) 94 | local s = self:peek_char(n) 95 | self._i = self._i + #s 96 | return s 97 | end 98 | 99 | function BinIter:peek_byte() 100 | return string.byte(self:peek_char(), 1) 101 | end 102 | 103 | function BinIter:read_byte() 104 | return string.byte(self:read_char(), 1) 105 | end 106 | 107 | function BinIter:read_str(n) 108 | return self:read_char(n) 109 | end 110 | 111 | function BinIter:read_unsigned(n) 112 | return to_unsigned(self:read_char(n)) 113 | end 114 | 115 | function BinIter:read_signed(n) 116 | return to_signed(self:read_char(n)) 117 | end 118 | 119 | end 120 | 121 | local function oid_node(iter) 122 | local n = 0 123 | repeat 124 | local octet = iter:read_byte() 125 | n = n * 128 + bit.band(0x7F, octet) 126 | until octet < 128 127 | 128 | return n 129 | end 130 | 131 | local decode 132 | local decoders = { 133 | -- Boolean 134 | [0x01] = function( iter, len ) 135 | local val = iter:read_byte() 136 | return val ~= 0xFF 137 | end; 138 | 139 | -- Integer 140 | [0x02] = function( iter, len ) 141 | return iter:read_signed(len) 142 | end; 143 | 144 | -- Octet String 145 | [0x04] = function( iter, len ) 146 | return iter:read_str(len) 147 | end; 148 | 149 | -- Null 150 | [0x05] = function( iter, len ) 151 | return false 152 | end; 153 | 154 | -- Object Identifier 155 | [0x06] = function( iter, len ) 156 | local oid = {} 157 | local str = iter:read_char(len) 158 | iter = BinIter.new(str) 159 | 160 | if iter:rest() > 0 then 161 | local b = iter:read_byte() 162 | oid[2] = math.fmod(b, 40) 163 | b = b - oid[2] 164 | oid[1] = math.floor(b/40 + 0.1) 165 | end 166 | 167 | while iter:rest() > 0 do 168 | local c = oid_node(iter) 169 | oid[#oid + 1] = c 170 | end 171 | 172 | return oid 173 | 174 | end; 175 | 176 | -- Context specific tags 177 | [0x30] = function( iter, len ) 178 | local seq = {} 179 | local hex = iter:read_char(len) 180 | iter = BinIter.new(hex) 181 | while iter:rest() > 0 do 182 | local value = decode(iter) 183 | seq[#seq + 1] = value 184 | end 185 | return seq 186 | end; 187 | } 188 | 189 | local function decode_unsigned(iter, len) 190 | return iter:read_unsigned(len) 191 | end 192 | 193 | decoders[0x40] = decoders[0x04] -- IP Address; 4 byte IPv4 194 | decoders[0x41] = decode_unsigned -- Counter; same as Integer 195 | decoders[0x42] = decoders[0x02] -- Gauge 196 | decoders[0x43] = decode_unsigned -- TimeTicks 197 | decoders[0x44] = decoders[0x04] -- Opaque; same as Octet String 198 | decoders[0x45] = decoders[0x06] -- NsapAddress 199 | decoders[0x46] = decode_unsigned -- Counter64 200 | decoders[0x47] = decode_unsigned -- UInteger32 201 | 202 | -- Context specific tags 203 | decoders[0xA0] = decoders[0x30] -- GetRequest-PDU 204 | decoders[0xA1] = decoders[0x30] -- GetNextRequest-PDU 205 | decoders[0xA2] = decoders[0x30] -- Response-PDU 206 | decoders[0xA3] = decoders[0x30] -- SetRequest-PDU 207 | decoders[0xA4] = decoders[0x30] -- Trap-PDU 208 | decoders[0xA5] = decoders[0x30] -- GetBulkRequest-PDU 209 | decoders[0xA6] = decoders[0x30] -- InformRequest-PDU (not implemented here yet) 210 | decoders[0xA7] = decoders[0x30] -- SNMPv2-Trap-PDU (not implemented here yet) 211 | 212 | local function read_length(iter) 213 | local len = iter:read_byte() 214 | if len > 128 then 215 | local size = len - 128 216 | len = 0 217 | for i = 1, size do 218 | len = len * 256 + iter:read_byte() 219 | end 220 | end 221 | return len 222 | end 223 | 224 | local function read_header(iter) 225 | local typ = iter:read_byte() 226 | local len = read_length(iter) 227 | return typ, len 228 | end 229 | 230 | function decode(iter) 231 | local typ, len = read_header(iter) 232 | local decoder = decoders[typ] 233 | if decoder then 234 | return decoder(iter, len) 235 | end 236 | end 237 | 238 | local function trap_decode(str) 239 | local iter = BinIter.new(str) 240 | local t = decode(iter) 241 | if not t then return nil end 242 | local p = {} 243 | 244 | if type(t[1]) ~= "number" then return end 245 | p.version = t[1] + 1 246 | if p.version <= 0 or p.version > 3 then return end 247 | 248 | if type(t[2]) ~= "string" then return end 249 | p.community = t[2] 250 | 251 | if type(t[3]) ~= "table" then return end 252 | local pdu = t[3] 253 | 254 | if type(pdu[1]) ~= "table" then return end 255 | p.enterprise = table.concat(pdu[1], ".") 256 | 257 | if type(pdu[2]) ~= "string" then return end 258 | p.agent = table.concat({string.byte(pdu[2], 1, #pdu[2])}, '.') 259 | 260 | if type(pdu[3]) ~= "number" then return end 261 | if pdu[3] < 0 or pdu[3] > 6 then return end 262 | p.generic = pdu[3] 263 | 264 | if type(pdu[4]) ~= "number" then return end 265 | p.specific = pdu[4] 266 | 267 | if type(pdu[5]) ~= "number" then return end 268 | p.time = pdu[5] 269 | 270 | if type(pdu[6]) ~= "table" then return end 271 | 272 | for i = 1, #pdu[6] do 273 | local msg = pdu[6][i] 274 | if type(msg) ~= "table" or type(msg[1]) ~= "table" then return end 275 | msg[1] = table.concat(msg[1], ".") 276 | end 277 | 278 | p.data = pdu[6] 279 | 280 | return p 281 | end 282 | 283 | local function trap_print(t) 284 | print("Version:", t.version) 285 | print("Community:", t.community) 286 | print("Enterprise:", t.enterprise) 287 | print("Agent:", t.agent) 288 | print("Generic:", (t.generic and trap_generic[t.generic] or 'Unknown') .. '(' .. tostring(t.generic) .. ')') 289 | print("Specific:", t.specific) 290 | print("Time:", t.time) 291 | print("Data:") 292 | for i = 1, #t.data do 293 | print("", t.data[i][1]) 294 | print("", t.data[i][2]) 295 | print("------------------") 296 | end 297 | end 298 | 299 | return { 300 | decode_hex = function(str) 301 | return trap_decode(hex2bin(str)) 302 | end; 303 | 304 | decode = function(str) 305 | return trap_decode(str) 306 | end; 307 | 308 | bin2hex = bin2hex; 309 | 310 | print = trap_print; 311 | } 312 | -------------------------------------------------------------------------------- /src/filter/main.lua: -------------------------------------------------------------------------------- 1 | local SERVICE = require "LuaService" 2 | local config = require "spylog.config" 3 | config.LOG.prefix = "[filter] " 4 | ------------------------------------------------- 5 | 6 | local log = require "spylog.log" 7 | local version = require "spylog.version" 8 | local uv = require "lluv" 9 | local zthreads = require "lzmq.threads" 10 | local ztimer = require "lzmq.timer" 11 | local cjson = require "cjson.safe" 12 | local stp = require "StackTracePlus" 13 | local regex = require "spylog.filter.regex" 14 | local FilterManager = require "spylog.filter.manager" 15 | local exit = require "spylog.exit" 16 | local CaptureFilter = require "spylog.cfilter" 17 | 18 | log.info('Starting %s version %s. %s', version._NAME, version._VERSION, version._COPYRIGHT) 19 | 20 | local pub, err = zthreads.context():socket("PUB", { 21 | [config.CONNECTIONS.FILTER.JAIL.type] = config.CONNECTIONS.FILTER.JAIL.address 22 | }) 23 | 24 | if not pub then 25 | log.fatal("Can not start filter interface: %s", tostring(err)) 26 | ztimer.sleep(500) 27 | return SERVICE.exit() 28 | end 29 | 30 | log.debug("config.LOG.multithread: %s", tostring(config.LOG.multithread)) 31 | 32 | local function jail(filter, capture) 33 | 34 | if filter.cfilter then 35 | local ok, cfilter_name = filter.cfilter:apply(capture) 36 | if not ok then 37 | log.debug("[%s] excluded by capture filter %s", filter.name, cfilter_name) 38 | return 39 | end 40 | end 41 | 42 | local msg, err = cjson.encode(capture) 43 | 44 | if not msg then 45 | log.alert("Can not encode msg: %s", tostring(err)) 46 | return 47 | end 48 | 49 | log.trace(msg) 50 | 51 | pub:send(msg) 52 | end 53 | 54 | local function init_service() 55 | local filters = FilterManager.new() 56 | 57 | for i = 1, #config.FILTERS do 58 | local filter = config.FILTERS[i] 59 | if filter.enabled then 60 | filter.name = filter.name or filter[1] 61 | filter.match = regex(filter) 62 | 63 | assert(type(filter.name) == 'string', 'invalid filter name') 64 | assert(filter.source, string.format('filter `%s` has no source', filter.name)) 65 | 66 | if filter.cfilter then 67 | local ok, cfilter= pcall(CaptureFilter.new, filter.cfilter) 68 | if not ok then 69 | local err = string.format('can not build capture filter `%s`: %s', filter.name, cfilter) 70 | return error(err) 71 | end 72 | 73 | local names = cfilter:filter_names() 74 | if #names == 0 then filter.cfilter = nil else 75 | filter.cfilter = cfilter 76 | for i = 1, #names do 77 | log.info('[%s] add capture filter `%s`', filter.name, names[i]) 78 | end 79 | end 80 | end 81 | 82 | filters:add(filter) 83 | end 84 | end 85 | 86 | if 0 == filters:start(jail) then 87 | log.warning("there no active filters") 88 | end 89 | end 90 | 91 | local ok, err = pcall(init_service) 92 | 93 | if not ok then 94 | log.fatal(err) 95 | ztimer.sleep(500) 96 | return SERVICE.exit() 97 | end 98 | 99 | log.info("Service start") 100 | 101 | exit.start_monitor(...) 102 | 103 | local ok, err = pcall(uv.run, stp.stacktrace) 104 | 105 | if not ok then 106 | log.alert(err) 107 | end 108 | 109 | log.info("Service stopped") 110 | 111 | ztimer.sleep(500) 112 | 113 | SERVICE.exit() 114 | -------------------------------------------------------------------------------- /src/jail/init.lua: -------------------------------------------------------------------------------- 1 | -- Configuration file for LuaService 2 | 3 | return { 4 | tracelevel = 7, 5 | name = "spylog_jail", 6 | display_name = "SpyLog - Jail", 7 | script = "main.lua", 8 | lua_cpath = '!\\..\\lib\\?.dll', 9 | lua_path = '!\\..\\lib\\?.lua;' .. 10 | '!\\..\\lib\\?\\init.lua;' .. 11 | '!\\lib\\?.lua'; 12 | } 13 | -------------------------------------------------------------------------------- /src/jail/lib/spylog/TimeCounters.lua: -------------------------------------------------------------------------------- 1 | local ztimer = require "lzmq.timer" 2 | local date = require "date" 3 | 4 | ------------------------------------------------------------------------------- 5 | -- Timers implementations 6 | ------------------------------------------------------------------------------- 7 | 8 | ------------------------------------------------------------------------------- 9 | local TimeCounter = {} do 10 | TimeCounter.__index = TimeCounter 11 | 12 | function TimeCounter:new(interval) 13 | local o = setmetatable({}, self) 14 | o._timer = ztimer.monotonic():start(interval * 1000) 15 | o._value = 0 16 | return o 17 | end 18 | 19 | function TimeCounter:inc(v) 20 | if self._timer:rest() == 0 then 21 | self:reset() 22 | end 23 | self._value = self._value + (v or 1) 24 | return self._value 25 | end 26 | 27 | function TimeCounter:get() 28 | if self._timer:rest() == 0 then 29 | self._value = 0 30 | end 31 | return self._value 32 | end 33 | 34 | function TimeCounter:raw() 35 | return self._value 36 | end 37 | 38 | function TimeCounter:reset(now) 39 | self._timer:start() 40 | self._value = 0 41 | end 42 | 43 | end 44 | ------------------------------------------------------------------------------- 45 | 46 | ------------------------------------------------------------------------------- 47 | local ExternalTimeCounter = {} do 48 | ExternalTimeCounter.__index = ExternalTimeCounter 49 | 50 | function ExternalTimeCounter:new(interval) 51 | local o = setmetatable({}, self) 52 | o._start = 0 53 | o._interval = interval 54 | o._value = 0 55 | return o 56 | end 57 | 58 | function ExternalTimeCounter:diff_(now) 59 | local diff 60 | if self._start > now then -- overflow 61 | diff = self._start - now 62 | else 63 | diff = now - self._start 64 | end 65 | return diff 66 | end 67 | 68 | function ExternalTimeCounter:inc(v, now) 69 | local diff = self:diff_(now) 70 | if diff > self._interval then 71 | self:reset(now) 72 | end 73 | self._value = self._value + (v or 1) 74 | return self._value 75 | end 76 | 77 | function ExternalTimeCounter:get(now) 78 | local diff = self:diff_(now) 79 | if diff > self._interval then 80 | self._value = 0 81 | end 82 | return self._value 83 | end 84 | 85 | function ExternalTimeCounter:reset(now) 86 | self._start = now 87 | self._value = 0 88 | end 89 | 90 | function ExternalTimeCounter:raw() 91 | return self._value 92 | end 93 | 94 | end 95 | ------------------------------------------------------------------------------- 96 | 97 | ------------------------------------------------------------------------------- 98 | local RpnBaseCounter = {} do 99 | RpnBaseCounter.__index = RpnBaseCounter 100 | 101 | local div = function(a, b) 102 | return math.floor(a/b) 103 | end 104 | 105 | local timer 106 | 107 | local function get_now(resolution) 108 | return div(timer:elapsed(), (resolution * 1000)) 109 | end 110 | 111 | function RpnBaseCounter:__init(interval, resolution) 112 | self._last_time = 0 113 | self._total = 0 114 | self._values = {} 115 | 116 | resolution = resolution or 1 117 | interval = interval or 60 118 | if interval < 60 then interval = 60 end -- at least one minute 119 | 120 | self._N = div(interval, resolution) 121 | if self._N > 60 then 122 | self._N = 60 123 | resolution = div(interval, self._N) 124 | end -- too many subcounters 125 | 126 | self._2N = 2 * self._N 127 | 128 | if resolution == 1 then 129 | if self._internal then 130 | self._now = function(self) 131 | return div(timer:elapsed(), 1000) 132 | end 133 | else 134 | self._now = function(self, now) 135 | return now 136 | end 137 | end 138 | else 139 | if self._internal then 140 | local sec = resolution * 1000 141 | self._now = function(self) 142 | return div(timer:elapsed(), sec) 143 | end 144 | else 145 | self._now = function(self, now) 146 | return div(now, resolution) 147 | end 148 | end 149 | end 150 | 151 | if self._internal then 152 | timer = timer or ztimer.monotonic():start() 153 | end 154 | 155 | for i = 0, self._N - 1 do self._values[i] = 0 end 156 | 157 | return self 158 | end 159 | 160 | function RpnBaseCounter:_refresh(now) 161 | local N = self._N 162 | 163 | now = self:_now(now) 164 | 165 | local elapsed = now - self._last_time 166 | 167 | if elapsed > self._2N then 168 | for i = 0, N - 1 do self._values[i] = 0 end 169 | self._last_time = now 170 | self._total = 0 171 | elseif elapsed >= self._N then 172 | local last_valid_time = now - N + 1 173 | local last_count_time = self._last_time 174 | 175 | local i = (last_valid_time - 1) % N 176 | local j = (last_count_time - 1) % N 177 | 178 | while i ~= j do 179 | self._total = self._total - self._values[i] 180 | self._values[i] = 0 181 | i = (i - 1 + N) % N 182 | end 183 | 184 | self._last_time = last_valid_time 185 | end 186 | 187 | return now 188 | end 189 | 190 | function RpnBaseCounter:inc(v, now) 191 | now = self:_refresh(now) 192 | 193 | local e = now % self._N 194 | self._values[e] = self._values[e] + (v or 1); 195 | self._total = self._total + (v or 1) 196 | 197 | return self._total 198 | end 199 | 200 | function RpnBaseCounter:get(now) 201 | self:_refresh(now) 202 | return self._total 203 | end 204 | 205 | function RpnBaseCounter:reset(now) 206 | self._last_time = 0 207 | self._total = 0 208 | for i = 0, self._N - 1 do self._values[i] = 0 end 209 | end 210 | 211 | end 212 | ------------------------------------------------------------------------------- 213 | 214 | ------------------------------------------------------------------------------- 215 | local RpnInternalCounter = setmetatable({}, RpnBaseCounter) do 216 | RpnInternalCounter.__index = RpnInternalCounter 217 | 218 | function RpnInternalCounter:new(interval, resolution) 219 | local o = setmetatable({}, RpnInternalCounter) 220 | o._internal = true 221 | RpnBaseCounter.__init(o, interval, resolution) 222 | return o 223 | end 224 | 225 | end 226 | ------------------------------------------------------------------------------- 227 | 228 | ------------------------------------------------------------------------------- 229 | local RpnExternalCounter = setmetatable({}, RpnBaseCounter) do 230 | RpnExternalCounter.__index = RpnExternalCounter 231 | 232 | function RpnExternalCounter:new(interval, resolution) 233 | local o = setmetatable({}, RpnExternalCounter) 234 | o._internal = false 235 | RpnBaseCounter.__init(o, interval, resolution) 236 | return o 237 | end 238 | 239 | end 240 | ------------------------------------------------------------------------------- 241 | 242 | ------------------------------------------------------------------------------- 243 | local TimeCounters = {} do 244 | TimeCounters.__index = TimeCounters 245 | 246 | function TimeCounters:new(Counter, interval, resolution) 247 | local o = setmetatable({}, self) 248 | o._Counter = Counter 249 | o._counters = {} 250 | o._interval = interval 251 | o._resolution = resolution 252 | 253 | return o 254 | end 255 | 256 | function TimeCounters:inc(value, delta, now) 257 | local counter = self._counters[value] 258 | if not counter then 259 | counter = self._Counter:new(self._interval, self._resolution) 260 | self._counters[value] = counter 261 | end 262 | 263 | return counter:inc(delta, now) 264 | end 265 | 266 | function TimeCounters:get(value, now) 267 | local counter = self._counters[value] 268 | if not counter then return 0 end 269 | 270 | return counter:get(now) 271 | end 272 | 273 | function TimeCounters:reset(value, now) 274 | local counter = self._counters[value] 275 | if not counter then return 0 end 276 | 277 | return counter:reset(now) 278 | end 279 | 280 | function TimeCounters:purge(now) 281 | for key, counter in pairs(self._counters) do 282 | if counter:get(now) == 0 then 283 | self._counters[key] = nil 284 | end 285 | end 286 | end 287 | 288 | function TimeCounters:count() 289 | local c = 0 290 | for _ in pairs(self._counters) do 291 | c = c + 1 292 | end 293 | return c 294 | end 295 | 296 | function TimeCounters:raw(value) 297 | local counter = self._counters[value] 298 | if not counter then 299 | counter = self._Counter:new(self._interval, now) 300 | self._counters[value] = counter 301 | end 302 | 303 | return counter:raw() 304 | end 305 | 306 | end 307 | ------------------------------------------------------------------------------- 308 | 309 | ------------------------------------------------------------------------------- 310 | -- Jail API 311 | ------------------------------------------------------------------------------- 312 | 313 | ------------------------------------------------------------------------------- 314 | local JailCounter = {} do 315 | JailCounter.__index = JailCounter 316 | 317 | local date_to_ts do 318 | 319 | local begin_time = date(2000, 1, 1) 320 | 321 | date_to_ts = function (d) 322 | return math.floor(date.diff(d, begin_time):spanseconds()) 323 | end 324 | 325 | end 326 | 327 | function JailCounter:new(jail) 328 | local o = setmetatable({}, self) 329 | 330 | o._external = jail.counter and jail.counter.time == 'filter' 331 | o._accumulate = jail.counter and jail.counter.type == 'accumulate' 332 | o._fixed = jail.counter and jail.counter.type == 'fixed' 333 | o._value = jail.counter and jail.counter.value or 'value' 334 | o._banwhat = jail.counter and jail.counter.capture or 'host' 335 | 336 | if not o._fixed then 337 | local resolution = jail.counter and jail.counter.resolution 338 | local counter 339 | if resolution then 340 | counter = o._external and RpnExternalCounter or RpnInternalCounter 341 | else 342 | counter = o._external and ExternalTimeCounter or TimeCounter 343 | end 344 | 345 | o._counter = TimeCounters:new(counter, jail.findtime, resolution) 346 | end 347 | 348 | return o 349 | end 350 | 351 | function JailCounter:inc(filter) 352 | if self._fixed then 353 | return tonumber(filter[self._value]) or 1 354 | end 355 | 356 | local now 357 | if self._external then 358 | now = date_to_ts(filter.date) 359 | end 360 | 361 | local inc 362 | if self._accumulate then 363 | inc = tonumber(filter[self._value]) 364 | end 365 | 366 | return self._counter:inc(filter[self._banwhat], inc, now) 367 | end 368 | 369 | function JailCounter:reset(filter) 370 | if self._fixed then return end 371 | 372 | local now 373 | if self._external then 374 | now = date_to_ts(filter.date) 375 | end 376 | 377 | self._counter:reset(filter[self._banwhat], now) 378 | end 379 | 380 | function JailCounter:purge(now) 381 | if self._fixed then return end 382 | 383 | local now 384 | if self._external then 385 | now = date_to_ts(now) 386 | end 387 | 388 | self._counter:purge(now) 389 | end 390 | 391 | function JailCounter:count() 392 | if self._fixed then return 0 end 393 | 394 | return self._counter:count() 395 | end 396 | 397 | end 398 | ------------------------------------------------------------------------------- 399 | 400 | return { 401 | jail = JailCounter; 402 | } -------------------------------------------------------------------------------- /src/jail/main.lua: -------------------------------------------------------------------------------- 1 | local SERVICE = require "LuaService" 2 | local config = require "spylog.config" 3 | config.LOG.prefix = "[jail] " 4 | ------------------------------------------------- 5 | 6 | local log = require "spylog.log" 7 | local version = require "spylog.version" 8 | local uv = require "lluv" 9 | uv.poll_zmq = require "lluv.poll_zmq" 10 | local zthreads = require "lzmq.threads" 11 | local ztimer = require "lzmq.timer" 12 | local cjson = require "cjson.safe" 13 | local Counter = require "spylog.TimeCounters" 14 | local date = require "date" 15 | local stp = require "StackTracePlus" 16 | local exit = require "spylog.exit" 17 | local var = require "spylog.var" 18 | local path = require "path" 19 | local CaptureFilter = require "spylog.cfilter" 20 | 21 | local function rndint(v) 22 | return math.random(0, v) 23 | end 24 | 25 | log.info('Starting %s version %s. %s', version._NAME, version._VERSION, version._COPYRIGHT) 26 | 27 | local DEFAULT = config.JAIL and config.JAIL.default or {} 28 | 29 | local sub, err = zthreads.context():socket("SUB",{ 30 | [config.CONNECTIONS.JAIL.FILTER.type] = config.CONNECTIONS.JAIL.FILTER.address; 31 | subscribe = ""; 32 | }) 33 | 34 | if not sub then 35 | log.fatal("Can not start filter interface: %s", tostring(err)) 36 | ztimer.sleep(500) 37 | return SERVICE.exit() 38 | end 39 | 40 | local pub, err = zthreads.context():socket("PUB",{ 41 | [config.CONNECTIONS.JAIL.ACTION.type] = config.CONNECTIONS.JAIL.ACTION.address; 42 | }) 43 | 44 | if not pub then 45 | log.fatal("Can not start action interface: %s", tostring(err)) 46 | ztimer.sleep(500) 47 | return SERVICE.exit() 48 | end 49 | 50 | local function apply_vars(jail, dst, src, context) 51 | for i, v in pairs(src) do 52 | if type(v) == 'table' then 53 | dst[i] = apply_vars(jail, {}, v, context) 54 | elseif type(v) == 'string' then 55 | local unknown 56 | dst[i], unknown = var.format(v, context) 57 | if unknown then 58 | return log.alert("[%s] unknown parameter: %s", jail.name, next(unknown)) 59 | end 60 | else dst[i] = v end 61 | end 62 | return dst 63 | end 64 | 65 | local function build_action(jail, action, context) 66 | if type(action) == 'string' then 67 | local unknown 68 | action, unknown = var.format(action, context) 69 | if unknown then 70 | return log.alert("[%s] unknown parameter: %s", jail.name, next(unknown)) 71 | end 72 | return {action} 73 | end 74 | 75 | local name, unknown, parameters = var.format(action[1], context) 76 | if unknown then 77 | return log.alert("[%s] unknown parameter: %s", jail.name, next(unknown)) 78 | end 79 | 80 | if action[2] then 81 | context.action = name 82 | parameters = apply_vars(jail, {}, action[2], context) 83 | end 84 | 85 | return {name, parameters} 86 | end 87 | 88 | local function action(jail, filter, value) 89 | -- extend filter capture 90 | filter.jail = jail.name 91 | filter.bantime = jail.bantime 92 | filter.counter = value 93 | if jail.rndtime then 94 | filter.bantime = filter.bantime + rndint(jail.rndtime) 95 | end 96 | 97 | -- build jail parameters 98 | local parameters 99 | if jail.parameters then 100 | local context = DEFAULT.parameters and var.combine{filter, DEFAULT.parameters} or filter 101 | parameters = apply_vars(jail, {}, jail.parameters, context) 102 | end 103 | 104 | local context 105 | if parameters then 106 | context = var.combine{filter, parameters, DEFAULT.parameters} 107 | elseif DEFAULT.parameters then 108 | context = var.combine{filter, DEFAULT.parameters} 109 | else 110 | context = filter 111 | end 112 | 113 | local actions = {} 114 | if type(jail.action) == 'string' then 115 | local action = build_action(jail, jail.action, context) 116 | if not action then return end 117 | actions[1] = action 118 | else 119 | for _, action in ipairs(jail.action) do 120 | action = build_action(jail, action, context) 121 | if not action then return end 122 | actions[#actions + 1] = action 123 | end 124 | end 125 | 126 | local msg = cjson.encode{ 127 | filter = filter.filter; 128 | jail = filter.jail; 129 | bantime = filter.bantime; 130 | host = filter.host; 131 | date = filter.date; 132 | action = actions; 133 | } 134 | 135 | log.trace("action %s", msg) 136 | 137 | pub:send(msg) 138 | end 139 | 140 | log.debug("config.LOG.multithread: %s", tostring(config.LOG.multithread)) 141 | 142 | log.notice("Connected to filters") 143 | 144 | local JAIL do 145 | 146 | local function append_jail(jails, filter_name, jail) 147 | local t = jails[filter_name] 148 | 149 | if not t then 150 | t = {} 151 | jails[filter_name] = t 152 | end 153 | t[#t + 1] = jail 154 | 155 | return jails 156 | end 157 | 158 | local function j(t) 159 | local jails = {} 160 | for i = 1, #t do if t[i].enabled or (t[i].enabled == nil) then 161 | local jail = t[i] 162 | jails[#jails + 1] = jail 163 | 164 | jail.name = jail.name or jail[1] 165 | if type(jail.filter) == 'table' then 166 | for j = 1, #jail.filter do 167 | append_jail(jails, jail.filter[j], jail) 168 | end 169 | else 170 | append_jail(jails, jail.filter, jail) 171 | end 172 | 173 | -- apply default values 174 | for name, value in pairs(DEFAULT) do 175 | if name ~= 'parameters' and jail[name] == nil then 176 | jail[name] = value 177 | end 178 | end 179 | 180 | if jail.cfilter then 181 | local ok, cfilter= pcall(CaptureFilter.new, jail.cfilter) 182 | if not ok then 183 | local err = string.format('can not build capture filter `%s`: %s', jail.name, cfilter) 184 | return nil, err 185 | end 186 | 187 | local names = cfilter:filter_names() 188 | if #names == 0 then jail.cfilter = nil else 189 | jail.cfilter = cfilter 190 | for i = 1, #names do 191 | log.info('[%s] add capture filter `%s`', jail.name, names[i]) 192 | end 193 | end 194 | end 195 | end end 196 | 197 | return jails 198 | end 199 | 200 | JAIL, err = j(config.JAILS) 201 | 202 | if not JAIL then 203 | log.fatal("Can not load jails: %s", tostring(err)) 204 | ztimer.sleep(500) 205 | return SERVICE.exit() 206 | end 207 | 208 | if not next(JAIL) then 209 | log.warning('there no any active jails') 210 | end 211 | 212 | end 213 | 214 | local jail_counters = {} 215 | 216 | local function create_counter(jail) 217 | local counter 218 | 219 | 220 | counter = Counter.jail:new( jail ) 221 | return counter 222 | end 223 | 224 | local function check_jail(jail, capture) 225 | local banwhat = jail.counter and jail.counter.capture or 'host' 226 | if not capture[banwhat] then 227 | log.error('filter `%s` does not provide `%s` capture', capture.filter, banwhat) 228 | return 229 | end 230 | 231 | if jail.cfilter then 232 | local ok, cfilter_name = jail.cfilter:apply(capture) 233 | if not ok then 234 | log.debug("[%s] message from %s excluded by capture filter %s", jail.name, capture.filter, cfilter_name) 235 | return 236 | end 237 | end 238 | 239 | local counter = jail_counters[jail.name] 240 | if not counter then 241 | counter = create_counter(jail) 242 | jail_counters[jail.name] = counter 243 | end 244 | 245 | local value = counter:inc(capture) 246 | 247 | if value then 248 | if value >= jail.maxretry then 249 | counter:reset(capture) 250 | log.warning("[%s] %s - %d", jail.name, capture[banwhat], value) 251 | action(jail, capture, value) --! @note `action` may add some fields to `capture` 252 | else 253 | log.trace("[%s] %s - %d", jail.name, capture[banwhat], value) 254 | end 255 | end 256 | end 257 | 258 | uv.poll_zmq(sub):start(function(handle, err, pipe) 259 | if err then 260 | log.fatal("poll: ", err) 261 | return uv.stop() 262 | end 263 | 264 | local msg, err = sub:recvx() 265 | if not msg then 266 | if err:name() ~= 'EAGAIN' then 267 | log.fatal("recv msg: ", err) 268 | uv.stop() 269 | end 270 | return 271 | end 272 | 273 | log.trace("%s", msg) 274 | 275 | local capture = cjson.decode(msg) 276 | if not (capture and capture.filter and capture.date) then 277 | log.error("invalid msg: ", msg:sub(128)) 278 | return 279 | end 280 | 281 | local jails = JAIL[capture.filter] 282 | if not jails then 283 | log.warning("unknown jail for filter `%s`", capture.filter) 284 | else for i = 1, #jails do 285 | check_jail(jails[i], capture) 286 | end end 287 | end) 288 | 289 | if config.JAIL and config.JAIL.purge_interval then 290 | local LVL_TRACE = require "log".LVL.TRACE 291 | local purge_counter = 0 292 | local purge_interval = config.JAIL.purge_interval 293 | log.info("Start purge timer %d [min]", purge_interval) 294 | uv.timer():start(60000, 60000, function() 295 | purge_counter = purge_counter + 1 296 | if purge_counter >= purge_interval then 297 | purge_counter = 0 298 | local now = date() 299 | for name, jail_counter in pairs(jail_counters) do 300 | local c 301 | if log.lvl() >= LVL_TRACE then 302 | c = jail_counter:count() 303 | end 304 | jail_counter:purge(now) 305 | if c then 306 | log.trace("Purge jail %s %d => %d", name, c, jail_counter:count()) 307 | end 308 | end 309 | end 310 | end) 311 | end 312 | 313 | exit.start_monitor(...) 314 | 315 | for filter, jails in pairs(JAIL) do 316 | if type(filter) ~= 'number' then 317 | for i = 1, #jails do 318 | log.info("Attach filter `%s` to jail `%s`", filter, jails[i].name) 319 | end 320 | end 321 | end 322 | 323 | local ok, err = pcall(uv.run, stp.stacktrace) 324 | 325 | if not ok then 326 | log.alert(err) 327 | end 328 | 329 | log.info("Service stopped") 330 | 331 | ztimer.sleep(500) 332 | 333 | SERVICE.exit() 334 | -------------------------------------------------------------------------------- /src/lib/spylog/args.lua: -------------------------------------------------------------------------------- 1 | local lpeg = require 'lpeg' 2 | 3 | local function MakeArgsGramma(sep, quot) 4 | assert(#sep == 1 and #quot == 1) 5 | local P, C, Cs, Ct, Cp = lpeg.P, lpeg.C, lpeg.Cs, lpeg.Ct, lpeg.Cp 6 | local nl = P('\n') 7 | local any = P(1) 8 | local eos = P(-1) 9 | local nonescaped = C((any - (nl + P(quot) + P(sep) + P('=') + eos))^0) 10 | local escaped = P(quot) * Cs((((any - P(quot)) + (P(quot) * P(quot)) / quot))^0) * P(quot) 11 | local escaped2 = Ct(nonescaped * '=' * (escaped+nonescaped)) 12 | local field = escaped + escaped2 + nonescaped 13 | local record = Ct(field * ( P(sep) * field )^0) * (nl + eos) * Cp() 14 | return record 15 | end 16 | 17 | local function Split(str, pat) 18 | local t, pos = pat:match(str) 19 | if pos ~= nil then 20 | str = str:sub(pos) 21 | end 22 | 23 | local r = {} 24 | if t then 25 | for i = 1, #t do 26 | if type(t[i]) == 'table' then 27 | r[ #r+1 ] = t[i][1] .. '=' .. t[i][2] .. '' 28 | elseif t[i] ~= '' then 29 | r[#r+1] = t[i] 30 | end 31 | end 32 | end 33 | 34 | if str == '' then 35 | str = nil 36 | end 37 | 38 | return r, str 39 | end 40 | 41 | local pattern = MakeArgsGramma(' ', '"') 42 | 43 | local function SplitArgs(cmd) 44 | local args, tail = Split(cmd, pattern) 45 | if not args then 46 | return nil, "can not parse command: " .. cmd 47 | end 48 | 49 | return args, tail 50 | end 51 | 52 | local function SplitCommand(cmd) 53 | local args, tail = SplitArgs(cmd, pattern) 54 | if (not args) or (#args == 0) then 55 | return nil, "can not parse command: " .. cmd 56 | end 57 | return table.remove(args, 1), args, tail 58 | end 59 | 60 | local function EscapeTag(str) 61 | str = tostring(str) 62 | return (string.gsub(str,'"', '""')) 63 | end 64 | 65 | local function ApplyTags(str, tags, escape) 66 | return (string.gsub(str, '%b<>', function(tag) 67 | tag = string.sub(tag, 2, -2) 68 | tag = tags[tag] or tags[string.lower(tag)] or '' 69 | return escape and EscapeTag(tag) or tag 70 | end)) 71 | end 72 | 73 | local function BuildArgs(args) 74 | local s 75 | for _, a in ipairs(args) do 76 | if s then s = s .. ' ' else s = '' end 77 | a = (a):gsub('"', '""') 78 | if a:find("%s") then 79 | -- escape `a="b c d"` 80 | local v, n = string.gsub(a, '^([^=%s]+)=(.+)$', '%1="%2"') 81 | if n == 1 then a = v 82 | else a = '"' .. a .. '"' end 83 | end 84 | s = s .. a 85 | end 86 | return s 87 | end 88 | 89 | local function DecodeArgs(arguments, tags) 90 | local args, tail, err 91 | 92 | if type(arguments) == 'string' then 93 | args, tail = SplitArgs(arguments) 94 | if not args then return nil, tail end 95 | else 96 | args = arguments 97 | end 98 | 99 | for i = 1, #args do 100 | args[i] = ApplyTags(args[i], tags, false) 101 | end 102 | 103 | return args, tail 104 | end 105 | 106 | local function DecodeCommand(command, tags) 107 | local cmd, args, tail, err 108 | 109 | if type(command) == 'string' then 110 | cmd, args, tail = SplitCommand(command) 111 | if not cmd then err = args end 112 | elseif command[2] then 113 | cmd = command[1] 114 | if not cmd then 115 | err = 'first element have to be a commad' 116 | else 117 | if type(command[2]) == 'string' then 118 | args, tail = SplitArgs(command[2]) 119 | if not args then err, cmd = tail end 120 | else 121 | args = command[2] 122 | end 123 | end 124 | elseif command[1] then 125 | cmd, args, tail = SplitCommand(command[1]) 126 | if not cmd then err = args end 127 | end 128 | 129 | if not cmd then return nil, err end 130 | 131 | for i = 1, #args do 132 | args[i] = ApplyTags(args[i], tags, false) 133 | end 134 | 135 | return cmd, args, tail 136 | end 137 | 138 | return { 139 | build = BuildArgs; 140 | split = SplitArgs; 141 | split_command = SplitCommand; 142 | escape_tag = EscapeTag; 143 | apply_tags = ApplyTags; 144 | decode = DecodeArgs; 145 | decode_command = DecodeCommand; 146 | } 147 | -------------------------------------------------------------------------------- /src/lib/spylog/cfilter.lua: -------------------------------------------------------------------------------- 1 | local ut = require "lluv.utils" 2 | 3 | local function append(t, v) 4 | t[#t + 1] = v 5 | return t 6 | end 7 | 8 | local CaptureFilter = ut.class() do 9 | 10 | function CaptureFilter:__init(t) 11 | self._filters = {} 12 | 13 | if type(t[1]) ~= 'table' then t = {t} end 14 | 15 | for i = 1, #t do 16 | local filter = t[i] 17 | 18 | local name = assert(filter[1], string.format('no name for pre filter #%d', i)) 19 | 20 | name = (string.sub(name, 1, 1) == '@') and string.sub(name, 2) or ("spylog.cfilter." .. name) 21 | local Filter = require (name) 22 | 23 | append(self._filters, assert(Filter.new(filter))) 24 | end 25 | 26 | return self 27 | end 28 | 29 | function CaptureFilter:apply(capture) 30 | for i = 1, #self._filters do 31 | local filter = self._filters[i] 32 | if not filter:apply(capture) then 33 | return false, filter:name() 34 | end 35 | end 36 | return true 37 | end 38 | 39 | function CaptureFilter:filter_names() 40 | local ret = {} for i = 1, #self._filters do 41 | append(ret, self._filters[i]:name()) 42 | end 43 | return ret 44 | end 45 | 46 | end 47 | 48 | return CaptureFilter -------------------------------------------------------------------------------- /src/lib/spylog/cfilter/acl.lua: -------------------------------------------------------------------------------- 1 | local log = require "spylog.log" 2 | local ut = require "lluv.utils" 3 | local BaseFilter = require "spylog.cfilter.base" 4 | local iputil = require "spylog.iputil" 5 | 6 | local AclFilter = ut.class(BaseFilter) do 7 | 8 | local Class = AclFilter 9 | 10 | function AclFilter:__init(filter) 11 | if type(filter.filter) == 'string' then 12 | filter.filter = {filter.filter} 13 | end 14 | local cidr = filter.filter 15 | 16 | assert(type(cidr) == 'table', 'capture filter with type `acl` has no cidr list') 17 | 18 | filter.capture = filter.capture or 'host' 19 | Class.__base.__init(self, filter) 20 | 21 | self._cidr = iputil.load_cidrs(cidr) 22 | 23 | return self 24 | end 25 | 26 | function AclFilter:apply(capture) 27 | local value = self:value(capture) 28 | 29 | if value and iputil.find_cidr(value, self._cidr) then 30 | return self._allow 31 | end 32 | 33 | return not self._allow 34 | end 35 | 36 | end 37 | 38 | return AclFilter -------------------------------------------------------------------------------- /src/lib/spylog/cfilter/base.lua: -------------------------------------------------------------------------------- 1 | local log = require "spylog.log" 2 | local ut = require "lluv.utils" 3 | 4 | local BaseFilter = ut.class() do 5 | 6 | function BaseFilter:__init(filter) 7 | self._name = filter[1] 8 | self._vname = filter.capture 9 | self._allow = filter.type == 'allow' 10 | return self 11 | end 12 | 13 | function BaseFilter:value(capture) 14 | local value = capture[self._vname] 15 | if not value then 16 | log.warning('filter %s has no capture %s', capture.filter, vname) 17 | end 18 | return value 19 | end 20 | 21 | function BaseFilter:name() 22 | return self._name 23 | end 24 | 25 | end 26 | 27 | return BaseFilter -------------------------------------------------------------------------------- /src/lib/spylog/cfilter/geoip.lua: -------------------------------------------------------------------------------- 1 | local log = require "spylog.log" 2 | local ut = require "lluv.utils" 3 | local config = require "spylog.config" 4 | local BaseFilter = require "spylog.cfilter.base" 5 | local path = require "path" 6 | local mmdb = require "mmdb" 7 | local lru do local ok 8 | ok, lru = pcall(require, "lru") 9 | if not ok then lru = nil end 10 | end 11 | 12 | local geodb = {} 13 | local geodb_cache = {} 14 | 15 | local GeoIPFilter = ut.class(BaseFilter) do 16 | 17 | local Class = GeoIPFilter 18 | 19 | local dummy = { country = { iso_code = "--" }, continent = { code = "--" } } 20 | 21 | local function find(self, host) 22 | local ok, ret 23 | 24 | if string.find(host, ':', nil, true) then 25 | ok, ret = pcall(self._mmdb.search_ipv6, self._mmdb, host) 26 | else 27 | ok, ret = pcall(self._mmdb.search_ipv4, self._mmdb, host) 28 | end 29 | 30 | if not ok then return nil, ret end 31 | 32 | return ret or dummy 33 | end 34 | 35 | local function find_with_cache(self, host) 36 | local ret, err = self._cache:get(host) 37 | if ret then return ret end 38 | ret, err = find(self, host) 39 | self._cache:set(host, ret) 40 | 41 | return ret 42 | end 43 | 44 | function GeoIPFilter:__init(filter) 45 | log.warning("geoip capture filter is still in experemental stage") 46 | 47 | if type(filter.filter) == 'string' then 48 | filter.filter = {filter.filter} 49 | end 50 | local list = filter.filter 51 | local dbname = filter.mmdb or "GeoLite2-Country.mmdb"; 52 | 53 | assert(type(list) == 'table', 'capture filter with type `geoip` has no country list') 54 | 55 | local db_full_path = dbname 56 | if not path.isfullpath(db_full_path) then 57 | db_full_path = path.join(config.CONFIG_DIR, 'data', dbname) 58 | end 59 | db_full_path = path.normalize(db_full_path) 60 | 61 | local db 62 | local ok, err = pcall(function() 63 | db = geodb[db_full_path] or mmdb.open(db_full_path) 64 | geodb[db_full_path] = db 65 | end) 66 | 67 | if not ok then 68 | error("can not open database file: " .. db_full_path .. "; " .. err) 69 | end 70 | 71 | local country = {} for i = 1, #list do 72 | local value = string.upper(list[i]) 73 | country[ value ] = true 74 | end 75 | 76 | local continent = {} 77 | if list.continent then 78 | if type(list.continent) == 'string' then 79 | list.continent = {list.continent} 80 | end 81 | 82 | for i = 1, #list.continent do 83 | local value = string.upper(list.continent[i]) 84 | continent[ value ] = true 85 | end 86 | end 87 | 88 | self._hash = {continent = continent, country = country} 89 | self._mmdb = db 90 | 91 | if filter.cache then 92 | if not lru then 93 | log.warning('can not use cache for geoip module. Please install `lua-lru` module.') 94 | else 95 | assert(type(filter.cache) == 'number', 'cache elment for geoip filter should be a number') 96 | local t = geodb_cache[db_full_path] or {} 97 | geodb_cache[db_full_path] = t 98 | local cache = t[filter.cache] or lru.new(filter.cache) 99 | t[filter.cache] = cache 100 | self._cache = cache 101 | end 102 | end 103 | 104 | self._find = self._cache and find_with_cache or find 105 | 106 | filter.capture = filter.capture or 'host' 107 | Class.__base.__init(self, filter) 108 | 109 | return self 110 | end 111 | 112 | function GeoIPFilter:apply(capture) 113 | local value = self:value(capture) 114 | 115 | if value then 116 | local info, err = self:_find(value) 117 | if err then 118 | log.warning("error while search IP: `%s` - %s", value, err) 119 | -- deny in any case 120 | return false 121 | end 122 | local country = info and info.country and info.country.iso_code 123 | if country and self._hash.country[country] then 124 | return self._allow 125 | end 126 | local continent = info and info.continent and info.continent.code 127 | if continent and self._hash.continent[continent] then 128 | return self._allow 129 | end 130 | end 131 | 132 | return not self._allow 133 | end 134 | 135 | end 136 | 137 | return GeoIPFilter -------------------------------------------------------------------------------- /src/lib/spylog/cfilter/list.lua: -------------------------------------------------------------------------------- 1 | local ut = require "lluv.utils" 2 | local BaseFilter = require "spylog.cfilter.base" 3 | 4 | local ListFilter = ut.class(BaseFilter) do 5 | 6 | local Class = ListFilter 7 | 8 | function ListFilter:__init(filter) 9 | if type(filter.filter) == 'string' then 10 | filter.filter = {filter.filter} 11 | end 12 | local list = filter.filter 13 | 14 | assert(type(list) == 'table', 'capture filter with type `list` has no list of values') 15 | assert(filter.capture, 'capture filter with type `list` has no capture name') 16 | 17 | Class.__base.__init(self, filter) 18 | 19 | self._nocase = not not filter.nocase 20 | 21 | 22 | local hash = {} for i = 1, #list do 23 | local value = self._nocase and string.upper(list[i]) or list[i] 24 | hash[ value ] = true 25 | end 26 | 27 | self._hash = hash 28 | 29 | return self 30 | end 31 | 32 | function ListFilter:apply(capture) 33 | local value = self:value(capture) 34 | 35 | if value then 36 | if self._nocase then 37 | value = string.upper(value) 38 | end 39 | if self._hash[value] then 40 | return self._allow 41 | end 42 | end 43 | 44 | return not self._allow 45 | end 46 | 47 | end 48 | 49 | return ListFilter -------------------------------------------------------------------------------- /src/lib/spylog/cfilter/prefix.lua: -------------------------------------------------------------------------------- 1 | local log = require "spylog.log" 2 | local ut = require "lluv.utils" 3 | local BaseFilter = require "spylog.cfilter.base" 4 | local config = require "spylog.config" 5 | local ptree = require "prefix_tree" 6 | local path = require "path" 7 | 8 | local PrefixFilter = ut.class(BaseFilter) do 9 | 10 | local Class = PrefixFilter 11 | 12 | function PrefixFilter:__init(filter) 13 | local prefixes = assert(filter.filter, 'capture filter with type `prefix` has no prefix list') 14 | 15 | filter.capture = filter.capture or 'number' 16 | Class.__base.__init(self, filter) 17 | 18 | local tree 19 | 20 | -- load prefixes 21 | if type(prefixes) == 'table' then 22 | tree = ptree.new() 23 | if prefixes[1] then -- load from array 24 | for _, prefix in ipairs(prefixes) do 25 | tree:add(prefix, '') 26 | end 27 | else -- load from map 28 | for prefix, value in pairs(prefixes) do 29 | tree:add(prefix, value) 30 | end 31 | end 32 | else -- load from file 33 | local base_prefix_dir = path.join(config.CONFIG_DIR, 'data') 34 | local full_path = path.fullpath(path.isfullpath(prefixes) or path.join(base_prefix_dir, prefixes)) 35 | log.debug('full path for prefix: %s', full_path) 36 | if not path.isfile(full_path) then 37 | return nil, string.format('can not find prefix file %s', full_path) 38 | end 39 | tree = ptree.LoadPrefixFromFile(full_path) 40 | end 41 | 42 | self._tree = tree 43 | 44 | return self 45 | end 46 | 47 | function PrefixFilter:apply(capture) 48 | local value = self:value(capture) 49 | 50 | if value and self._tree:find(value) then 51 | return self._allow 52 | end 53 | 54 | return not self._allow 55 | end 56 | 57 | end 58 | 59 | return PrefixFilter -------------------------------------------------------------------------------- /src/lib/spylog/cfilter/regex.lua: -------------------------------------------------------------------------------- 1 | local ut = require "lluv.utils" 2 | local BaseFilter = require "spylog.cfilter.base" 3 | 4 | local ENGINES = { 5 | default = function(list) 6 | local regex = {} for i = 1, #list do 7 | assert(type(list[i]) == 'string') 8 | regex[i] = list[i] 9 | end 10 | 11 | return function(s) 12 | for i = 1, #regex do 13 | if string.find(s, regex[i]) then 14 | return true 15 | end 16 | end 17 | end 18 | end; 19 | 20 | pcre = function(list) 21 | local rex = require "rex_pcre" 22 | 23 | local regex = {} for i = 1, #list do 24 | regex[i] = assert(rex.new(list[i])) 25 | end 26 | 27 | return function(s) 28 | for i = 1, #regex do 29 | if regex[i]:find(s) then 30 | return true 31 | end 32 | end 33 | end 34 | end; 35 | } 36 | 37 | local RegexFilter = ut.class(BaseFilter) do 38 | 39 | local Class = RegexFilter 40 | 41 | function RegexFilter:__init(filter) 42 | if type(filter.filter) == 'string' then 43 | filter.filter = {filter.filter} 44 | end 45 | local regexes = filter.filter 46 | 47 | assert(type(regexes) == 'table', 'capture filter with type `regex` has no regex list') 48 | assert(filter.capture, 'capture filter with type `regex` has no capture name') 49 | 50 | Class.__base.__init(self, filter) 51 | 52 | local engine = filter.engine or 'default' 53 | 54 | engine = assert(ENGINES[engine], string.format('capture filter with type `regex` has unknown engine %s', tostring(engine))) 55 | 56 | self._find = engine(regexes) 57 | 58 | return self 59 | end 60 | 61 | function RegexFilter:apply(capture) 62 | local value = self:value(capture) 63 | 64 | if value and self._find(value) then 65 | return self._allow 66 | end 67 | 68 | return not self._allow 69 | end 70 | 71 | end 72 | 73 | return RegexFilter -------------------------------------------------------------------------------- /src/lib/spylog/config.lua: -------------------------------------------------------------------------------- 1 | local Service = require "LuaService" 2 | local path = require "path" 3 | 4 | local CONFIG_DIR = string.match(Service.PATH, "^(.-)[\\/][^\\/]+$") 5 | CONFIG_DIR = path.join(CONFIG_DIR, 'config') 6 | 7 | local WHITE_IP = {} 8 | 9 | local SOURCES = {} 10 | 11 | local FILTERS = {} 12 | 13 | local JAILS = {} 14 | 15 | local ACTIONS = {} 16 | 17 | local CONNECTIONS = {} 18 | 19 | local LOG = {} 20 | 21 | local FILTER = {} 22 | 23 | local JAIL = {} 24 | 25 | local ACTION = {} 26 | 27 | local function load_config(file, env) 28 | local fn 29 | if setfenv then 30 | fn = assert(loadfile(file)) 31 | setfenv(fn, env) 32 | else 33 | fn = assert(loadfile(file, "bt", env)) 34 | end 35 | return fn() 36 | end 37 | 38 | local function load_configs(base) 39 | local function append(t, v) 40 | t[#t + 1] = v 41 | end 42 | 43 | local function appender(t) 44 | return function(v) return append(t, v) end 45 | end 46 | 47 | local main_config = path.join(base, "spylog.lua") 48 | 49 | load_config(main_config, { 50 | LOG = function(t) LOG = t end; 51 | WHITE_IP = function(t) WHITE_IP = t end; 52 | FILTER = function(t) FILTER = t end; 53 | JAIL = function(t) JAIL = t end; 54 | ACTION = function(t) ACTION = t end; 55 | CONNECT = function(t) 56 | for k, v in pairs(t) do CONNECTIONS[k] = v end 57 | end; 58 | }) 59 | 60 | local env = { 61 | FILTER = appender(FILTERS); 62 | JAIL = appender(JAILS); 63 | ACTION = function(t) 64 | local name = t[1] 65 | t[1], t[2] = t[2] 66 | ACTIONS[name] = t 67 | end; 68 | SOURCE = function(t) 69 | local name = t[1] 70 | t[1], t[2] = t[2] 71 | SOURCES[name] = t 72 | end; 73 | WHITE_IP = WHITE_IP; 74 | } 75 | 76 | for _,cfg in ipairs{'sources', 'filters', 'jails', 'actions'} do 77 | path.each(path.join(base, cfg, "*.lua"), "f", function(fname) 78 | load_config(fname, env) 79 | end, {recurse=true}) 80 | end 81 | 82 | end 83 | 84 | load_configs(CONFIG_DIR) 85 | 86 | return { 87 | CONFIG_DIR = CONFIG_DIR; 88 | FILTERS = FILTERS; 89 | JAILS = JAILS; 90 | ACTIONS = ACTIONS; 91 | FILTER = FILTER; 92 | JAIL = JAIL; 93 | ACTION = ACTION; 94 | CONNECTIONS = CONNECTIONS; 95 | SOURCES = SOURCES; 96 | LOG = LOG; 97 | } 98 | -------------------------------------------------------------------------------- /src/lib/spylog/exit.lua: -------------------------------------------------------------------------------- 1 | local SERVICE = require "LuaService" 2 | local uv = require "lluv" 3 | uv.poll_zmq = require "lluv.poll_zmq" 4 | local log = require "spylog.log" 5 | 6 | local function stop_service() 7 | log.info("Stopping service...") 8 | uv.stop() 9 | end 10 | 11 | local function register_stop_monitor(...) 12 | local pipe = ... 13 | if not pipe or not pipe.recvx then pipe = nil end 14 | 15 | if pipe then 16 | log.info("Run as child thread") 17 | 18 | uv.poll_zmq(pipe):start(function(handle, err, pipe) 19 | if err then 20 | if err:name() ~= 'ETERM' then 21 | log.fatal("pipe poll: ", err) 22 | end 23 | return stop_service() 24 | end 25 | 26 | local msg, err = pipe:recvx() 27 | if not msg then 28 | if err:name() == 'ETERM' then 29 | return stop_service() 30 | end 31 | if err:name() ~= 'EAGAIN' then 32 | log.fatal("recv msg: %s", tostring(err)) 33 | end 34 | return 35 | end 36 | 37 | if msg == 'CLOSE' then 38 | return stop_service() 39 | end 40 | end) 41 | 42 | elseif SERVICE.RUN_AS_SERVICE then 43 | log.info("Run as service") 44 | 45 | uv.timer():start(1000, 1000, function() 46 | if SERVICE.check_stop(0) then 47 | stop_service() 48 | end 49 | end) 50 | else 51 | log.info("Run as console") 52 | 53 | uv.signal():start(uv.SIGINT, stop_service) 54 | 55 | uv.signal():start(uv.SIGBREAK, stop_service) 56 | end 57 | end 58 | 59 | return { 60 | start_monitor = register_stop_monitor; 61 | stop_service = stop_service; 62 | } 63 | -------------------------------------------------------------------------------- /src/lib/spylog/iputil.lua: -------------------------------------------------------------------------------- 1 | local bit = require "bit32" 2 | 3 | local masks = { 4 | "128.0.0.0", 5 | "192.0.0.0", 6 | "224.0.0.0", 7 | "240.0.0.0", 8 | "248.0.0.0", 9 | "252.0.0.0", 10 | "254.0.0.0", 11 | "255.0.0.0", 12 | "255.128.0.0", 13 | 14 | "255.192.0.0", 15 | "255.224.0.0", 16 | "255.240.0.0", 17 | "255.248.0.0", 18 | "255.252.0.0", 19 | "255.254.0.0", 20 | "255.255.0.0", 21 | 22 | "255.255.128.0", 23 | "255.255.192.0", 24 | "255.255.224.0", 25 | "255.255.240.0", 26 | "255.255.248.0", 27 | "255.255.252.0", 28 | "255.255.254.0", 29 | 30 | "255.255.255.0", 31 | "255.255.255.128", 32 | "255.255.255.192", 33 | "255.255.255.224", 34 | "255.255.255.240", 35 | "255.255.255.248", 36 | "255.255.255.252", 37 | "255.255.255.254", 38 | "255.255.255.255" 39 | } 40 | 41 | local bin_mask, bin_not_mask = {}, {} 42 | 43 | local function ip2octets(s) 44 | local a, b, c, d = string.match(s, "^(%d+)%.(%d+)%.(%d+)%.(%d+)$") 45 | a,b,c,d = tonumber(a),tonumber(b),tonumber(c),tonumber(d) 46 | if not a then return end 47 | if a > 255 or b > 255 or c > 255 or d > 255 then 48 | return 49 | end 50 | return a, b, c, d 51 | end 52 | 53 | local function ip2int(s) 54 | local a, b, c, d = ip2octets(s) 55 | if not a then return end 56 | return a * (256 ^ 3) + b * (256 ^ 2) + c * (256 ^ 1) + d 57 | end 58 | 59 | local tmp = {} 60 | local function int2ip(s) 61 | if s < 0 or s > 0xFFFFFFFF then 62 | return 63 | end 64 | 65 | for i = 4,1,-1 do 66 | tmp[i] = math.mod(s, 256); 67 | s = math.floor( s / 256 ); 68 | end 69 | return table.concat(tmp, '.') 70 | end 71 | 72 | for i = 1, #masks do 73 | bin_mask[i] = ip2int(masks[i]) 74 | bin_not_mask[i] = 0xFFFFFFFF - bin_mask[i] 75 | 76 | bin_mask[tostring(i)] = bin_mask[i] 77 | bin_not_mask[tostring(i)] = bin_not_mask[i] 78 | end 79 | 80 | local function cidr2range(s) 81 | local a, b, c, d, cidr = string.match(s, "^(%d+)%.(%d+)%.(%d+)%.(%d+)/(%d+)$") 82 | a,b,c,d = tonumber(a),tonumber(b),tonumber(c),tonumber(d) 83 | if not a then return end 84 | if a > 255 or b > 255 or c > 255 or d > 255 then 85 | return 86 | end 87 | local mask, not_mask = bin_mask[cidr], bin_not_mask[cidr] 88 | if not mask then 89 | return 90 | end 91 | local net = a * (256 ^ 3) + b * (256 ^ 2) + c * (256 ^ 1) + d 92 | 93 | local low, hi = bit.band(net, mask), bit.bor(net, not_mask) 94 | return low, hi 95 | end 96 | 97 | local function load_cidrs(s) 98 | local t = {ip={}} 99 | 100 | for i = 1, #s do 101 | local ip = ip2int(s[i]) 102 | if ip then t.ip[s[i]] = true 103 | else 104 | local low, hi = cidr2range(s[i]) 105 | if low then 106 | t[#t+1] = {low, hi, s[i]} 107 | end 108 | end 109 | end 110 | 111 | table.sort(t, function(lhs, rhs) 112 | if lhs[1] == rhs[1] then 113 | return lhs[2] < rhs[2] 114 | end 115 | return lhs[1] < rhs[1] 116 | end) 117 | 118 | return t 119 | end 120 | 121 | local function find_cidr(ip, s) 122 | if s.ip[ip] then return ip end 123 | 124 | local bin, i, n = ip2int(ip), 1, #s 125 | if bin then 126 | while (i <= n) and (bin >= s[i][1]) do 127 | if bin <= s[i][2] then return s[i][3] end 128 | i = i + 1 129 | end 130 | end 131 | end 132 | 133 | local function cidr2mask(s) 134 | local ip = cidr2range(s) 135 | if not ip then return end 136 | local cidr = string.match(s, "^%d+%.%d+%.%d+%.%d+/(%d+)$") 137 | return int2ip(ip), masks[tonumber(cidr)] 138 | end 139 | 140 | return { 141 | load_cidrs = load_cidrs; 142 | cidr2mask = cidr2mask; 143 | cidr2range = cidr2mask; 144 | ip2int = ip2int; 145 | int2ip = int2ip; 146 | find_cidr = find_cidr; 147 | } -------------------------------------------------------------------------------- /src/lib/spylog/log.lua: -------------------------------------------------------------------------------- 1 | local config = require "spylog.config" 2 | 3 | local function build_writer() 4 | local SERVICE = require "LuaService" 5 | local config = require "spylog.config" 6 | local stdout_writer 7 | 8 | if not SERVICE.RUN_AS_SERVICE then 9 | stdout_writer = require 'log.writer.stdout'.new() 10 | end 11 | 12 | local writer = require "log.writer.list".new( 13 | require 'log.writer.file'.new(config.LOG.file), 14 | require 'log.writer.net.zmq'.new(config.LOG.zmq), 15 | stdout_writer 16 | ) 17 | return writer 18 | end 19 | 20 | if config.LOG.multithread then 21 | writer = require "log.writer.async.zmq".new('inproc://async.logger', 22 | config.main_thread and string.dump(build_writer) 23 | ) 24 | else 25 | writer = build_writer() 26 | end 27 | 28 | local log = require "log".new( 29 | config.LOG.level or "info", 30 | require "log.writer.prefix".new(config.LOG.prefix or "", writer), 31 | require "log.formatter.mix".new( 32 | require "log.formatter.pformat".new() 33 | ) 34 | ) 35 | 36 | return log -------------------------------------------------------------------------------- /src/lib/spylog/spawn.lua: -------------------------------------------------------------------------------- 1 | local uv = require "lluv" 2 | local ut = require "lluv.utils" 3 | 4 | local SIGNALS ={ 5 | SIGINT = uv.SIGINT, 6 | SIGBREAK = uv.SIGBREAK, 7 | SIGHUP = uv.SIGHUP, 8 | SIGWINCH = uv.SIGWINCH, 9 | SIGPIPE = uv.SIGPIPE, 10 | SIGQUIT = uv.SIGQUIT, 11 | SIGILL = uv.SIGILL, 12 | SIGABRT = uv.SIGABRT, 13 | SIGTRAP = uv.SIGTRAP, 14 | SIGIOT = uv.SIGIOT, 15 | SIGEMT = uv.SIGEMT, 16 | SIGFPE = uv.SIGFPE, 17 | SIGKILL = uv.SIGKILL, 18 | SIGBUS = uv.SIGBUS, 19 | SIGSEGV = uv.SIGSEGV, 20 | SIGSYS = uv.SIGSYS, 21 | SIGALRM = uv.SIGALRM, 22 | SIGUSR1 = uv.SIGUSR1, 23 | SIGUSR2 = uv.SIGUSR2, 24 | SIGCHLD = uv.SIGCHLD, 25 | SIGCLD = uv.SIGCLD, 26 | SIGPWR = uv.SIGPWR, 27 | SIGXCPU = uv.SIGXCPU, 28 | SIGTERM = uv.SIGTERM, 29 | } 30 | 31 | local SIGNALS_INVERT = {} 32 | for name, value in pairs(SIGNALS) do 33 | SIGNALS_INVERT[value] = name 34 | end 35 | 36 | local function signal_name(s) 37 | return SIGNALS_INVERT[s] or string.format('%d', s) 38 | end 39 | 40 | local StatusError = ut.class() do 41 | 42 | function StatusError:__init(status, signal, stderr) 43 | self._staus = assert(status) 44 | self._signal = assert(signal or SIGNALS.SIGTERM) 45 | self._stderr = stderr 46 | 47 | return self 48 | end 49 | 50 | function StatusError:cat() return 'SPAWN' end 51 | 52 | function StatusError:name() return 'ESTATUS' end 53 | 54 | function StatusError:no() return -1 end 55 | 56 | function StatusError:status() return self._staus end 57 | 58 | function StatusError:signal() return self._signal end 59 | 60 | function StatusError:signal_name() return signal_name(self._signal) end 61 | 62 | function StatusError:msg() 63 | return string.format("Status: %d Signal: %s", self:status(), self:signal_name()) 64 | end 65 | 66 | function StatusError:ext() return end 67 | 68 | function StatusError:__tostring() 69 | local err = string.format("[%s][%s] %s", 70 | self:cat(), self:name(), self:msg() 71 | ) 72 | return err 73 | end 74 | 75 | function StatusError:__eq(rhs) 76 | return self:cat() == rhs:cat() 77 | and self:name() == rhs:name() 78 | and self:status() == rhs:status() 79 | end 80 | 81 | end 82 | 83 | local function P(read, write, pipe) 84 | local ioflags = 0 85 | if read then ioflags = ioflags + uv.READABLE_PIPE end 86 | if write then ioflags = ioflags + uv.WRITABLE_PIPE end 87 | if ioflags ~= 0 then 88 | if not pipe then 89 | pipe = uv.pipe() 90 | ioflags = ioflags + uv.CREATE_PIPE 91 | else 92 | ioflags = ioflags + uv.INHERIT_STREAM 93 | end 94 | end 95 | 96 | return { 97 | stream = pipe, 98 | flags = ioflags + uv.PROCESS_DETACHED 99 | } 100 | end 101 | 102 | local function spawn_ex(file, args, env, timeout, on_management, on_stdout, on_stderr) 103 | local stdout = on_stdout and P(false, true) or {} 104 | local stderr = on_stderr and P(false, true) or {} 105 | 106 | if type(args) == 'string' then 107 | args = {args} 108 | end 109 | 110 | local opt = { 111 | file = file, 112 | args = args or {}, 113 | env = env, 114 | stdio = {{}, stdout, stderr}, 115 | } 116 | 117 | local exit_code, run_error, timer 118 | 119 | local proc, pid = uv.spawn(opt, function(handle, err, status, signal) 120 | handle:close() 121 | if timer then 122 | timer:close() 123 | timer = nil 124 | end 125 | 126 | if stdout.stream then stdout.stream:close() stdout.stream = nil end 127 | if stderr.stream then stderr.stream:close() stderr.stream = nil end 128 | 129 | on_management('exit', err, status, signal) 130 | end) 131 | 132 | if not proc then 133 | if stdout.stream then stdout.stream:close() stdout.stream = nil end 134 | if stderr.stream then stderr.stream:close() stderr.stream = nil end 135 | return nil, pid 136 | end 137 | 138 | if timeout then 139 | timer = uv.timer():start(timeout, function() 140 | timer:close() 141 | timer = nil 142 | proc:kill() 143 | end) 144 | end 145 | 146 | local function on_data(cb) return function(self, err, data) 147 | if not err then return cb(data) end 148 | 149 | local typ = (self == stdout.stream) and 'stdout' or 'stderr' 150 | if err and err:name() == 'EOF' then 151 | return 152 | end 153 | on_management(typ, err, data) 154 | end end 155 | 156 | if on_stdout then stdout.stream:start_read(on_data(on_stdout)) end 157 | if on_stderr then stderr.stream:start_read(on_data(on_stderr)) end 158 | 159 | return proc, pid 160 | end 161 | 162 | local function spawn(file, args, timeout, cb) 163 | return spawn_ex(file, args, nil, timeout, cb, 164 | function(data) cb('stdout', nil, data) end, 165 | function(data) cb('stderr', nil, data) end 166 | ) 167 | end 168 | 169 | local function pipe(commands, timeout, cb) 170 | local processes, timer = {} 171 | 172 | local function interrupt() 173 | if timer then 174 | timer:close() 175 | timer = nil 176 | end 177 | 178 | for process in pairs(processes) do 179 | process:kill() 180 | end 181 | end 182 | 183 | local last_id, stdout, stderr 184 | for i, command in ipairs(commands) do 185 | local file, args = command[1], command[2] 186 | 187 | if type(args) == 'string' then args = {args} end 188 | 189 | local stdin = stdout and P(true, false, stdout.stream) or {} 190 | stdout = P(false, true) 191 | stderr = P(false, true) 192 | 193 | local opt = { 194 | file = file, 195 | args = args or {}, 196 | stdio = {stdin or {}, stdout, stderr}, 197 | } 198 | 199 | local command_id, active_command = i, command 200 | local process = uv.spawn(opt, function(handle, err, status, signal) 201 | processes[handle] = nil 202 | if timer and not next(processes) then 203 | timer:close() 204 | timer = nil 205 | end 206 | 207 | if not err then 208 | if not (active_command.ignore_status or status == 0) then 209 | err = StatusError.new(status, signal) 210 | end 211 | end 212 | 213 | cb(command_id, 'exit', err, status, signal) 214 | 215 | if not next(processes) then 216 | cb(0, 'done') 217 | end 218 | end) 219 | 220 | processes[process] = true 221 | 222 | stderr.stream:start_read(function(self, err, data) 223 | if err and err:name() == 'EOF' then return end 224 | cb(command_id, 'stderr', err, data) 225 | end) 226 | end 227 | 228 | if stdout then 229 | local command_id = #commands 230 | stdout.stream:start_read(function(self, err, data) 231 | if err and err:name() == 'EOF' then return end 232 | cb(command_id, 'stdout', err, data) 233 | end) 234 | end 235 | 236 | if next(processes) and timeout then 237 | timer = uv.timer():start(timeout, function() 238 | interrupt(cb) 239 | end) 240 | end 241 | end 242 | 243 | local function chain_(i, commands, timeout, cb) 244 | local command = commands[i] 245 | if not command then return uv.defer(cb, 0, 'done') end 246 | 247 | spawn(command[1], command[2], timeout, function(typ, err, status, signal) 248 | if typ == 'exit' then 249 | 250 | if not err then 251 | if not (command.ignore_status or status == 0) then 252 | err = StatusError.new(status, signal) 253 | end 254 | end 255 | 256 | uv.defer(cb, i, 'exit', err, status, signal) 257 | 258 | if not err then 259 | if command.ignore_status or status == 0 then 260 | return uv.defer(chain_, i + 1, commands, timeout, cb) 261 | end 262 | end 263 | 264 | return uv.defer(cb, 0, 'done') 265 | end 266 | 267 | cb(i, typ, err, status, signal) 268 | end) 269 | end 270 | 271 | local function chain(commands, timeout, cb) 272 | return chain_(1, commands, timeout, cb) 273 | end 274 | 275 | return setmetatable({ 276 | estatus = StatusError.new; 277 | pipe = pipe; 278 | chain = chain; 279 | spawn_ex = spawn_ex; 280 | },{__call = function(_, ...) return spawn(...) end}) 281 | -------------------------------------------------------------------------------- /src/lib/spylog/var.lua: -------------------------------------------------------------------------------- 1 | local Format do 2 | 3 | local lpeg = require "lpeg" 4 | 5 | local P, C, Cs, Ct, Cp, S = lpeg.P, lpeg.C, lpeg.Cs, lpeg.Ct, lpeg.Cp, lpeg.S 6 | 7 | local any = P(1) 8 | local sym = any-S':}' 9 | local esc = P'%%' / '%%' 10 | local var = P'%{' * C(sym^1) * '}' 11 | local fmt = P'%{' * C(sym^1) * ':' * C(sym^1) * '}' 12 | 13 | local function LpegFormat(str, context) 14 | local unknown 15 | 16 | local function fmt_sub(k, fmt) 17 | local v = context[k] 18 | if v == nil then 19 | local n = tonumber(k) 20 | if n then v = context[n] end 21 | end 22 | 23 | if v ~= nil then 24 | return string.format("%"..fmt, context[k]) 25 | end 26 | 27 | unknown = unknown or {} 28 | unknown[k] = '' 29 | end 30 | 31 | local function var_sub(k) 32 | local v = context[k] 33 | if v == nil then 34 | local n = tonumber(k) 35 | if n then v = context[n] end 36 | end 37 | if v ~= nil then 38 | return tostring(v) 39 | end 40 | unknown = unknown or {} 41 | unknown[k] = '' 42 | end 43 | 44 | local pattern = Cs((esc + (fmt / fmt_sub) + (var / var_sub) + any)^0) 45 | 46 | return pattern:match(str), unknown 47 | end 48 | 49 | local function LuaFormat(str, context) 50 | local unknown 51 | 52 | -- %{name:format} 53 | str = string.gsub(str, '%%%{([%w_][%w_]*)%:([-0-9%.]*[cdeEfgGiouxXsq])%}', 54 | function(k, fmt) 55 | local v = context[k] 56 | if v == nil then 57 | local n = tonumber(k) 58 | if n then v = context[n] end 59 | end 60 | 61 | if v ~= nil then 62 | return string.format("%"..fmt, context[k]) 63 | end 64 | unknown = unknown or {} 65 | unknown[k] = '' 66 | end 67 | ) 68 | 69 | -- %{name} 70 | return str:gsub('%%%{([%w_][%w_]*)%}', function(k) 71 | local v = context[k] 72 | if v == nil then 73 | local n = tonumber(k) 74 | if n then v = context[n] end 75 | end 76 | if v ~= nil then 77 | return tostring(v) 78 | end 79 | unknown = unknown or {} 80 | unknown[k] = '' 81 | end), unknown 82 | end 83 | 84 | Format = function(str, context) 85 | if string.find(str, '%%', 1, true) then 86 | return LpegFormat(str, context) 87 | end 88 | return LuaFormat(str, context) 89 | end 90 | 91 | end 92 | 93 | local combine do 94 | 95 | local mt = { 96 | __index = function(self, k) 97 | for i = 1, #self do 98 | if self[i][k] ~= nil then 99 | return self[i][k] 100 | end 101 | end 102 | end 103 | } 104 | 105 | combine = function(t) 106 | return setmetatable(t, mt) 107 | end 108 | 109 | end 110 | 111 | return { 112 | format = Format; 113 | combine = combine; 114 | } -------------------------------------------------------------------------------- /src/lib/spylog/version.lua: -------------------------------------------------------------------------------- 1 | -- This file uses also to make installer. 2 | -- !!! Do not change single quotes 3 | -- !!! Do not define more than one value per line 4 | return { 5 | _NAME = 'SpyLog'; 6 | _VERSION = '0.0.3-dev'; 7 | _COPYRIGHT = 'Copyright (C) 2015-2018 Alexey Melnichuk'; 8 | _URL = 'https://github.com/moteus/lua-spylog'; 9 | _LICENSE = 'MIT'; 10 | } -------------------------------------------------------------------------------- /src/spylog/init.lua: -------------------------------------------------------------------------------- 1 | -- Configuration file for LuaService 2 | 3 | return { 4 | tracelevel = 7, 5 | name = "spylog", 6 | display_name = "SpyLog", 7 | script = "main.lua", 8 | lua_cpath = '!\\..\\lib\\?.dll', 9 | lua_path = '!\\..\\lib\\?.lua;' .. 10 | '!\\..\\lib\\?\\init.lua;' .. 11 | '!\\..\\action\\lib\\?.lua;' .. 12 | '!\\..\\filter\\lib\\?.lua;' .. 13 | '!\\..\\jail\\lib\\?.lua'; 14 | } 15 | -------------------------------------------------------------------------------- /src/spylog/main.lua: -------------------------------------------------------------------------------- 1 | local SERVICE = require "LuaService" 2 | local config = require "spylog.config" 3 | config.LOG.prefix = "[spylog] " 4 | config.main_thread = true 5 | config.LOG.multithread = true 6 | --------------------------------------------------- 7 | 8 | local version = require "spylog.version" 9 | local zthreads = require "lzmq.threads" 10 | local ztimer = require "lzmq.timer" 11 | local uv = require "lluv" 12 | local stp = require "StackTracePlus" 13 | local loglib = require "log" 14 | local log = require "spylog.log" 15 | local exit = require "spylog.exit" 16 | 17 | log.info('Starting %s version %s. %s', version._NAME, version._VERSION, version._COPYRIGHT) 18 | 19 | local init_thread = function(...) 20 | require "LuaService" 21 | local config = require "spylog.config" 22 | config.LOG.multithread = true 23 | return ... 24 | end 25 | 26 | local THREADS = { 27 | {'filter', '@../filter/main.lua'}; 28 | {'jail', '@../jail/main.lua' }; 29 | {'action', '@../action/main.lua'}; 30 | } 31 | 32 | local threads = {} 33 | local function start_threads() 34 | for _, t in ipairs(THREADS) do 35 | threads[t[1]] = zthreads.xactor{t[2], prelude=init_thread}:start() 36 | ztimer.sleep(1000) 37 | end 38 | end 39 | 40 | local ok, err = pcall(start_threads) 41 | if not ok then 42 | log.alert("Can not start work thread: %s", tostring(err)) 43 | ztimer.sleep(500) 44 | return SERVICE.exit() 45 | end 46 | 47 | exit.start_monitor() 48 | 49 | uv.timer():start(1000, 10000, function() 50 | for name, thread in pairs(threads) do 51 | if not thread:alive() then 52 | log.alert("%s thread dead!", name) 53 | return exit.stop_service() 54 | end 55 | end 56 | end) 57 | 58 | local ok, err = pcall(uv.run, stp.stacktrace) 59 | 60 | if not ok then 61 | log.alert(err) 62 | end 63 | 64 | for _, thread in pairs(threads) do 65 | if thread:alive() then 66 | thread:send("CLOSE") 67 | end 68 | end 69 | 70 | log.info("Service stopped") 71 | 72 | -- allow proceed all log messages 73 | ztimer.sleep(1000) 74 | 75 | -- terminate workers and close files 76 | loglib.close() 77 | 78 | SERVICE.exit() 79 | --------------------------------------------------------------------------------