rcs-common 9.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +49 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +1 -0
- data/Rakefile +27 -0
- data/lib/rcs-common.rb +21 -0
- data/lib/rcs-common/binary.rb +64 -0
- data/lib/rcs-common/cgi.rb +7 -0
- data/lib/rcs-common/component.rb +87 -0
- data/lib/rcs-common/crypt.rb +71 -0
- data/lib/rcs-common/deploy.rb +96 -0
- data/lib/rcs-common/diagnosticable.rb +136 -0
- data/lib/rcs-common/evidence.rb +261 -0
- data/lib/rcs-common/evidence/addressbook.rb +173 -0
- data/lib/rcs-common/evidence/application.rb +59 -0
- data/lib/rcs-common/evidence/calendar.rb +62 -0
- data/lib/rcs-common/evidence/call.rb +185 -0
- data/lib/rcs-common/evidence/camera.rb +25 -0
- data/lib/rcs-common/evidence/chat.rb +272 -0
- data/lib/rcs-common/evidence/clibpoard.rb +58 -0
- data/lib/rcs-common/evidence/command.rb +50 -0
- data/lib/rcs-common/evidence/common.rb +78 -0
- data/lib/rcs-common/evidence/content/camera/001.jpg +0 -0
- data/lib/rcs-common/evidence/content/coin/wallet_bit.dat +0 -0
- data/lib/rcs-common/evidence/content/coin/wallet_lite.dat +0 -0
- data/lib/rcs-common/evidence/content/file/Einstein.docx +0 -0
- data/lib/rcs-common/evidence/content/file/arabic.docx +0 -0
- data/lib/rcs-common/evidence/content/mouse/001.jpg +0 -0
- data/lib/rcs-common/evidence/content/mouse/002.jpg +0 -0
- data/lib/rcs-common/evidence/content/mouse/003.jpg +0 -0
- data/lib/rcs-common/evidence/content/mouse/004.jpg +0 -0
- data/lib/rcs-common/evidence/content/print/001.jpg +0 -0
- data/lib/rcs-common/evidence/content/screenshot/001.jpg +0 -0
- data/lib/rcs-common/evidence/content/screenshot/002.jpg +0 -0
- data/lib/rcs-common/evidence/content/screenshot/003.jpg +0 -0
- data/lib/rcs-common/evidence/content/url/001.jpg +0 -0
- data/lib/rcs-common/evidence/content/url/002.jpg +0 -0
- data/lib/rcs-common/evidence/content/url/003.jpg +0 -0
- data/lib/rcs-common/evidence/device.rb +23 -0
- data/lib/rcs-common/evidence/download.rb +54 -0
- data/lib/rcs-common/evidence/exec.rb +0 -0
- data/lib/rcs-common/evidence/file.rb +129 -0
- data/lib/rcs-common/evidence/filesystem.rb +71 -0
- data/lib/rcs-common/evidence/info.rb +24 -0
- data/lib/rcs-common/evidence/keylog.rb +84 -0
- data/lib/rcs-common/evidence/mail.rb +237 -0
- data/lib/rcs-common/evidence/mic.rb +39 -0
- data/lib/rcs-common/evidence/mms.rb +36 -0
- data/lib/rcs-common/evidence/money.rb +676 -0
- data/lib/rcs-common/evidence/mouse.rb +62 -0
- data/lib/rcs-common/evidence/password.rb +60 -0
- data/lib/rcs-common/evidence/photo.rb +80 -0
- data/lib/rcs-common/evidence/position.rb +303 -0
- data/lib/rcs-common/evidence/print.rb +50 -0
- data/lib/rcs-common/evidence/screenshot.rb +53 -0
- data/lib/rcs-common/evidence/sms.rb +91 -0
- data/lib/rcs-common/evidence/url.rb +133 -0
- data/lib/rcs-common/fixnum.rb +48 -0
- data/lib/rcs-common/gridfs.rb +294 -0
- data/lib/rcs-common/heartbeat.rb +96 -0
- data/lib/rcs-common/keywords.rb +50 -0
- data/lib/rcs-common/mime.rb +65 -0
- data/lib/rcs-common/mongoid.rb +19 -0
- data/lib/rcs-common/pascalize.rb +62 -0
- data/lib/rcs-common/path_utils.rb +67 -0
- data/lib/rcs-common/resolver.rb +40 -0
- data/lib/rcs-common/rest.rb +17 -0
- data/lib/rcs-common/sanitize.rb +42 -0
- data/lib/rcs-common/serializer.rb +404 -0
- data/lib/rcs-common/signature.rb +141 -0
- data/lib/rcs-common/stats.rb +94 -0
- data/lib/rcs-common/symbolize.rb +10 -0
- data/lib/rcs-common/systemstatus.rb +136 -0
- data/lib/rcs-common/temporary.rb +13 -0
- data/lib/rcs-common/time.rb +24 -0
- data/lib/rcs-common/trace.rb +138 -0
- data/lib/rcs-common/trace.yaml +42 -0
- data/lib/rcs-common/updater/client.rb +354 -0
- data/lib/rcs-common/updater/dsl.rb +178 -0
- data/lib/rcs-common/updater/payload.rb +79 -0
- data/lib/rcs-common/updater/server.rb +126 -0
- data/lib/rcs-common/updater/shared_key.rb +55 -0
- data/lib/rcs-common/updater/tmp_dir.rb +13 -0
- data/lib/rcs-common/utf16le.rb +83 -0
- data/lib/rcs-common/version.rb +5 -0
- data/lib/rcs-common/winfirewall.rb +235 -0
- data/rcs-common.gemspec +64 -0
- data/spec/gridfs_spec.rb +637 -0
- data/spec/mongoid.yaml +6 -0
- data/spec/signature_spec.rb +105 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/updater_spec.rb +80 -0
- data/tasks/deploy.rake +21 -0
- data/tasks/protect.rake +90 -0
- data/test/helper.rb +17 -0
- data/test/test_binary.rb +107 -0
- data/test/test_cgi.rb +14 -0
- data/test/test_crypt.rb +125 -0
- data/test/test_evidence.rb +52 -0
- data/test/test_evidence_manager.rb +119 -0
- data/test/test_fixnum.rb +35 -0
- data/test/test_keywords.rb +137 -0
- data/test/test_mime.rb +49 -0
- data/test/test_pascalize.rb +100 -0
- data/test/test_path_utils.rb +24 -0
- data/test/test_rcs-common.rb +7 -0
- data/test/test_sanitize.rb +40 -0
- data/test/test_serialization.rb +20 -0
- data/test/test_stats.rb +90 -0
- data/test/test_symbolize.rb +20 -0
- data/test/test_systemstatus.rb +35 -0
- data/test/test_time.rb +56 -0
- data/test/test_trace.rb +25 -0
- data/test/test_utf16le.rb +71 -0
- data/test/test_winfirewall.rb +68 -0
- metadata +423 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
require 'timeout'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
require 'open3'
|
|
4
|
+
require_relative "tmp_dir"
|
|
5
|
+
require_relative "../trace.rb"
|
|
6
|
+
|
|
7
|
+
module RCS
|
|
8
|
+
module Updater
|
|
9
|
+
class Payload
|
|
10
|
+
include RCS::Tracer
|
|
11
|
+
include TmpDir
|
|
12
|
+
|
|
13
|
+
attr_reader :options, :payload, :timeout
|
|
14
|
+
attr_reader :filepath, :output, :return_code, :stored
|
|
15
|
+
|
|
16
|
+
DEFAULT_TIMEOUT = 600
|
|
17
|
+
|
|
18
|
+
def initialize(payload, options = {})
|
|
19
|
+
@options = options
|
|
20
|
+
@payload = payload
|
|
21
|
+
|
|
22
|
+
@timeout = options['timeout'].to_i
|
|
23
|
+
@timeout = DEFAULT_TIMEOUT if @timeout <= 0
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def ruby?; options['ruby']; end
|
|
27
|
+
|
|
28
|
+
def storable?; options['store']; end
|
|
29
|
+
|
|
30
|
+
def spawn?; options['spawn']; end
|
|
31
|
+
|
|
32
|
+
def runnable?
|
|
33
|
+
options['exec'] or ruby? or spawn?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def [](name)
|
|
37
|
+
instance_variable_get("@#{name}")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def run
|
|
41
|
+
@return_code = nil
|
|
42
|
+
@output = ""
|
|
43
|
+
|
|
44
|
+
cmd = "#{'ruby ' if ruby?}#{storable? ? filepath : payload}"
|
|
45
|
+
|
|
46
|
+
if spawn?
|
|
47
|
+
trace(:debug, "[spawn] #{cmd}")
|
|
48
|
+
return spawn(cmd)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
Timeout::timeout(@timeout) do
|
|
52
|
+
trace(:debug, "Timeout has been set to #{@timeout} sec") if @timeout != DEFAULT_TIMEOUT
|
|
53
|
+
|
|
54
|
+
trace(:debug, "[popen] #{cmd}")
|
|
55
|
+
|
|
56
|
+
Open3.popen2e(cmd) do |stdin, std_out_err, wait_thr|
|
|
57
|
+
while line = std_out_err.gets
|
|
58
|
+
trace(:debug, "[std_out_err] #{line.strip}")
|
|
59
|
+
@output << line
|
|
60
|
+
end
|
|
61
|
+
@return_code = wait_thr.value.exitstatus
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
return @return_code
|
|
66
|
+
ensure
|
|
67
|
+
FileUtils.rm_f(filepath) if stored
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def store
|
|
71
|
+
@filepath = "#{tmpdir}/" + (@options['filename'] || Time.now.to_f.to_s.gsub(".", ""))
|
|
72
|
+
FileUtils.mkdir_p(tmpdir)
|
|
73
|
+
trace(:debug, "Storing payload into #{filepath}")
|
|
74
|
+
File.open(filepath, "wb") { |f| f.write(payload) }
|
|
75
|
+
@stored = true
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
require 'yajl/json_gem'
|
|
2
|
+
require 'em-http-server'
|
|
3
|
+
require 'digest/md5'
|
|
4
|
+
require 'monitor'
|
|
5
|
+
require_relative "payload"
|
|
6
|
+
require_relative "shared_key"
|
|
7
|
+
require_relative "../trace"
|
|
8
|
+
require_relative "../winfirewall"
|
|
9
|
+
|
|
10
|
+
module RCS
|
|
11
|
+
module Updater
|
|
12
|
+
class AuthError < Exception; end
|
|
13
|
+
|
|
14
|
+
class Server < EM::HttpServer::Server
|
|
15
|
+
include MonitorMixin
|
|
16
|
+
include RCS::Tracer
|
|
17
|
+
extend RCS::Tracer
|
|
18
|
+
|
|
19
|
+
def initialize(*args)
|
|
20
|
+
@shared_key = SharedKey.new
|
|
21
|
+
super
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def x_options
|
|
25
|
+
@x_options ||= @shared_key.decrypt_hash(@http[:x_options]) rescue nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def remote_addr
|
|
29
|
+
ary = get_peername[2,6].unpack("nC4")
|
|
30
|
+
ary[1..-1].join(".")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def private_ipv4?
|
|
34
|
+
a,b,c,d = remote_addr.split(".").map(&:to_i)
|
|
35
|
+
return true if a==127 && b==0 && c==0 && d==1 # localhost
|
|
36
|
+
return true if a==192 && b==168 && c.between?(0,255) && d.between?(0,255) # 192.168.0.0/16
|
|
37
|
+
return true if a==172 && b.between?(16,31) && c.between?(0,255) && d.between?(0,255) # 172.16.0.0/12
|
|
38
|
+
return true if a==10 && b.between?(0,255) && c.between?(0,255) && d.between?(0,255) # 10.0.0.0/8
|
|
39
|
+
return false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def process_http_request
|
|
43
|
+
EM.defer do
|
|
44
|
+
begin
|
|
45
|
+
trace(:info, "[#{@http[:host]}] REQ #{@http_protocol} #{@http_request_method} #{@http_content.size} bytes from #{remote_addr}")
|
|
46
|
+
|
|
47
|
+
raise AuthError.new("Invalid http method") if @http_request_method != "POST"
|
|
48
|
+
raise AuthError.new("No content") unless @http_content
|
|
49
|
+
raise AuthError.new("Missing server signature") unless @shared_key.read_key_from_file
|
|
50
|
+
raise AuthError.new("remote_addr is not private") unless private_ipv4?
|
|
51
|
+
raise AuthError.new("Invalid signature") unless x_options
|
|
52
|
+
raise AuthError.new("Payload checksum failed") if x_options['md5'] != Digest::MD5.hexdigest(@http_content)
|
|
53
|
+
|
|
54
|
+
synchronize do
|
|
55
|
+
@@x_options_last_tm ||= nil
|
|
56
|
+
raise AuthError.new("Reply attack") if @@x_options_last_tm and x_options['tm'] <= @@x_options_last_tm
|
|
57
|
+
@@x_options_last_tm = x_options['tm']
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
payload = Payload.new(@http_content, x_options)
|
|
61
|
+
|
|
62
|
+
set_comm_inactivity_timeout(payload.timeout + 30)
|
|
63
|
+
|
|
64
|
+
payload.store if payload.storable?
|
|
65
|
+
payload.run if payload.runnable?
|
|
66
|
+
|
|
67
|
+
send_response(200, payload_to_hash(payload))
|
|
68
|
+
rescue AuthError => ex
|
|
69
|
+
print_exception(ex, backtrace: false)
|
|
70
|
+
close_connection
|
|
71
|
+
rescue Exception => ex
|
|
72
|
+
print_exception(ex)
|
|
73
|
+
send_response(500, payload_to_hash(payload))
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def payload_to_hash(payload)
|
|
79
|
+
{path: payload.filepath, output: payload.output, return_code: payload.return_code, stored: payload.stored} if payload
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def http_request_errback(ex)
|
|
83
|
+
print_exception(ex)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def print_exception(ex, backtrace: true)
|
|
87
|
+
text = "[#{ex.class}] #{ex.message}"
|
|
88
|
+
text << "\n\t#{ex.backtrace.join("\n\t")}" if ex.backtrace and backtrace
|
|
89
|
+
trace(:error, text)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def send_response(status_code, content = nil)
|
|
93
|
+
response = EM::DelegatedHttpResponse.new(self)
|
|
94
|
+
response.status = status_code
|
|
95
|
+
response.content_type('application/json')
|
|
96
|
+
response.content = content.to_json if content
|
|
97
|
+
response.send_response
|
|
98
|
+
trace(:info, "[#{@http[:host]}] REP #{status_code} #{response.content.size} bytes")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def self.add_firewall_rule(port)
|
|
102
|
+
if WinFirewall.exists?
|
|
103
|
+
rule_name = "RCS_FWD Updater"
|
|
104
|
+
WinFirewall.del_rule(rule_name)
|
|
105
|
+
WinFirewall.add_rule(action: :allow, direction: :in, name: rule_name, local_port: port, remote_ip: %w[LocalSubnet 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16], protocol: :tcp)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def self.start(port: 6677, address: "0.0.0.0")
|
|
110
|
+
EM::run do
|
|
111
|
+
trace_setup rescue $stderr.puts("trace_setup failed - logging only to stdout")
|
|
112
|
+
add_firewall_rule(port)
|
|
113
|
+
|
|
114
|
+
trace(:info, "Starting RCS Updater server on #{address}:#{port}")
|
|
115
|
+
EM::start_server(address, port, self)
|
|
116
|
+
end
|
|
117
|
+
rescue Interrupt
|
|
118
|
+
trace(:fatal, "Interrupted by the user")
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
if __FILE__ == $0
|
|
125
|
+
RCS::Updater::Server.start
|
|
126
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require 'openssl'
|
|
2
|
+
require 'base64'
|
|
3
|
+
|
|
4
|
+
module RCS
|
|
5
|
+
module Updater
|
|
6
|
+
class SharedKey
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
# Search for the signature file in some places
|
|
10
|
+
[File.expand_path(Dir.pwd), "C:/RCS/DB", "C:/RCS/Collector"].each do |root|
|
|
11
|
+
["#{root}/config/rcs-updater.sig", "#{root}/config/certs/rcs-updater.sig"].each do |path|
|
|
12
|
+
@path = path if File.exists?(path)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def read_key_from_file
|
|
18
|
+
return ENV['SIGNATURE'] if ENV['SIGNATURE']
|
|
19
|
+
|
|
20
|
+
if @path
|
|
21
|
+
key = File.read(@path)
|
|
22
|
+
return key.empty? ? nil : key
|
|
23
|
+
else
|
|
24
|
+
return nil
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def prepare_cipher(mode)
|
|
29
|
+
@cipher = OpenSSL::Cipher::AES.new(256, :CBC)
|
|
30
|
+
mode == :encrypt ? @cipher.encrypt : @cipher.decrypt
|
|
31
|
+
@cipher.padding = 1
|
|
32
|
+
@cipher.key = read_key_from_file || raise("Missing or empty signature file")
|
|
33
|
+
@cipher.iv = "\xBA\xF0\xC0Z\xD7\xE8~[TP\xFE\x88rW\xC8\xF4"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def encrypt(data)
|
|
37
|
+
prepare_cipher(:encrypt)
|
|
38
|
+
return @cipher.update(data) + @cipher.final
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def decrypt(data)
|
|
42
|
+
prepare_cipher(:decrypt)
|
|
43
|
+
return @cipher.update(data) + @cipher.final
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def encrypt_hash(hash)
|
|
47
|
+
Base64.urlsafe_encode64(encrypt(hash.to_json))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def decrypt_hash(data)
|
|
51
|
+
JSON.parse(decrypt(Base64.urlsafe_decode64(data)))
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Helper method for decoding windows WCHAR strings.
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
require 'stringio'
|
|
6
|
+
|
|
7
|
+
class StringIO
|
|
8
|
+
def read_utf16le_string
|
|
9
|
+
# at least the null terminator
|
|
10
|
+
return '' if self.size < 2
|
|
11
|
+
|
|
12
|
+
# empty string by default
|
|
13
|
+
str = ''
|
|
14
|
+
# read until the end of buffer or null termination
|
|
15
|
+
until self.eof? do
|
|
16
|
+
t = self.read(2)
|
|
17
|
+
break if t == "\0\0"
|
|
18
|
+
str << t
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# misaligned string
|
|
22
|
+
return '' if str.bytesize % 2 != 0
|
|
23
|
+
|
|
24
|
+
return str
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def read_ascii_string
|
|
28
|
+
# at least the null terminator
|
|
29
|
+
return '' if self.size < 1
|
|
30
|
+
|
|
31
|
+
# empty string by default
|
|
32
|
+
str = ''
|
|
33
|
+
# read until the end of buffer or null termination
|
|
34
|
+
until self.tell == self.size do
|
|
35
|
+
t = self.read(1)
|
|
36
|
+
break if t == "\0"
|
|
37
|
+
str << t
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
return str
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class String
|
|
46
|
+
def to_binary
|
|
47
|
+
self.unpack("H*").pack("H*")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_utf16le_binary
|
|
51
|
+
self.encode('UTF-16LE').to_binary
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def to_utf16le_binary_null
|
|
55
|
+
# with null termination
|
|
56
|
+
(self + "\0").to_utf16le_binary
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def terminate_utf16le
|
|
60
|
+
self.force_encoding('UTF-16LE') + "\0".encode('UTF-16LE')
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def to_utf16le
|
|
64
|
+
self.encode('UTF-16LE')
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def utf16le_to_utf8
|
|
68
|
+
self.force_encoding('UTF-16LE').encode('UTF-8').chomp("\0")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def safe_utf8_encode_invalid
|
|
72
|
+
return self if self.encoding == Encoding::UTF_8 and self.valid_encoding?
|
|
73
|
+
self.safe_utf8_encode
|
|
74
|
+
return self if self.valid_encoding?
|
|
75
|
+
self.force_encoding('BINARY')
|
|
76
|
+
self.encode! 'BINARY', 'UTF-8', invalid: :replace, undef: :replace, replace: '?'
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def safe_utf8_encode
|
|
80
|
+
self.force_encoding('UTF-8')
|
|
81
|
+
self.encode! 'UTF-8', 'UTF-8', invalid: :replace, undef: :replace, replace: ''
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
require 'resolv'
|
|
2
|
+
require 'timeout'
|
|
3
|
+
require_relative 'trace'
|
|
4
|
+
require_relative 'resolver'
|
|
5
|
+
|
|
6
|
+
module RCS
|
|
7
|
+
module Common
|
|
8
|
+
module WinFirewall
|
|
9
|
+
extend RCS::Tracer
|
|
10
|
+
|
|
11
|
+
# Represent a Windows Firewall rule.
|
|
12
|
+
class Rule
|
|
13
|
+
include Resolver
|
|
14
|
+
|
|
15
|
+
ATTRIBUTES = %i[direction action local_ip remote_ip local_port remote_port name protocol profiles enabled grouping edge_traversal]
|
|
16
|
+
|
|
17
|
+
RULE_GROUP = 'RCS Firewall Rules'
|
|
18
|
+
|
|
19
|
+
attr_reader :attributes
|
|
20
|
+
|
|
21
|
+
def initialize(attributes = {})
|
|
22
|
+
# Default attribute values
|
|
23
|
+
@attributes = {
|
|
24
|
+
grouping: RULE_GROUP
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Merge default attributes with the given ones
|
|
28
|
+
# and remove invalid attributes
|
|
29
|
+
attributes.symbolize_keys! if attributes.respond_to?(:symbolize_keys!)
|
|
30
|
+
attributes.reject! { |key| !ATTRIBUTES.include?(key) }
|
|
31
|
+
@attributes.merge!(attributes)
|
|
32
|
+
|
|
33
|
+
# Define getters and setters
|
|
34
|
+
ATTRIBUTES.each do |name|
|
|
35
|
+
define_singleton_method(name) { @attributes[name] }
|
|
36
|
+
define_singleton_method("#{name}=") { |value| @attributes[name] = value }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def resolve_addresses!
|
|
41
|
+
resolve_addresses(true)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def resolve_addresses(_raise = false)
|
|
45
|
+
return if @addresses_resolved
|
|
46
|
+
|
|
47
|
+
%i[remote_ip local_ip].each do |name|
|
|
48
|
+
next unless @attributes[name]
|
|
49
|
+
|
|
50
|
+
addresses = [@attributes[name]].flatten
|
|
51
|
+
|
|
52
|
+
addresses.each_with_index do |address, index|
|
|
53
|
+
next if %w[any localsubnet dns dhcp wins defaultgateway].include?(address.to_s.downcase)
|
|
54
|
+
next if address.to_s =~ Resolv::IPv4::Regex
|
|
55
|
+
next if address.to_s =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)\/(\d+)/
|
|
56
|
+
|
|
57
|
+
is_localhost = Socket.gethostname.casecmp(address).zero?
|
|
58
|
+
|
|
59
|
+
addresses[index] = if is_localhost
|
|
60
|
+
'127.0.0.1'
|
|
61
|
+
elsif _raise
|
|
62
|
+
resolve_dns(address)
|
|
63
|
+
else
|
|
64
|
+
resolve_dns(address) rescue address
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
@attributes[name] = addresses.size == 1 ? addresses[0] : addresses
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
@addresses_resolved = true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def save
|
|
75
|
+
resolve_addresses!
|
|
76
|
+
|
|
77
|
+
if Advfirewall.call("firewall add rule #{stringify_attributes}").ok?
|
|
78
|
+
true
|
|
79
|
+
else
|
|
80
|
+
raise "Unable to save firewall rule #{@attributes[:name]}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def del
|
|
85
|
+
resolve_addresses
|
|
86
|
+
|
|
87
|
+
only = %i[dir profile program service localip remoteip localport remoteport protocol name]
|
|
88
|
+
|
|
89
|
+
Advfirewall.call("firewall delete rule #{stringify_attributes(only)}")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def stringify_attributes(only = [])
|
|
95
|
+
attrs = {
|
|
96
|
+
name: name,
|
|
97
|
+
dir: direction,
|
|
98
|
+
action: action,
|
|
99
|
+
enable: enabled,
|
|
100
|
+
protocol: protocol,
|
|
101
|
+
profile: profiles,
|
|
102
|
+
remoteip: remote_ip,
|
|
103
|
+
localip: local_ip,
|
|
104
|
+
localport: local_port,
|
|
105
|
+
remoteport: remote_port,
|
|
106
|
+
#group: grouping / why isn't working?
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
string = ""
|
|
110
|
+
|
|
111
|
+
attrs.each do |key, value|
|
|
112
|
+
next if only.any? and !only.include?(key)
|
|
113
|
+
next if value.to_s.strip.empty?
|
|
114
|
+
next if value == :any
|
|
115
|
+
value = value.respond_to?(:join) ? value.map(&:to_s).join(',') : "\"#{value}\""
|
|
116
|
+
string << "#{key}=#{value} "
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
string
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# Parse the response of the netsh advfirewall command
|
|
125
|
+
class AdvfirewallResponse < String
|
|
126
|
+
SEPARATOR = '-'*70
|
|
127
|
+
|
|
128
|
+
attr_accessor :ok
|
|
129
|
+
|
|
130
|
+
def ok?
|
|
131
|
+
return self.ok unless self.ok.nil?
|
|
132
|
+
self.strip =~ /OK\.\z/i
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def has_separator?
|
|
136
|
+
self.include?(SEPARATOR)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def first_line
|
|
140
|
+
index = nil
|
|
141
|
+
self.lines.each_with_index{ |line, i| index = i if line.include?(SEPARATOR) }
|
|
142
|
+
self.lines[index+1].strip if index
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class Advfirewall
|
|
148
|
+
extend RCS::Tracer
|
|
149
|
+
|
|
150
|
+
# Return true if the current os is Windows
|
|
151
|
+
def self.exists?
|
|
152
|
+
@firewall_exists ||= (RbConfig::CONFIG['host_os'] =~ /mingw/i)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def self.call(command, read: false)
|
|
156
|
+
command = "netsh advfirewall #{command.strip}"
|
|
157
|
+
|
|
158
|
+
unless exists?
|
|
159
|
+
raise "The Windows Firewall is missing. You cannot call the command #{command.inspect} on this OS."
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
#trace(:debug, "[Advfirewall] #{command}")
|
|
163
|
+
|
|
164
|
+
if read
|
|
165
|
+
resp = AdvfirewallResponse.new(`#{command}`)
|
|
166
|
+
trace(:debug, "[Advfirewall] #{resp}") unless resp.ok?
|
|
167
|
+
resp
|
|
168
|
+
else
|
|
169
|
+
resp = AdvfirewallResponse.new
|
|
170
|
+
resp.ok = system(command)
|
|
171
|
+
resp
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
extend self
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# Return :on or :off depending of the firewall state
|
|
181
|
+
#
|
|
182
|
+
# Note that the files test/fixtures/advfirewall/show_currentprofile_state_on and
|
|
183
|
+
# test/fixtures/advfirewall/show_currentprofile_state_off contains an example of the command output
|
|
184
|
+
def status
|
|
185
|
+
return status_from_registry if @use_registry_for_status
|
|
186
|
+
|
|
187
|
+
first_line = Advfirewall.call("show currentprofile state", read: true).first_line
|
|
188
|
+
|
|
189
|
+
if first_line =~ /ON\z/
|
|
190
|
+
:on
|
|
191
|
+
elsif first_line =~ /OFF\z/
|
|
192
|
+
:off
|
|
193
|
+
else
|
|
194
|
+
@use_registry_for_status = true
|
|
195
|
+
status_from_registry
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def status_from_registry
|
|
200
|
+
command = 'reg query HKLM\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\StandardProfile /v EnableFirewall'
|
|
201
|
+
trace(:debug, "[Advfirewall] #{command}")
|
|
202
|
+
`#{command}`.include?('0x1') ? :on : :off
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Returns true if the default firewall policy is to block all inbound connections
|
|
206
|
+
def block_inbound?
|
|
207
|
+
line = Advfirewall.call("show currentprofile firewallpolicy", read: true).first_line
|
|
208
|
+
line.to_s.downcase.include?('blockinbound')
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Delegate
|
|
212
|
+
def exists?
|
|
213
|
+
Advfirewall.exists?
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def add_rule(attributes)
|
|
217
|
+
Rule.new(attributes).save
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def del_rule(name)
|
|
221
|
+
Rule.new(name: name.to_s).del
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def has_rule?(name)
|
|
225
|
+
Advfirewall.call("firewall show rule name=\"#{name}\"").ok?
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def raw_rules
|
|
229
|
+
Advfirewall.call("firewall show rule name=all", read: true)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
WinFirewall = RCS::Common::WinFirewall
|