catflap 0.0.2 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.rubocop.yml +21 -0
- data/.rubocop_todo.yml +7 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +49 -0
- data/LICENSE +20 -0
- data/README.md +134 -0
- data/Rakefile +6 -0
- data/bin/catflap +71 -64
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/catflap.gemspec +32 -0
- data/etc/config.yaml +30 -0
- data/etc/init.d/catflap +89 -0
- data/etc/passfile.yaml +7 -0
- data/lib/catflap.rb +108 -64
- data/lib/catflap/command.rb +102 -0
- data/lib/catflap/firewall.rb +56 -0
- data/lib/catflap/http.rb +288 -0
- data/lib/catflap/netfilter/writer.rb +127 -0
- data/lib/catflap/plugins/firewall/iptables.rb +104 -0
- data/lib/catflap/plugins/firewall/netfilter.rb +114 -0
- data/lib/catflap/plugins/firewall/plugin.rb +67 -0
- data/lib/catflap/version.rb +5 -0
- data/lib/netfilter/writer.rb +125 -0
- data/ui/css/catflap.css +44 -0
- data/ui/images/catflap.png +0 -0
- data/ui/index.rhtml +23 -0
- data/ui/js/catflap.js +85 -0
- data/ui/js/sha256.js +166 -0
- metadata +109 -11
- data/lib/catflap-http.rb +0 -111
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'open3'
|
2
|
+
require 'resolv'
|
3
|
+
|
4
|
+
# Mixin module to add functions to firewall drivers that need them.
|
5
|
+
#
|
6
|
+
# It is methods in this module that must run with root privileges and at some
|
7
|
+
# point it may form the basis for a micro-service that runs with system
|
8
|
+
# privileges, so that the webserver can be run as a non-privileged user.
|
9
|
+
#
|
10
|
+
# @author Nyk Cowham <nykcowham@gmail.com>
|
11
|
+
module Firewall
|
12
|
+
# http://blog.markhatton.co.uk/2011/03/15/regular-expressions-for-ip-addresses-cidr-ranges-and-hostnames/
|
13
|
+
CIDR_PATTERN = %r{
|
14
|
+
^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}
|
15
|
+
([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])
|
16
|
+
(\/([0-9]|[1-2][0-9]|3[0-2]))?$
|
17
|
+
}x
|
18
|
+
|
19
|
+
# Execute firewall commands in a forked process.
|
20
|
+
# @param [String] output firewall command string to be forked and executed.
|
21
|
+
# @return [String] any output returned by the forked process is returned.
|
22
|
+
# @raise StandardError when the forked process returns an error.
|
23
|
+
|
24
|
+
def execute(output)
|
25
|
+
puts output if @verbose
|
26
|
+
return if @noop
|
27
|
+
|
28
|
+
out, err, = Open3.capture3 output << ' 2>/dev/null'
|
29
|
+
raise err if err != ''
|
30
|
+
out
|
31
|
+
end
|
32
|
+
|
33
|
+
# Execute firewall command in forked process to see if it emits an error.
|
34
|
+
#
|
35
|
+
# This is used by the 'check' command to see if an IP rule is already in the
|
36
|
+
# firewall allow chain or not. It is to get around the quirkiness of the
|
37
|
+
# netfilter -C check rule.
|
38
|
+
# @param [String] output firewall command string to be forked and executed.
|
39
|
+
# @return [Boolean] true if no error was returned from the forked process.
|
40
|
+
|
41
|
+
def execute_true?(output)
|
42
|
+
_, err, = Open3.capture3 output << ' 2>/dev/null'
|
43
|
+
(err == '')
|
44
|
+
end
|
45
|
+
|
46
|
+
# Data validation method to ensure that user-submitted IP addresses can be
|
47
|
+
# resolved to a valid IP address. This prevents any attempt at a shell/command
|
48
|
+
# injection attack.
|
49
|
+
# @param [String] suspect the user-submitted address to be validated.
|
50
|
+
# @raise Resolve::ResolvError if string doesn't resolve to an IP address.
|
51
|
+
|
52
|
+
def assert_valid_ipaddr(suspect)
|
53
|
+
return suspect if suspect =~ CIDR_PATTERN
|
54
|
+
Resolv.getaddress(suspect) # raises Resolv::ResolvError on bad IP.
|
55
|
+
end
|
56
|
+
end
|
data/lib/catflap/http.rb
ADDED
@@ -0,0 +1,288 @@
|
|
1
|
+
require 'catflap'
|
2
|
+
require 'webrick'
|
3
|
+
require 'json'
|
4
|
+
include WEBrick
|
5
|
+
|
6
|
+
##
|
7
|
+
# Module to implement a WEBrick web server.
|
8
|
+
#
|
9
|
+
# @author Nyk Cowham <nykcowham@gmail.com>
|
10
|
+
module CfWebserver
|
11
|
+
# Add a mime type for *.rhtml files
|
12
|
+
HTTPUtils::DefaultMimeTypes.store('rhtml', 'text/html')
|
13
|
+
|
14
|
+
# Factory method to generate a new WEBrick server.
|
15
|
+
# @param [Catflap] cf a fully instantiated Catflap object.
|
16
|
+
# @param [Boolean] https set to true to generate an HTTPS server.
|
17
|
+
# @return void
|
18
|
+
def generate_server(cf, https = false)
|
19
|
+
port = (https == true) ? cf.https['port'] : cf.port
|
20
|
+
|
21
|
+
# Expand relative paths - particularly important for daemonizing.
|
22
|
+
docroot = File.expand_path cf.docroot
|
23
|
+
|
24
|
+
config = {
|
25
|
+
BindAddress: cf.listen_addr,
|
26
|
+
Port: port,
|
27
|
+
DocumentRoot: docroot,
|
28
|
+
StartCallback: lambda do
|
29
|
+
# Write the pid to file when the server starts.
|
30
|
+
if File.writable? cf.pid_path
|
31
|
+
File.open(get_pidfile(cf, https), 'w') do |file|
|
32
|
+
file.puts Process.pid.to_s
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end,
|
36
|
+
StopCallback: lambda do
|
37
|
+
# Delete the pid file when the server shuts down.
|
38
|
+
pidfile = get_pidfile(cf, https)
|
39
|
+
File.delete pidfile if File.exist? pidfile
|
40
|
+
end
|
41
|
+
}
|
42
|
+
|
43
|
+
config[:ServerType] = Daemon if cf.daemonize
|
44
|
+
|
45
|
+
if https
|
46
|
+
require 'webrick/https'
|
47
|
+
require 'openssl'
|
48
|
+
|
49
|
+
if File.readable? cf.https['certificate']
|
50
|
+
cert = OpenSSL::X509::Certificate.new File.read cf.https['certificate']
|
51
|
+
end
|
52
|
+
|
53
|
+
if File.readable? cf.https['private_key']
|
54
|
+
pkey = OpenSSL::PKey::RSA.new File.read cf.https['private_key']
|
55
|
+
end
|
56
|
+
|
57
|
+
config[:SSLEnable] = true
|
58
|
+
|
59
|
+
if cert && pkey
|
60
|
+
config[:SSLCertificate] = cert
|
61
|
+
config[:SSLPrivateKey] = pkey
|
62
|
+
else
|
63
|
+
# We don't have a certificate so generate a new self-signed certificate.
|
64
|
+
config[:SSLCertName] = [%w(CN localhost)]
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
server = HTTPServer.new(config)
|
70
|
+
yield server if block_given?
|
71
|
+
|
72
|
+
%w(INT TERM).each do |signal|
|
73
|
+
trap(signal) { server.shutdown }
|
74
|
+
end
|
75
|
+
|
76
|
+
server.start
|
77
|
+
end
|
78
|
+
|
79
|
+
# Method to start the WEBrick web server.
|
80
|
+
# @param [Catflap] cf a fully instantiated Catflap object.
|
81
|
+
# @param [Boolean] https set to true to start an HTTPS server.
|
82
|
+
# @return void
|
83
|
+
def server_start(cf, https = false)
|
84
|
+
generate_server(cf, https) do |server|
|
85
|
+
server.mount cf.endpoint, CfApiServlet, cf
|
86
|
+
|
87
|
+
# Redirect HTTP to HTTPS if force option is set.
|
88
|
+
if !https && cf.https['force']
|
89
|
+
server.mount_proc '/' do |req, res|
|
90
|
+
res.set_redirect WEBrick::HTTPStatus::TemporaryRedirect,
|
91
|
+
'https://' + req.server_name
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Method to stop the WEBrick web server.
|
98
|
+
# @param [Catflap] cf a fully instantiated Catflap object.
|
99
|
+
# @param [Boolean] https set to true to stop an HTTPS server.
|
100
|
+
# @return [Boolean] true if process termination was successful.
|
101
|
+
def server_stop(cf, https = false)
|
102
|
+
pid = get_pid(cf, https)
|
103
|
+
Process.kill('INT', pid) if pid > 0
|
104
|
+
end
|
105
|
+
|
106
|
+
# Method to get the status of the WEBrick web server and process id.
|
107
|
+
# @param [Catflap] cf a fully instantiated Catflap object.
|
108
|
+
# @param [Boolean] https set to true to get status of an HTTPS server.
|
109
|
+
# @return [Hash<Sym, Object>] the process id or 0 if there is no pid.
|
110
|
+
def server_status(cf, https = false)
|
111
|
+
{
|
112
|
+
pid: get_pid(cf, https),
|
113
|
+
address: cf.listen_addr,
|
114
|
+
port: https ? cf.https['port'] : cf.port
|
115
|
+
}
|
116
|
+
end
|
117
|
+
|
118
|
+
# Method to get the process id of the running process.
|
119
|
+
# @param [Catflap] cf a fully instantiated Catflap object.
|
120
|
+
# @param [Boolean] https set to true to get process id of an HTTPS server.
|
121
|
+
# @return [Integer] the process id or 0 if there is no pid.
|
122
|
+
def get_pid(cf, https = false)
|
123
|
+
pidfile = get_pidfile(cf, https)
|
124
|
+
pid = nil
|
125
|
+
if File.readable? pidfile
|
126
|
+
File.open(pidfile, 'r') do |file|
|
127
|
+
pid = file.readline
|
128
|
+
end
|
129
|
+
end
|
130
|
+
pid.to_i
|
131
|
+
end
|
132
|
+
|
133
|
+
# Method to get the pid file path
|
134
|
+
# @param [Catflap] cf a fully instantiated Catflap object.
|
135
|
+
# @param [Boolean] https set to true to get pidfile of an HTTPS server.
|
136
|
+
# @return [String] the file path to the pid file.
|
137
|
+
def get_pidfile(cf, https = false)
|
138
|
+
filename = https ? 'catflap-https.pid' : 'catflap-http.pid'
|
139
|
+
cf.pid_path + File::SEPARATOR + filename
|
140
|
+
end
|
141
|
+
|
142
|
+
##
|
143
|
+
# A WEBrick servlet class to handle API requrests
|
144
|
+
# @author Nyk Cowham <nykcowham@gmail.com>
|
145
|
+
class CfApiServlet < HTTPServlet::AbstractServlet
|
146
|
+
# Initializer to construct a new CfApiServlet object.
|
147
|
+
# @param [HTTPServer] server a WEBrick HTTP server object.
|
148
|
+
# @param [Catflap] cf a fully instantiated Catflap object.
|
149
|
+
# @return void
|
150
|
+
def initialize(server, cf)
|
151
|
+
super server
|
152
|
+
@cf = cf
|
153
|
+
end
|
154
|
+
|
155
|
+
# Implementation of HTTPServlet::AbstractServlet method to handle GET
|
156
|
+
# method requests.
|
157
|
+
# @param [HTTPRequest] req a WEBrick::HTTPRequest object.
|
158
|
+
# @param [HTTPResponse] resp a WEBrick::HTTPResponse object.
|
159
|
+
# @return void
|
160
|
+
# rubocop:disable Style/MethodName
|
161
|
+
def do_POST(req, resp)
|
162
|
+
# Split the path into piece
|
163
|
+
path = req.path[1..-1].split('/')
|
164
|
+
|
165
|
+
# We don't want to cache catflap login page so set response headers.
|
166
|
+
# Chrome and FF respect the no-store, while IE respects no-cache.
|
167
|
+
resp['Cache-Control'] = 'no-cache, no-store'
|
168
|
+
resp['Pragma'] = 'no-cache' # Legacy
|
169
|
+
resp['Expires'] = '-1' # Microsoft advises this for older IE browsers.
|
170
|
+
|
171
|
+
response_class = CfRestService.const_get 'CfRestService'
|
172
|
+
|
173
|
+
raise "#{response_class} not a Class" unless response_class.is_a?(Class)
|
174
|
+
|
175
|
+
raise HTTPStatus::NotFound unless path[1]
|
176
|
+
|
177
|
+
response_method = path[1].to_sym
|
178
|
+
# Make sure the method exists in the class
|
179
|
+
raise HTTPStatus::NotFound unless response_class
|
180
|
+
.respond_to? response_method
|
181
|
+
|
182
|
+
if :sync == response_method
|
183
|
+
resp.body = response_class.send response_method, req, resp, @cf
|
184
|
+
end
|
185
|
+
|
186
|
+
if :knock == response_method
|
187
|
+
resp.body = response_class.send response_method, req, resp, @cf
|
188
|
+
end
|
189
|
+
|
190
|
+
# Remaining path segments get passed in as arguments to the method
|
191
|
+
if path.length > 2
|
192
|
+
resp.body = response_class.send response_method, req, resp,
|
193
|
+
@cf, path[1..-1]
|
194
|
+
else
|
195
|
+
resp.body = response_class.send response_method, req, resp, @cf
|
196
|
+
end
|
197
|
+
raise HTTPStatus::OK
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
# rubocop:enable Style/MethodName
|
202
|
+
|
203
|
+
##
|
204
|
+
# REST service to handle REST requests from CfApiServlet.
|
205
|
+
# @author Nyk Cowham <nykcowham@gmail.com>
|
206
|
+
module CfRestService
|
207
|
+
# REST service handler Class
|
208
|
+
class CfRestService
|
209
|
+
# Numeric response code indicating that the token has expired.
|
210
|
+
STATUS_TOKEN_EXPIRED = 405
|
211
|
+
# Numeric response code indicating that the handshake was ok.
|
212
|
+
STATUS_SYNC_OK = 200
|
213
|
+
# Numeric response code indicating a failed authentication attempt.
|
214
|
+
STATUS_AUTH_FAIL = 401
|
215
|
+
# Numeric response code indicating a successful authentication attempt.
|
216
|
+
STATUS_AUTH_PASS = 200
|
217
|
+
|
218
|
+
# Handler method for handling sync/timestamp requests for handshaking.
|
219
|
+
#
|
220
|
+
# This is a handshake request from the browser for a timestamp to use
|
221
|
+
# to encrypt the pass phrase. This timestamp is passed back along with
|
222
|
+
# pass phrase. If the timestamp is older than the expiry then the token
|
223
|
+
# will be rejected. If the timestamp has not expired it will be used to
|
224
|
+
# generate a matching token for authentication.
|
225
|
+
# @return [Integer] an integer representation of a unix timestamp.
|
226
|
+
|
227
|
+
def self.sync(_req, _res, _cf)
|
228
|
+
result = {
|
229
|
+
Status: 'Handshake sync OK',
|
230
|
+
StatusCode: STATUS_SYNC_OK,
|
231
|
+
Timestamp: Time.new.to_i
|
232
|
+
}
|
233
|
+
JSON.generate(result)
|
234
|
+
end
|
235
|
+
|
236
|
+
# Handler method for handling knock requests for authentication.
|
237
|
+
# @param [WEBRick::HTTPRequest] req a WEBrick request object.
|
238
|
+
# @param [WEBrick::HTTPResponse] resp a WEBrick response object.
|
239
|
+
# @param [Catflap] cf a fully instantiated Catflap object.
|
240
|
+
# @return void
|
241
|
+
|
242
|
+
def self.knock(req, _resp, cf)
|
243
|
+
ip = req.peeraddr.pop
|
244
|
+
query = req.query
|
245
|
+
passkey = query['_key']
|
246
|
+
|
247
|
+
# Calculate difference between the timestamp sent to the client and
|
248
|
+
# the current timestamp.
|
249
|
+
ts_delta = Time.new.to_i - query['ts'].to_i
|
250
|
+
|
251
|
+
if ts_delta > cf.token_ttl
|
252
|
+
result = {
|
253
|
+
Status: 'Expired Token',
|
254
|
+
StatusCode: STATUS_TOKEN_EXPIRED
|
255
|
+
}
|
256
|
+
JSON.generate(result)
|
257
|
+
end
|
258
|
+
|
259
|
+
# If we have a matching key in the passfile then create a test token.
|
260
|
+
unless cf.passphrases[query['_key']].nil?
|
261
|
+
test_token = cf.generate_token(cf.passphrases[passkey], query['ts'])
|
262
|
+
end
|
263
|
+
|
264
|
+
# by default we tell the browser to reload the page, but we can configure
|
265
|
+
# catflap in the configuration file to redirect to some other URL.
|
266
|
+
redirect_url = cf.redirect_url ? cf.redirect_url : 'reload'
|
267
|
+
|
268
|
+
if test_token && test_token == query['token']
|
269
|
+
# The tokens matched and validated so we add the address and respond
|
270
|
+
# to the browser.
|
271
|
+
cf.firewall.add_address ip unless cf.firewall.check_address(ip)
|
272
|
+
|
273
|
+
result = {
|
274
|
+
Status: 'Authenticated',
|
275
|
+
StatusCode: STATUS_AUTH_PASS,
|
276
|
+
RedirectUrl: redirect_url
|
277
|
+
}
|
278
|
+
|
279
|
+
else
|
280
|
+
result = {
|
281
|
+
Status: 'Authentication failed',
|
282
|
+
StatusCode: STATUS_AUTH_FAIL
|
283
|
+
}
|
284
|
+
end
|
285
|
+
JSON.generate(result)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'catflap/firewall'
|
2
|
+
include Firewall
|
3
|
+
|
4
|
+
# Mixin module to add rule handling functions to netfilter-based drivers.
|
5
|
+
#
|
6
|
+
# @author Nyk Cowham <nykcowham@gmail.com>
|
7
|
+
module NetfilterWriter
|
8
|
+
# Class providing a DSL for defining netfilter rules
|
9
|
+
# @author Nyk Cowham <nykcowham@gmail.com>
|
10
|
+
class Rules
|
11
|
+
attr_accessor :noop, :verbose, :match
|
12
|
+
|
13
|
+
def initialize(table, dports = nil)
|
14
|
+
@table = table
|
15
|
+
@dports = dports
|
16
|
+
@match = nil
|
17
|
+
@buffer = ''
|
18
|
+
end
|
19
|
+
|
20
|
+
# Chainable setter function: change the default table for rules.
|
21
|
+
# @param [String] table the name of the table (e.g. 'nat', 'filter', etc.)
|
22
|
+
# @return self
|
23
|
+
def table(table)
|
24
|
+
@table = table
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
# Chainable setter function: change the default destination ports for rules.
|
29
|
+
# @param [String] table the name of the table (e.g. 'nat', 'filter', etc.)
|
30
|
+
# @return self
|
31
|
+
def dports(dports)
|
32
|
+
@dports = dports
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
# Create, flush and delete chains and other iptable chain operations.
|
37
|
+
# @param [Symbol] cmd the operation to perform (:new, :delete, :flush)
|
38
|
+
# @param [String] chain name of the chain (e.g. INPUT, CATFLAP-DENY, etc.)
|
39
|
+
# @param [Hash] p parameters for specific iptables features.
|
40
|
+
# @return self
|
41
|
+
# @example
|
42
|
+
# Rules.new('nat').chain(:list, 'MY-CHAIN', numeric: true).flush
|
43
|
+
# => "iptables -t nat -n -L MY-CHAIN"
|
44
|
+
def chain(cmd, chain, p = {})
|
45
|
+
cmds = {
|
46
|
+
new: '-N', rename: '-E', delete: '-X', flush: '-F',
|
47
|
+
list_rules: '-S', list: '-L', zero: '-Z', policy: '-P'
|
48
|
+
}
|
49
|
+
|
50
|
+
@buffer << [
|
51
|
+
'iptables',
|
52
|
+
option('-t', @table), cmds[cmd], option('-n', p[:numeric]), chain,
|
53
|
+
option(false, p[:rulenum]), option(false, p[:to])
|
54
|
+
].compact.join(' ') << "\n"
|
55
|
+
|
56
|
+
self
|
57
|
+
end
|
58
|
+
|
59
|
+
# Create, flush and delete chains
|
60
|
+
# @param [String] cmd the operation to perform (add, delete, insert, etc.)
|
61
|
+
# @param [Hash] p parameters for specific iptables features.
|
62
|
+
# @param [Block] block will evaluate a block that will return true/false.
|
63
|
+
# @return self
|
64
|
+
def rule(cmd, p, &block)
|
65
|
+
# Evaluate a block expression and return early if it evaluates to false.
|
66
|
+
# If no block is passed it is equivalent to the block: { true }.
|
67
|
+
return self if block_given? && !instance_eval(&block)
|
68
|
+
|
69
|
+
raise ArgumentError, 'chain is a required argument' unless p[:chain]
|
70
|
+
assert_valid_ipaddr(p[:src]) if p[:src]
|
71
|
+
assert_valid_ipaddr(p[:dst]) if p[:dst]
|
72
|
+
|
73
|
+
# Map of commands for rules
|
74
|
+
cmds = {
|
75
|
+
add: '-A', delete: '-D', insert: '-I', replace: '-R', check: '-C'
|
76
|
+
}
|
77
|
+
|
78
|
+
@buffer << [
|
79
|
+
'iptables', option('-t', @table), cmds[cmd], p[:chain],
|
80
|
+
option(false, p[:rulenum]), option('-f', p[:frag]),
|
81
|
+
option('-s', p[:src]), option('-d', p[:dst]),
|
82
|
+
option('-o', p[:out]), option('-i', p[:in]),
|
83
|
+
option('-p', p[:proto] || 'tcp'), option('-m', p[:match] || @match),
|
84
|
+
option('--sport', p[:sports] || @sports),
|
85
|
+
option('--dport', p[:dports] || @dports), p[:jump] || p[:goto],
|
86
|
+
option('--to-port', p[:to_port])
|
87
|
+
].compact.join(' ') << "\n"
|
88
|
+
|
89
|
+
self
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def option(flag, value)
|
94
|
+
return flag if value.is_a?(TrueClass)
|
95
|
+
return flag.insert(0, '!') if value.is_a?(FalseClass)
|
96
|
+
return value if flag.is_a?(FalseClass)
|
97
|
+
flag << ' ' << value.to_s if flag && value
|
98
|
+
end
|
99
|
+
|
100
|
+
# Add a raw text rule, (e.g.: iptables -t nat CATFLAP-ALLOW ...)
|
101
|
+
# @param [String] raw_rule custom raw iptables command.
|
102
|
+
# @return self
|
103
|
+
def raw(raw_rule)
|
104
|
+
@buffer = raw_rule
|
105
|
+
self
|
106
|
+
end
|
107
|
+
|
108
|
+
# Flush the rule buffer and output the resulting iptables commands.
|
109
|
+
# @return [String] rule text that can be sent iptables user-space client.
|
110
|
+
def flush
|
111
|
+
out = @buffer
|
112
|
+
@buffer = ''
|
113
|
+
out
|
114
|
+
end
|
115
|
+
|
116
|
+
# Flush the rule and execute in iptables user-space client.
|
117
|
+
# @return void
|
118
|
+
def do
|
119
|
+
execute flush
|
120
|
+
end
|
121
|
+
|
122
|
+
# Flush the rule and execute commands and return success/fail value.
|
123
|
+
# @return [Boolean] true if the execution was successful.
|
124
|
+
def do?
|
125
|
+
execute_true? flush
|
126
|
+
end
|
127
|
+
end
|