├── .gitignore ├── LICENSE ├── README.md ├── lib └── resty │ └── wirefilter.lua ├── lua-resty-wirefilter-v1.0.0-1.rockspec └── t └── filter.t /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.swp 3 | *.swo 4 | t/servroot* 5 | /go -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Amir Keshavarz 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 | Name 2 | ==== 3 | 4 | lua-resty-wirefilter - LuaJIT FFI bindings to wirefilter, An execution engine for Wireshark-like filters 5 | 6 | Table of Contents 7 | ================= 8 | 9 | * [Name](#name) 10 | * [Status](#status) 11 | * [Synopsis](#synopsis) 12 | * [Methods](#methods) 13 | * [new](#new) 14 | * [exec](#exec) 15 | * [Installation](#installation) 16 | * [Authors](#authors) 17 | 18 | 19 | 20 | Synopsis 21 | ======== 22 | ```lua 23 | local wirefilter = require "resty.wirefilter" 24 | 25 | local wf, err = wirefilter:new({ 26 | fields = { 27 | ["http.user_agent"] = wirefilter.types.BYTES, 28 | ["http.port"] = wirefilter.types.INT, 29 | ["http.remote_ip"] = wirefilter.types.IP, 30 | ["ssl"] = wirefilter.types.BOOL 31 | }, 32 | filter = "http.user_agent matches \"(googlebot|facebook)\" && http.port == 80 && http.remote_ip == 192.168.0.1 && ssl" 33 | }) 34 | 35 | 36 | local match_result, err = wf:exec({ 37 | ["http.user_agent"] = "googlebot", 38 | ["http.port"] = 80, 39 | ["ssl"] = true, 40 | ["http.remote_ip"] = "192.168.0.1" 41 | }) 42 | ``` 43 | 44 | Methods 45 | ======= 46 | 47 | [Back to TOC](#table-of-contents) 48 | 49 | ### new 50 | 51 | `syntax: wf, err = wirefilter:new(args)` 52 | 53 | Creates a wirefilter instance. 54 | 55 | `args` is a table containing the following settings: 56 | 57 | - `fields` a table containing the necessary fields. 58 | - `filter` wirefilter-style filter 59 | 60 | `fields` contains the names of the fields and their type. We have 4 types in wirefilter: 61 | 62 | - Bytes: `wirefilter.types.BYTES` 63 | - Integer: `wirefilter.types.INT` 64 | - IP Address: `wirefilter.types.IP` 65 | - Boolean: `wirefilter.types.BOOL` 66 | 67 | `fields` example: 68 | ```lua 69 | fields = { 70 | ["http.user_agent"] = wirefilter.types.BYTES, 71 | ["http.port"] = wirefilter.types.INT, 72 | ["http.remote_ip"] = wirefilter.types.IP, 73 | ["ssl"] = wirefilter.types.BOOL 74 | } 75 | ``` 76 | 77 | 78 | ### exec 79 | 80 | `syntax: result, err = wirefilter:exec(values)` 81 | 82 | `exec` matches the given values against the filter. If will 83 | 84 | `values` is a table containing the fields and their values. 85 | 86 | `values` example: 87 | ```lua 88 | { 89 | ["http.user_agent"] = "googlebot", 90 | ["http.port"] = 80, 91 | ["ssl"] = true, 92 | ["http.remote_ip"] = "192.168.0.1" 93 | } 94 | ``` 95 | 96 | [Back to TOC](#table-of-contents) 97 | 98 | Installation 99 | ============ 100 | To run this module, You need to compile the wirefilter lib and put the .so file where OpenResty can find it: 101 | 102 | https://github.com/cloudflare/wirefilter/tree/master/ffi 103 | 104 | ## Build 105 | Run the following in the module directory: 106 | ``` 107 | luarocks make 108 | ``` 109 | 110 | You need to configure 111 | the [lua_package_path](https://github.com/chaoslawful/lua-nginx-module#lua_package_path) directive to 112 | add the path of your `lua-resty-wirefilter` source tree to ngx_lua's Lua module search path, as in 113 | 114 | ``` 115 | http { 116 | lua_package_path "/path/to/lua-resty-wirefilter/lib/?.lua;;"; 117 | ... 118 | } 119 | ``` 120 | 121 | 122 | and then load the library in Lua: 123 | 124 | ```lua 125 | local wf = require "resty.wirefilter" 126 | ``` 127 | 128 | [Back to TOC](#table-of-contents) 129 | 130 | Authors 131 | ======= 132 | 133 | Amir Keshavarz . 134 | 135 | [Back to TOC](#table-of-contents) 136 | -------------------------------------------------------------------------------- /lib/resty/wirefilter.lua: -------------------------------------------------------------------------------- 1 | local ffi = require("ffi"); 2 | local ffi_cdef = ffi.cdef 3 | local ffi_load = ffi.load 4 | local ffi_new = ffi.new 5 | 6 | local wirefilter = ffi_load "wirefilter"; 7 | 8 | ffi_cdef [[ 9 | 10 | typedef struct wirefilter_scheme wirefilter_scheme_t; 11 | typedef struct wirefilter_execution_context wirefilter_execution_context_t; 12 | typedef struct wirefilter_filter_ast wirefilter_filter_ast_t; 13 | typedef struct wirefilter_filter wirefilter_filter_t; 14 | 15 | typedef struct { 16 | const char *data; 17 | size_t length; 18 | } wirefilter_rust_allocated_str_t; 19 | 20 | typedef struct { 21 | const char *data; 22 | size_t length; 23 | } wirefilter_static_rust_allocated_str_t; 24 | 25 | typedef struct { 26 | const char *data; 27 | size_t length; 28 | } wirefilter_externally_allocated_str_t; 29 | 30 | typedef struct { 31 | const unsigned char *data; 32 | size_t length; 33 | } wirefilter_externally_allocated_byte_arr_t; 34 | 35 | typedef union { 36 | uint8_t success; 37 | struct { 38 | uint8_t _res1; 39 | wirefilter_rust_allocated_str_t msg; 40 | } err; 41 | struct { 42 | uint8_t _res2; 43 | wirefilter_filter_ast_t *ast; 44 | } ok; 45 | } wirefilter_parsing_result_t; 46 | 47 | typedef enum { 48 | WIREFILTER_TYPE_IP, 49 | WIREFILTER_TYPE_BYTES, 50 | WIREFILTER_TYPE_INT, 51 | WIREFILTER_TYPE_BOOL, 52 | } wirefilter_type_t; 53 | 54 | wirefilter_scheme_t *wirefilter_create_scheme(); 55 | void wirefilter_free_scheme(wirefilter_scheme_t *scheme); 56 | 57 | void wirefilter_add_type_field_to_scheme( 58 | wirefilter_scheme_t *scheme, 59 | wirefilter_externally_allocated_str_t name, 60 | wirefilter_type_t type 61 | ); 62 | 63 | wirefilter_parsing_result_t wirefilter_parse_filter( 64 | const wirefilter_scheme_t *scheme, 65 | wirefilter_externally_allocated_str_t input 66 | ); 67 | 68 | void wirefilter_free_parsing_result(wirefilter_parsing_result_t result); 69 | 70 | wirefilter_filter_t *wirefilter_compile_filter(wirefilter_filter_ast_t *ast); 71 | void wirefilter_free_compiled_filter(wirefilter_filter_t *filter); 72 | 73 | wirefilter_execution_context_t *wirefilter_create_execution_context( 74 | const wirefilter_scheme_t *scheme 75 | ); 76 | void wirefilter_free_execution_context( 77 | wirefilter_execution_context_t *exec_ctx 78 | ); 79 | 80 | void wirefilter_add_int_value_to_execution_context( 81 | wirefilter_execution_context_t *exec_ctx, 82 | wirefilter_externally_allocated_str_t name, 83 | int32_t value 84 | ); 85 | 86 | void wirefilter_add_bytes_value_to_execution_context( 87 | wirefilter_execution_context_t *exec_ctx, 88 | wirefilter_externally_allocated_str_t name, 89 | wirefilter_externally_allocated_byte_arr_t value 90 | ); 91 | 92 | void wirefilter_add_ipv6_value_to_execution_context( 93 | wirefilter_execution_context_t *exec_ctx, 94 | wirefilter_externally_allocated_str_t name, 95 | uint8_t value[16] 96 | ); 97 | 98 | void wirefilter_add_ipv4_value_to_execution_context( 99 | wirefilter_execution_context_t *exec_ctx, 100 | wirefilter_externally_allocated_str_t name, 101 | uint8_t value[4] 102 | ); 103 | 104 | void wirefilter_add_bool_value_to_execution_context( 105 | wirefilter_execution_context_t *exec_ctx, 106 | wirefilter_externally_allocated_str_t name, 107 | bool value 108 | ); 109 | 110 | bool wirefilter_match( 111 | const wirefilter_filter_t *filter, 112 | const wirefilter_execution_context_t *exec_ctx 113 | ); 114 | 115 | bool wirefilter_filter_uses( 116 | const wirefilter_filter_ast_t *ast, 117 | wirefilter_externally_allocated_str_t field_name 118 | ); 119 | 120 | uint64_t wirefilter_get_filter_hash(const wirefilter_filter_ast_t *ast); 121 | 122 | wirefilter_rust_allocated_str_t wirefilter_serialize_filter_to_json( 123 | const wirefilter_filter_ast_t *ast 124 | ); 125 | 126 | void wirefilter_free_string(wirefilter_rust_allocated_str_t str); 127 | 128 | wirefilter_static_rust_allocated_str_t wirefilter_get_version(); 129 | 130 | 131 | ]] 132 | 133 | local _M = { 134 | _VERSION = '1.0.0', 135 | types = { 136 | BYTES = ffi.C.WIREFILTER_TYPE_BYTES, 137 | IP = ffi.C.WIREFILTER_TYPE_IP, 138 | BOOL = ffi.C.WIREFILTER_TYPE_BOOL, 139 | INT = ffi.C.WIREFILTER_TYPE_INT 140 | } 141 | } 142 | local mt = { 143 | __index = _M 144 | } 145 | 146 | function _M:new(args) 147 | local args = args or {} 148 | local fields = args.fields or {} 149 | local filter = args.filter or "" 150 | local fields_map = {} 151 | 152 | local scheme, err = self:init_scheme(fields, fields_map) 153 | if (scheme == nil) then 154 | return nil, err 155 | end 156 | 157 | local filter, err = self:create_filter(scheme, args.filter) 158 | if (filter == nil) then 159 | return nil, err 160 | end 161 | 162 | local self = { 163 | scheme = scheme, 164 | filter = filter, 165 | fields_map = fields_map 166 | } 167 | 168 | return setmetatable(self, mt) 169 | end 170 | 171 | function _M:create_execution_context(scheme) 172 | local context = ffi_new("wirefilter_execution_context_t*") 173 | local context = wirefilter.wirefilter_create_execution_context(scheme) 174 | if (context == nil) then 175 | return nil, "could not create execution context" 176 | end 177 | 178 | return context 179 | end 180 | 181 | function _M:match(filter, context) 182 | local match_result = wirefilter.wirefilter_match(filter, context) 183 | return match_result 184 | end 185 | 186 | function _M:free_execution_context(context) 187 | wirefilter.wirefilter_free_execution_context(context) 188 | end 189 | 190 | function _M:exec(values) 191 | 192 | local context, err = self:create_execution_context(self.scheme) 193 | if (context == nil) then 194 | return nil, err 195 | end 196 | 197 | for name, value in pairs(values) do 198 | local result, err = self:add_value_to_execution_context(context, { 199 | name = name, 200 | value = value 201 | }) 202 | 203 | if not result then 204 | return nil, err 205 | end 206 | 207 | end 208 | 209 | local match_result = self:match(self.filter, context) 210 | self:free_execution_context(context) 211 | 212 | return match_result 213 | 214 | end 215 | 216 | function _M:get_field(value) 217 | local field = self.fields_map[value.name] 218 | if (field == nil) then 219 | return nil, "field does not exist" 220 | end 221 | 222 | return field 223 | end 224 | 225 | function _M:add_value_to_execution_context(context, value) 226 | local field, err = self:get_field(value) 227 | if (field == nil) then 228 | return false, err 229 | end 230 | 231 | if (field == self.types.BYTES) then 232 | wirefilter.wirefilter_add_bytes_value_to_execution_context(context, self:wirefilter_string(value.name), 233 | self:wirefilter_byte(value.value)) 234 | 235 | elseif (field == self.types.BOOL) then 236 | wirefilter.wirefilter_add_bool_value_to_execution_context(context, self:wirefilter_string(value.name), 237 | value.value) 238 | 239 | elseif (field == self.types.INT) then 240 | local int_value, err = self:wirefilter_int(value.value) 241 | if (int_value == nil) then 242 | return false, err 243 | end 244 | 245 | wirefilter.wirefilter_add_int_value_to_execution_context(context, self:wirefilter_string(value.name), int_value) 246 | 247 | elseif (field == self.types.IP) then 248 | local ip_value, err = self:wirefilter_ip(value.value) 249 | if (ip_value == nil) then 250 | return false, err 251 | end 252 | 253 | wirefilter.wirefilter_add_ipv4_value_to_execution_context(context, self:wirefilter_string(value.name), ip_value) 254 | end 255 | 256 | return true 257 | end 258 | 259 | function _M:clear(filter, scheme) 260 | self:free_compiled_filter(filter) 261 | self:free_scheme(scheme) 262 | end 263 | 264 | function _M:free_compiled_filter(filter) 265 | wirefilter.wirefilter_free_compiled_filter(filter); 266 | end 267 | 268 | function _M:free_scheme(scheme) 269 | wirefilter.wirefilter_free_scheme(scheme); 270 | end 271 | 272 | function _M:create_filter(scheme, filter_string) 273 | 274 | local result = ffi_new("wirefilter_parsing_result_t") 275 | local result = wirefilter.wirefilter_parse_filter(scheme, self:wirefilter_string(filter_string)) 276 | 277 | if (result.success ~= 1) then 278 | return nil, "could not parse filter" 279 | end 280 | 281 | if (result.ok.ast == nil) then 282 | return nil, "could not parse filter" 283 | end 284 | 285 | local filter = ffi_new("wirefilter_filter_t*") 286 | local filter = wirefilter.wirefilter_compile_filter(result.ok.ast) 287 | 288 | if (filter == nil) then 289 | return nil, "could not compile filter" 290 | end 291 | 292 | return filter 293 | 294 | end 295 | 296 | function _M:init_scheme(fields, fields_map) 297 | 298 | local scheme, err = self:create_scheme() 299 | 300 | if (scheme == nil) then 301 | return nil, err 302 | end 303 | 304 | for name, type in pairs(fields) do 305 | self:add_type_field_to_scheme(scheme, fields_map, name, type) 306 | end 307 | 308 | return scheme 309 | 310 | end 311 | 312 | function _M:create_scheme() 313 | local scheme = ffi_new("wirefilter_scheme_t*") 314 | local scheme = wirefilter.wirefilter_create_scheme() 315 | 316 | if (scheme == nil) then 317 | return nil, "could not create scheme" 318 | end 319 | 320 | return scheme 321 | end 322 | 323 | function _M:add_type_field_to_scheme(scheme, fields_map, name, type) 324 | wirefilter.wirefilter_add_type_field_to_scheme(scheme, self:wirefilter_string(name), type) 325 | fields_map[name] = type 326 | end 327 | 328 | function _M:wirefilter_int(value) 329 | local value = tonumber(value) 330 | if (value == nil) then 331 | return nil, "number is not valid" 332 | end 333 | return value 334 | end 335 | 336 | function _M:wirefilter_string(value) 337 | local value = tostring(value) 338 | local str = ffi_new("wirefilter_externally_allocated_str_t", { 339 | data = value, 340 | length = string.len(value) 341 | }) 342 | return str 343 | end 344 | 345 | function _M:wirefilter_byte(value) 346 | local value = tostring(value) 347 | local bytes = ffi_new("wirefilter_externally_allocated_byte_arr_t", { 348 | data = value, 349 | length = string.len(value) 350 | }) 351 | return bytes 352 | end 353 | 354 | function _M:wirefilter_ip(value) 355 | 356 | local ip_tab = {} 357 | 358 | for s in string.gmatch(value, "[^.]+") do 359 | local ip_seg = tonumber(s) 360 | if (ip_seg ~= nil and ip_seg >= 0 and ip_seg <= 255) then 361 | table.insert(ip_tab, ip_seg) 362 | end 363 | end 364 | 365 | if (#ip_tab ~= 4) then 366 | return nil, "invalid ipv4 address" 367 | end 368 | 369 | local ip = ffi_new("uint8_t[4]", ip_tab) 370 | return ip 371 | end 372 | 373 | return _M 374 | -------------------------------------------------------------------------------- /lua-resty-wirefilter-v1.0.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-wirefilter" 2 | version = "v1.0.0-1" 3 | 4 | source = { 5 | url = "git://github.com/satrobit/lua-resty-wirefilter.git" 6 | } 7 | 8 | description = { 9 | summary = "LuaJIT FFI bindings to wirefilter - An execution engine for Wireshark-like filters", 10 | homepage = "https://github.com/satrobit/lua-resty-wirefilter", 11 | license = "MIT", 12 | maintainer = "amirkekh@gmail.com" 13 | } 14 | 15 | dependencies = { 16 | "lua >= 5.1" 17 | } 18 | 19 | build = { 20 | type = "builtin", 21 | modules = { 22 | ["resty.wirefilter"] = "lib/resty/wirefilter.lua" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /t/filter.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | 3 | run_tests(); 4 | 5 | __DATA__ 6 | 7 | === TEST 1: filter 8 | --- config 9 | location = /t { 10 | content_by_lua_block { 11 | local wirefilter = require "resty.wirefilter" 12 | local wf, err = wirefilter:new({ 13 | fields = { 14 | ["http.user_agent"] = wirefilter.types.BYTES, 15 | ["http.port"] = wirefilter.types.INT, 16 | ["http.remote_ip"] = wirefilter.types.IP, 17 | ["ssl"] = wirefilter.types.BOOL 18 | }, 19 | filter = "http.user_agent matches \"(googlebot|facebook)\" && http.port == 80 && http.remote_ip == 192.168.0.1 && ssl" 20 | }) 21 | 22 | local match_result, err = wf:exec({ 23 | ["http.user_agent"] = "googlebot", 24 | ["http.port"] = 80, 25 | ["ssl"] = true, 26 | ["http.remote_ip"] = "192.168.0.1" 27 | }) 28 | 29 | ngx.say(match_result) 30 | } 31 | } 32 | --- request 33 | GET /t 34 | --- response_body 35 | true 36 | --------------------------------------------------------------------------------