immunio 0.15.2
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/LICENSE +234 -0
- data/README.md +147 -0
- data/bin/immunio +5 -0
- data/lib/immunio.rb +29 -0
- data/lib/immunio/agent.rb +260 -0
- data/lib/immunio/authentication.rb +96 -0
- data/lib/immunio/blocked_app.rb +38 -0
- data/lib/immunio/channel.rb +432 -0
- data/lib/immunio/cli.rb +39 -0
- data/lib/immunio/context.rb +114 -0
- data/lib/immunio/errors.rb +43 -0
- data/lib/immunio/immunio_ca.crt +45 -0
- data/lib/immunio/logger.rb +87 -0
- data/lib/immunio/plugins/action_dispatch.rb +45 -0
- data/lib/immunio/plugins/action_view.rb +431 -0
- data/lib/immunio/plugins/active_record.rb +707 -0
- data/lib/immunio/plugins/active_record_relation.rb +370 -0
- data/lib/immunio/plugins/authlogic.rb +80 -0
- data/lib/immunio/plugins/csrf.rb +24 -0
- data/lib/immunio/plugins/devise.rb +40 -0
- data/lib/immunio/plugins/environment_reporter.rb +69 -0
- data/lib/immunio/plugins/eval.rb +51 -0
- data/lib/immunio/plugins/exception_handler.rb +55 -0
- data/lib/immunio/plugins/gems_tracker.rb +5 -0
- data/lib/immunio/plugins/haml.rb +36 -0
- data/lib/immunio/plugins/http_finisher.rb +50 -0
- data/lib/immunio/plugins/http_tracker.rb +203 -0
- data/lib/immunio/plugins/io.rb +96 -0
- data/lib/immunio/plugins/redirect.rb +42 -0
- data/lib/immunio/plugins/warden.rb +66 -0
- data/lib/immunio/processor.rb +234 -0
- data/lib/immunio/rails.rb +26 -0
- data/lib/immunio/request.rb +139 -0
- data/lib/immunio/rufus_lua_ext/ref.rb +27 -0
- data/lib/immunio/rufus_lua_ext/state.rb +157 -0
- data/lib/immunio/rufus_lua_ext/table.rb +137 -0
- data/lib/immunio/rufus_lua_ext/utils.rb +13 -0
- data/lib/immunio/version.rb +5 -0
- data/lib/immunio/vm.rb +291 -0
- data/lua-hooks/ext/all.c +78 -0
- data/lua-hooks/ext/bitop/README +22 -0
- data/lua-hooks/ext/bitop/bit.c +189 -0
- data/lua-hooks/ext/extconf.rb +38 -0
- data/lua-hooks/ext/libinjection/COPYING +37 -0
- data/lua-hooks/ext/libinjection/libinjection.h +65 -0
- data/lua-hooks/ext/libinjection/libinjection_html5.c +847 -0
- data/lua-hooks/ext/libinjection/libinjection_html5.h +54 -0
- data/lua-hooks/ext/libinjection/libinjection_sqli.c +2301 -0
- data/lua-hooks/ext/libinjection/libinjection_sqli.h +295 -0
- data/lua-hooks/ext/libinjection/libinjection_sqli_data.h +9349 -0
- data/lua-hooks/ext/libinjection/libinjection_xss.c +531 -0
- data/lua-hooks/ext/libinjection/libinjection_xss.h +21 -0
- data/lua-hooks/ext/libinjection/lualib.c +109 -0
- data/lua-hooks/ext/lpeg/HISTORY +90 -0
- data/lua-hooks/ext/lpeg/lpcap.c +537 -0
- data/lua-hooks/ext/lpeg/lpcap.h +43 -0
- data/lua-hooks/ext/lpeg/lpcode.c +986 -0
- data/lua-hooks/ext/lpeg/lpcode.h +34 -0
- data/lua-hooks/ext/lpeg/lpeg-128.gif +0 -0
- data/lua-hooks/ext/lpeg/lpeg.html +1429 -0
- data/lua-hooks/ext/lpeg/lpprint.c +244 -0
- data/lua-hooks/ext/lpeg/lpprint.h +35 -0
- data/lua-hooks/ext/lpeg/lptree.c +1238 -0
- data/lua-hooks/ext/lpeg/lptree.h +77 -0
- data/lua-hooks/ext/lpeg/lptypes.h +149 -0
- data/lua-hooks/ext/lpeg/lpvm.c +355 -0
- data/lua-hooks/ext/lpeg/lpvm.h +58 -0
- data/lua-hooks/ext/lpeg/makefile +55 -0
- data/lua-hooks/ext/lpeg/re.html +498 -0
- data/lua-hooks/ext/lpeg/test.lua +1409 -0
- data/lua-hooks/ext/lua-cmsgpack/CMakeLists.txt +45 -0
- data/lua-hooks/ext/lua-cmsgpack/README.md +115 -0
- data/lua-hooks/ext/lua-cmsgpack/lua_cmsgpack.c +957 -0
- data/lua-hooks/ext/lua-cmsgpack/test.lua +570 -0
- data/lua-hooks/ext/lua-snapshot/LICENSE +7 -0
- data/lua-hooks/ext/lua-snapshot/Makefile +12 -0
- data/lua-hooks/ext/lua-snapshot/README.md +18 -0
- data/lua-hooks/ext/lua-snapshot/dump.lua +15 -0
- data/lua-hooks/ext/lua-snapshot/snapshot.c +455 -0
- data/lua-hooks/ext/lua/COPYRIGHT +34 -0
- data/lua-hooks/ext/lua/lapi.c +1087 -0
- data/lua-hooks/ext/lua/lapi.h +16 -0
- data/lua-hooks/ext/lua/lauxlib.c +652 -0
- data/lua-hooks/ext/lua/lauxlib.h +174 -0
- data/lua-hooks/ext/lua/lbaselib.c +659 -0
- data/lua-hooks/ext/lua/lcode.c +831 -0
- data/lua-hooks/ext/lua/lcode.h +76 -0
- data/lua-hooks/ext/lua/ldblib.c +398 -0
- data/lua-hooks/ext/lua/ldebug.c +638 -0
- data/lua-hooks/ext/lua/ldebug.h +33 -0
- data/lua-hooks/ext/lua/ldo.c +519 -0
- data/lua-hooks/ext/lua/ldo.h +57 -0
- data/lua-hooks/ext/lua/ldump.c +164 -0
- data/lua-hooks/ext/lua/lfunc.c +174 -0
- data/lua-hooks/ext/lua/lfunc.h +34 -0
- data/lua-hooks/ext/lua/lgc.c +710 -0
- data/lua-hooks/ext/lua/lgc.h +110 -0
- data/lua-hooks/ext/lua/linit.c +38 -0
- data/lua-hooks/ext/lua/liolib.c +556 -0
- data/lua-hooks/ext/lua/llex.c +463 -0
- data/lua-hooks/ext/lua/llex.h +81 -0
- data/lua-hooks/ext/lua/llimits.h +128 -0
- data/lua-hooks/ext/lua/lmathlib.c +263 -0
- data/lua-hooks/ext/lua/lmem.c +86 -0
- data/lua-hooks/ext/lua/lmem.h +49 -0
- data/lua-hooks/ext/lua/loadlib.c +705 -0
- data/lua-hooks/ext/lua/loadlib_rel.c +760 -0
- data/lua-hooks/ext/lua/lobject.c +214 -0
- data/lua-hooks/ext/lua/lobject.h +381 -0
- data/lua-hooks/ext/lua/lopcodes.c +102 -0
- data/lua-hooks/ext/lua/lopcodes.h +268 -0
- data/lua-hooks/ext/lua/loslib.c +243 -0
- data/lua-hooks/ext/lua/lparser.c +1339 -0
- data/lua-hooks/ext/lua/lparser.h +82 -0
- data/lua-hooks/ext/lua/lstate.c +214 -0
- data/lua-hooks/ext/lua/lstate.h +169 -0
- data/lua-hooks/ext/lua/lstring.c +111 -0
- data/lua-hooks/ext/lua/lstring.h +31 -0
- data/lua-hooks/ext/lua/lstrlib.c +871 -0
- data/lua-hooks/ext/lua/ltable.c +588 -0
- data/lua-hooks/ext/lua/ltable.h +40 -0
- data/lua-hooks/ext/lua/ltablib.c +287 -0
- data/lua-hooks/ext/lua/ltm.c +75 -0
- data/lua-hooks/ext/lua/ltm.h +54 -0
- data/lua-hooks/ext/lua/lua.c +392 -0
- data/lua-hooks/ext/lua/lua.def +131 -0
- data/lua-hooks/ext/lua/lua.h +388 -0
- data/lua-hooks/ext/lua/lua.rc +28 -0
- data/lua-hooks/ext/lua/lua_dll.rc +26 -0
- data/lua-hooks/ext/lua/luac.c +200 -0
- data/lua-hooks/ext/lua/luac.rc +1 -0
- data/lua-hooks/ext/lua/luaconf.h +763 -0
- data/lua-hooks/ext/lua/luaconf.h.in +724 -0
- data/lua-hooks/ext/lua/luaconf.h.orig +763 -0
- data/lua-hooks/ext/lua/lualib.h +53 -0
- data/lua-hooks/ext/lua/lundump.c +227 -0
- data/lua-hooks/ext/lua/lundump.h +36 -0
- data/lua-hooks/ext/lua/lvm.c +767 -0
- data/lua-hooks/ext/lua/lvm.h +36 -0
- data/lua-hooks/ext/lua/lzio.c +82 -0
- data/lua-hooks/ext/lua/lzio.h +67 -0
- data/lua-hooks/ext/lua/print.c +227 -0
- data/lua-hooks/ext/luautf8/README.md +152 -0
- data/lua-hooks/ext/luautf8/lutf8lib.c +1274 -0
- data/lua-hooks/ext/luautf8/unidata.h +3064 -0
- data/lua-hooks/lib/boot.lua +254 -0
- data/lua-hooks/lib/encode.lua +4 -0
- data/lua-hooks/lib/lexers/LICENSE +21 -0
- data/lua-hooks/lib/lexers/bash.lua +134 -0
- data/lua-hooks/lib/lexers/bash_dqstr.lua +62 -0
- data/lua-hooks/lib/lexers/css.lua +216 -0
- data/lua-hooks/lib/lexers/html.lua +106 -0
- data/lua-hooks/lib/lexers/javascript.lua +68 -0
- data/lua-hooks/lib/lexers/lexer.lua +1575 -0
- data/lua-hooks/lib/lexers/markers.lua +33 -0
- metadata +308 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
module Immunio
|
|
2
|
+
# API for triggering the Immunio authentication hooks.
|
|
3
|
+
module Authentication
|
|
4
|
+
# Each method below takes an options hash argument. The options hash may
|
|
5
|
+
# contain any of the following information:
|
|
6
|
+
#
|
|
7
|
+
# * user_id: String or Number
|
|
8
|
+
# * username: String
|
|
9
|
+
# * email: String
|
|
10
|
+
# * user_record: ActiveRecord object for the user
|
|
11
|
+
# * reason: String (for failures)
|
|
12
|
+
|
|
13
|
+
# Call after a successful login.
|
|
14
|
+
def login(options_ro={})
|
|
15
|
+
plugin, options = parse_opts(options_ro)
|
|
16
|
+
return unless plugin
|
|
17
|
+
Immunio.run_hook! plugin, "authenticate", options.merge({is_valid: true})
|
|
18
|
+
Immunio.run_hook! plugin, "framework_login", options
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Call after an unsuccessful login.
|
|
22
|
+
def failed_login(options_ro={})
|
|
23
|
+
plugin, options = parse_opts(options_ro)
|
|
24
|
+
return unless plugin
|
|
25
|
+
Immunio.run_hook! plugin, "authenticate", options.merge({is_valid: false})
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Call after a user logs out.
|
|
29
|
+
def logout(options_ro={})
|
|
30
|
+
plugin, options = parse_opts(options_ro)
|
|
31
|
+
return unless plugin
|
|
32
|
+
Immunio.run_hook! plugin, "framework_logout", options
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Call after the current user is changed.
|
|
36
|
+
def set_user(options_ro={})
|
|
37
|
+
plugin, options = parse_opts(options_ro)
|
|
38
|
+
return unless plugin
|
|
39
|
+
Immunio.run_hook! plugin, "framework_user", options
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Call after a successful password reset has been requested for a user.
|
|
43
|
+
def password_reset(options_ro={})
|
|
44
|
+
plugin, options = parse_opts(options_ro)
|
|
45
|
+
return unless plugin
|
|
46
|
+
options[:is_valid] = true
|
|
47
|
+
Immunio.run_hook! plugin, "framework_password_reset", options
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Call after a failed password reset has been requested.
|
|
51
|
+
def failed_password_reset(options_ro={})
|
|
52
|
+
plugin, options = parse_opts(options_ro)
|
|
53
|
+
return unless plugin
|
|
54
|
+
options[:is_valid] = false
|
|
55
|
+
Immunio.run_hook! plugin, "framework_password_reset", options
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
def parse_opts(options_ro)
|
|
60
|
+
unless options_ro.is_a? Hash
|
|
61
|
+
Immunio.logger.warn "Passed a non-hash options object into an authentication method: #{options_ro.inspect}"
|
|
62
|
+
return
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
Immunio.logger.debug {"Authentication API called with options #{options_ro}"}
|
|
66
|
+
|
|
67
|
+
options = options_ro.clone
|
|
68
|
+
plugin = options.delete(:plugin) { "api" }
|
|
69
|
+
options[:user_id] = options[:user_id].to_s if options[:user_id]
|
|
70
|
+
|
|
71
|
+
user_record = options.delete :user_record
|
|
72
|
+
if user_record
|
|
73
|
+
# Prefer user info passed in over those parsed from user_record
|
|
74
|
+
record_info = parse_record(user_record)
|
|
75
|
+
options = record_info.merge(options)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
return plugin, options
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def parse_record(record)
|
|
82
|
+
info = {}
|
|
83
|
+
|
|
84
|
+
info[:user_id] = record.id.to_s if record.respond_to?(:id)
|
|
85
|
+
info[:email] = record.email.to_s if record.respond_to?(:email)
|
|
86
|
+
|
|
87
|
+
attribute = [:username, :login, :name].detect { |attr| record.respond_to?(attr) }
|
|
88
|
+
info[:username] = record.send(attribute).to_s if attribute
|
|
89
|
+
|
|
90
|
+
info
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Make all module methods accessible from `Immunio` module. Eg.: `Immunio.login`
|
|
95
|
+
extend Authentication
|
|
96
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Immunio
|
|
2
|
+
# Rack app called when a request is blocked.
|
|
3
|
+
# Change this to customize the response sent on blocked requests.
|
|
4
|
+
# Defaults to a plain text 403 response.
|
|
5
|
+
mattr_accessor :blocked_app
|
|
6
|
+
mattr_accessor :override_response
|
|
7
|
+
|
|
8
|
+
self.blocked_app = ->(_env) do
|
|
9
|
+
[
|
|
10
|
+
403,
|
|
11
|
+
{ 'Content-Type' => 'text/plain' },
|
|
12
|
+
["Request blocked"]
|
|
13
|
+
]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
self.override_response = ->(_env, override) do
|
|
17
|
+
[
|
|
18
|
+
override.status,
|
|
19
|
+
list_to_headers(override.headers.to_a),
|
|
20
|
+
[override.body]
|
|
21
|
+
]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.list_to_headers(list)
|
|
25
|
+
new_headers = {}
|
|
26
|
+
list.each do |name, value|
|
|
27
|
+
# If this header is already in `new_headers`, append to the
|
|
28
|
+
# existing value with a linefeed separator.
|
|
29
|
+
if new_headers.has_key?(name)
|
|
30
|
+
new_headers[name] += ("\n" + value)
|
|
31
|
+
else
|
|
32
|
+
new_headers[name] = value
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
new_headers
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
end
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
require "thread"
|
|
2
|
+
require "faraday"
|
|
3
|
+
require "faraday_middleware"
|
|
4
|
+
require "msgpack"
|
|
5
|
+
require "base64"
|
|
6
|
+
|
|
7
|
+
require_relative "errors"
|
|
8
|
+
require_relative "logger"
|
|
9
|
+
require_relative "version"
|
|
10
|
+
|
|
11
|
+
module Immunio
|
|
12
|
+
# Communication channel with the Immunio webservice.
|
|
13
|
+
class Channel
|
|
14
|
+
DIGEST = OpenSSL::Digest.new('sha1')
|
|
15
|
+
|
|
16
|
+
attr_reader :success_count, :error_count, :message_queue
|
|
17
|
+
attr_reader :rejected_message_count
|
|
18
|
+
|
|
19
|
+
def initialize(config)
|
|
20
|
+
@config = config
|
|
21
|
+
|
|
22
|
+
@agent_uuid = nil
|
|
23
|
+
|
|
24
|
+
# Messages waiting to be sent.
|
|
25
|
+
@message_queue = Queue.new
|
|
26
|
+
# Messages that were sent but failed. Need to be resent.
|
|
27
|
+
@send_buffer = []
|
|
28
|
+
@send_buffer_bytes = 0
|
|
29
|
+
@last_report_time = 0
|
|
30
|
+
|
|
31
|
+
# A large message we may have popped from the queue but couldn't fit
|
|
32
|
+
# in the current report
|
|
33
|
+
@next_message = nil
|
|
34
|
+
|
|
35
|
+
@send_seq = 0
|
|
36
|
+
@dropped_message_count = 0
|
|
37
|
+
@rejected_message_count = 0
|
|
38
|
+
@success_count = 0
|
|
39
|
+
@error_count = 0
|
|
40
|
+
@quick_connect = true
|
|
41
|
+
|
|
42
|
+
@started = false
|
|
43
|
+
@ready = false
|
|
44
|
+
|
|
45
|
+
@callbacks = []
|
|
46
|
+
|
|
47
|
+
# Anything looking to add to the messages sent to the server:
|
|
48
|
+
@senders = []
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def ready?
|
|
52
|
+
@ready
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def set_ready
|
|
56
|
+
@ready = true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def started?
|
|
60
|
+
@started
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def messages_count
|
|
64
|
+
@message_queue.size
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def start
|
|
68
|
+
return if @started
|
|
69
|
+
|
|
70
|
+
@started = true
|
|
71
|
+
@thread = Thread.new { run }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Stop and wait for the last messages to be sent.
|
|
75
|
+
def stop
|
|
76
|
+
return unless @started
|
|
77
|
+
|
|
78
|
+
Immunio.logger.debug "Stopping channel"
|
|
79
|
+
|
|
80
|
+
@started = false
|
|
81
|
+
@ready = false
|
|
82
|
+
|
|
83
|
+
if @thread
|
|
84
|
+
@thread.kill
|
|
85
|
+
@thread.join
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def send_message(message)
|
|
90
|
+
send_encoded_message message.to_msgpack
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def send_encoded_message(message)
|
|
94
|
+
if @message_queue.size > @config.max_send_queue_size
|
|
95
|
+
Immunio.logger.warn "Dropping message for agent manager due to queue overflow (#{@message_queue.size} > #{@config.max_send_queue_size})"
|
|
96
|
+
Immunio.logger.debug "Dropped message: (#{message})"
|
|
97
|
+
# No room for this message on the queue. Discard.
|
|
98
|
+
@dropped_message_count += 1
|
|
99
|
+
return
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
Immunio.logger.debug {"Sending message to backend: #{MessagePack.unpack(message)}"}
|
|
103
|
+
|
|
104
|
+
@message_queue << message
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def on_message(&block)
|
|
108
|
+
@callbacks << block
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def on_sending(&block)
|
|
112
|
+
@senders << block
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Wait until we receive a message from the agentmanager.
|
|
116
|
+
# This is used primarily for internal testing to wait until all the hooks
|
|
117
|
+
# are loaded.
|
|
118
|
+
def wait_until_ready!
|
|
119
|
+
return if @ready
|
|
120
|
+
|
|
121
|
+
if @config.ready_timeout.to_i <= 0
|
|
122
|
+
return
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
Immunio.logger.debug "Channel waiting #{@config.ready_timeout.to_i} seconds until ready..."
|
|
126
|
+
Timeout.timeout @config.ready_timeout.to_i do
|
|
127
|
+
# Wait until we get a response from the agentmanager
|
|
128
|
+
sleep 0.1 until ready?
|
|
129
|
+
Immunio.logger.debug "Channel ready!"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
protected
|
|
134
|
+
def setup_connection(faraday)
|
|
135
|
+
# Override to modify Faraday options & middlewares
|
|
136
|
+
faraday.adapter Faraday.default_adapter
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
# Core method running in a thread
|
|
141
|
+
def run
|
|
142
|
+
Immunio.logger.debug "Starting channel on thread #{Thread.current.object_id}"
|
|
143
|
+
# Create an empty cert_store to prevent Faraday from using the system default OpenSSL store.
|
|
144
|
+
cert_store = OpenSSL::X509::Store.new
|
|
145
|
+
# Setup the connection for making requests to the server.
|
|
146
|
+
@connection = Faraday::Connection.new(@config.hello_url, ssl: { ca_file: "#{Immunio::DIR}/immunio/immunio_ca.crt", cert_store: cert_store },
|
|
147
|
+
request: { timeout: @config.http_timeout }) do |faraday|
|
|
148
|
+
faraday.request :url_encoded
|
|
149
|
+
|
|
150
|
+
# Provide a hook for additional Faraday config.
|
|
151
|
+
setup_connection faraday
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Get the polling URL from the server.
|
|
155
|
+
hello
|
|
156
|
+
|
|
157
|
+
# Start sending messages
|
|
158
|
+
while @started
|
|
159
|
+
poll
|
|
160
|
+
@success_count += 1
|
|
161
|
+
@error_count = 0
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
rescue StandardError => e
|
|
165
|
+
# Retry forever on error
|
|
166
|
+
@error_count += 1
|
|
167
|
+
|
|
168
|
+
log_error(e)
|
|
169
|
+
|
|
170
|
+
@success_count = 0
|
|
171
|
+
@quick_connect = true
|
|
172
|
+
|
|
173
|
+
exponential_backoff
|
|
174
|
+
|
|
175
|
+
retry
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def log_error(e)
|
|
179
|
+
if @error_count == 1
|
|
180
|
+
Immunio.logger.warn "Connection failed after #{@success_count} successes: #{e} (#{e.class})"
|
|
181
|
+
else
|
|
182
|
+
Immunio.logger.warn "Connection failure [#{@error_count}]: #{e} (#{e.class})"
|
|
183
|
+
end
|
|
184
|
+
Immunio.logger.debug e.backtrace.join("\n")
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def exponential_backoff()
|
|
188
|
+
# Exponential backoff
|
|
189
|
+
delay_ms = @config.initial_delay_ms * (2 ** (@error_count - 1))
|
|
190
|
+
# Cap at max_delay_ms
|
|
191
|
+
delay_ms = [delay_ms, @config.max_delay_ms].min
|
|
192
|
+
|
|
193
|
+
# choose a random delay less than the computed delay.
|
|
194
|
+
# The randomness avoids a herd effect from transient failures.
|
|
195
|
+
delay_ms *= rand
|
|
196
|
+
delay_ms = delay_ms.round
|
|
197
|
+
|
|
198
|
+
Immunio.logger.info "Delaying #{delay_ms} ms before next request"
|
|
199
|
+
sleep delay_ms / 1000.0
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def notify(message)
|
|
203
|
+
message = message.symbolize_keys!
|
|
204
|
+
@callbacks.each { |callback| callback.call(message) }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def raw_log(raw)
|
|
208
|
+
raw.encode 'utf-8', invalid: :replace, undef: :replace
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Send initial hello request.
|
|
212
|
+
def hello
|
|
213
|
+
response = @connection.get do |req|
|
|
214
|
+
req.url "/"
|
|
215
|
+
|
|
216
|
+
req.params['name'] = AGENT_TYPE
|
|
217
|
+
req.params['version'] = VERSION
|
|
218
|
+
req.params['key'] = @config[:key]
|
|
219
|
+
|
|
220
|
+
req.headers['Accept'] = 'application/x-msgpack'
|
|
221
|
+
|
|
222
|
+
Immunio.logger.trace {"Sending hello request to agent manager (#{req})"}
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
if response.status != 200 then
|
|
226
|
+
raise Error, "Bad response from Immunio server: #{response.status} #{raw_log response.body}"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
Immunio.logger.trace {"Received hello response from agent manager (status: #{response.status}, body: #{raw_log response.body})"}
|
|
230
|
+
|
|
231
|
+
body = MessagePack.unpack(response.body)
|
|
232
|
+
|
|
233
|
+
@polling_url = body["url"]
|
|
234
|
+
|
|
235
|
+
if @polling_url.blank? then
|
|
236
|
+
raise Error, "No URL in HELLO response: #{response.body}"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
Immunio.logger.info "Agent connected to #{@config.hello_url}"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Execute a block for at max a given time to match `@config.max_report_interval`.
|
|
243
|
+
def with_report_timeout
|
|
244
|
+
time_since_last_report = Time.now.to_i - @last_report_time
|
|
245
|
+
|
|
246
|
+
# Interval expired already
|
|
247
|
+
return if time_since_last_report >= @config.max_report_interval
|
|
248
|
+
|
|
249
|
+
# Compute time to wait for new messages
|
|
250
|
+
timeout = @config.max_report_interval - time_since_last_report
|
|
251
|
+
|
|
252
|
+
Timeout.timeout timeout do
|
|
253
|
+
yield
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
rescue Timeout::Error # rubocop:disable Lint/HandleExceptions
|
|
257
|
+
# Timeout expired
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def send_buffer_has_room(extra_bytes)
|
|
261
|
+
used_bytes = @send_buffer_bytes + extra_bytes + @next_message.bytesize
|
|
262
|
+
return @send_buffer.size < @config.max_report_size && used_bytes < @config.max_report_bytes
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def add_to_send_buffer(message)
|
|
266
|
+
@send_buffer_bytes += message.bytesize
|
|
267
|
+
@send_buffer << message
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Fill send_buffer with messages to send
|
|
271
|
+
def collect_messages(used_bytes)
|
|
272
|
+
# if we had a message leftover from last send, make sure its not
|
|
273
|
+
# so big that it would fill the whole buffer
|
|
274
|
+
if @next_message != nil
|
|
275
|
+
if send_buffer_has_room used_bytes
|
|
276
|
+
add_to_send_buffer @next_message
|
|
277
|
+
else
|
|
278
|
+
Immunio.logger.warn "Dropped message over max byte send size, next message size #{@next_message.bytesize}"
|
|
279
|
+
Immunio.logger.debug "Dropped next message used: #{used_bytes} over max byte: #{@next_message}"
|
|
280
|
+
@dropped_message_count += 1
|
|
281
|
+
end
|
|
282
|
+
@next_message = nil
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Empty the queue as much as possible.
|
|
286
|
+
while !@message_queue.empty?
|
|
287
|
+
@next_message = @message_queue.pop
|
|
288
|
+
if !send_buffer_has_room used_bytes
|
|
289
|
+
break
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
add_to_send_buffer @next_message
|
|
293
|
+
@next_message = nil
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Return immediately if we've got the minimum number of messages to
|
|
297
|
+
# report or we're still waiting for an agent.ready message,
|
|
298
|
+
# or if we're at the maximum buffer size
|
|
299
|
+
return @send_buffer if @send_buffer.size >= @config.min_report_size || @next_message != nil
|
|
300
|
+
|
|
301
|
+
# Wait for messages from the queue until we have enough or get a timeout.
|
|
302
|
+
with_report_timeout do
|
|
303
|
+
while @send_buffer.size < @config.min_report_size
|
|
304
|
+
# If there are no messages in the queue, this will block until one arrives.
|
|
305
|
+
@next_message = @message_queue.pop
|
|
306
|
+
if !send_buffer_has_room used_bytes
|
|
307
|
+
break
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
add_to_send_buffer @next_message
|
|
311
|
+
@next_message = nil
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
@send_buffer
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def gzip(s)
|
|
319
|
+
gzip_io = StringIO.new
|
|
320
|
+
begin
|
|
321
|
+
gzip = Zlib::GzipWriter.new(gzip_io)
|
|
322
|
+
gzip.write s
|
|
323
|
+
ret = gzip_io.string
|
|
324
|
+
ensure
|
|
325
|
+
gzip.close
|
|
326
|
+
end
|
|
327
|
+
ret
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Poll the server sending queued messages at the same time.
|
|
331
|
+
def poll
|
|
332
|
+
# Prep data
|
|
333
|
+
body = {
|
|
334
|
+
send_seq: @send_seq,
|
|
335
|
+
dropped_message_count: @dropped_message_count,
|
|
336
|
+
rejected_message_count: @rejected_message_count,
|
|
337
|
+
name: AGENT_TYPE,
|
|
338
|
+
version: VERSION,
|
|
339
|
+
vm_version: VM_VERSION,
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
body[:agent_uuid] = @agent_uuid if @agent_uuid
|
|
343
|
+
|
|
344
|
+
# Add any values that senders would like to send:
|
|
345
|
+
@senders.each { |callback|
|
|
346
|
+
body.merge! callback.call()
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
# Encode the body of the request using msgpack streaming API.
|
|
350
|
+
# See http://ruby.msgpack.org/MessagePack/Packer.html#write_map_header-instance_method for other methods.
|
|
351
|
+
packer = MessagePack::Packer.new
|
|
352
|
+
|
|
353
|
+
# Pack the `body` hash.
|
|
354
|
+
packer.write_map_header body.size + 1 # body's + 'msgs'
|
|
355
|
+
body.each_pair { |key, value| packer.write(key).write(value) }
|
|
356
|
+
|
|
357
|
+
packer.write 'msgs' # The key
|
|
358
|
+
if not @quick_connect
|
|
359
|
+
# Append the prepacked messages.
|
|
360
|
+
collect_messages packer.size
|
|
361
|
+
packer.write_array_header @send_buffer.size
|
|
362
|
+
@send_buffer.each { |message| packer.buffer << message }
|
|
363
|
+
else
|
|
364
|
+
packer.write_array_header 0
|
|
365
|
+
@quick_connect = false
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
encoded_body = packer.to_s
|
|
369
|
+
|
|
370
|
+
# Send the request
|
|
371
|
+
response = @connection.post do |req|
|
|
372
|
+
req.url @polling_url
|
|
373
|
+
|
|
374
|
+
req.params['name'] = AGENT_TYPE
|
|
375
|
+
req.params['version'] = VERSION
|
|
376
|
+
req.params['key'] = @config[:key]
|
|
377
|
+
req.params['sig'] = OpenSSL::HMAC.hexdigest(DIGEST, @config.secret, encoded_body)
|
|
378
|
+
|
|
379
|
+
req.headers['Content-Type'] = 'application/x-msgpack'
|
|
380
|
+
req.headers['Content-Encoding'] = 'gzip'
|
|
381
|
+
req.headers['Accept'] = 'application/x-msgpack'
|
|
382
|
+
|
|
383
|
+
req.body = gzip(encoded_body)
|
|
384
|
+
|
|
385
|
+
Immunio.logger.trace {"Sending request to agent manager (data: #{MessagePack.unpack(encoded_body)}, request: #{req})"}
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
if response.status >= 400 and response.status < 500 then
|
|
389
|
+
# 4XX response codes should NOT be retried. Discard the report.
|
|
390
|
+
@rejected_message_count += @send_buffer.size
|
|
391
|
+
Immunio.logger.trace "Rejecting #{@send_buffer.size} messages"
|
|
392
|
+
@send_buffer = []
|
|
393
|
+
@send_buffer_bytes = 0
|
|
394
|
+
|
|
395
|
+
Immunio.logger.trace {"Received response from agent manager (status: #{response.status}, raw body: #{raw_log response.body})"}
|
|
396
|
+
raise Error, "Bad response from Immunio server: #{response.status} #{response.body}"
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
if response.status >= 500 then
|
|
400
|
+
# 5XX response codes are treated like errors.
|
|
401
|
+
Immunio.logger.trace {"Received response from agent manager (status: #{response.status}, raw body: #{raw_log response.body})"}
|
|
402
|
+
raise Error, "Bad response from Immunio server: #{response.status} #{response.body}"
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
body = MessagePack.unpack(response.body)
|
|
406
|
+
|
|
407
|
+
Immunio.logger.trace {"Received response from agent manager (status: #{response.status}, body: #{body}, raw body: #{raw_log response.body})"}
|
|
408
|
+
|
|
409
|
+
# Update local data from response
|
|
410
|
+
new_agent_uuid = body["agent_uuid"]
|
|
411
|
+
if new_agent_uuid
|
|
412
|
+
Immunio.logger.info "Agent UUID: #{new_agent_uuid}" if new_agent_uuid != @agent_uuid
|
|
413
|
+
@agent_uuid = new_agent_uuid
|
|
414
|
+
end
|
|
415
|
+
@send_seq += @send_buffer.size
|
|
416
|
+
|
|
417
|
+
# If messages were delivered successfully, clear send buffer.
|
|
418
|
+
if response.success?
|
|
419
|
+
@send_buffer = []
|
|
420
|
+
@send_buffer_bytes = 0
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
@last_report_time = Time.now.to_i
|
|
424
|
+
|
|
425
|
+
received_messages = body["msgs"]
|
|
426
|
+
if received_messages
|
|
427
|
+
received_messages.each { |message| notify message }
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
end
|