aikido-zen 0.1.0.alpha4-x86_64-linux → 0.1.1-x86_64-linux
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 +4 -4
- data/.simplecov +19 -0
- data/CHANGELOG.md +16 -0
- data/README.md +136 -23
- data/Rakefile +4 -0
- data/benchmarks/README.md +27 -0
- data/benchmarks/rails7.1_sql_injection.js +74 -0
- data/docs/banner.svg +203 -0
- data/docs/config.md +123 -0
- data/docs/rails.md +70 -0
- data/lib/aikido/zen/actor.rb +1 -1
- data/lib/aikido/zen/agent/heartbeats_manager.rb +66 -0
- data/lib/aikido/zen/agent.rb +100 -112
- data/lib/aikido/zen/collector/hosts.rb +15 -0
- data/lib/aikido/zen/collector/routes.rb +64 -0
- data/lib/aikido/zen/{stats → collector}/sink_stats.rb +1 -1
- data/lib/aikido/zen/collector/stats.rb +111 -0
- data/lib/aikido/zen/{stats → collector}/users.rb +6 -2
- data/lib/aikido/zen/collector.rb +117 -0
- data/lib/aikido/zen/config.rb +17 -11
- data/lib/aikido/zen/context.rb +8 -1
- data/lib/aikido/zen/errors.rb +3 -1
- data/lib/aikido/zen/event.rb +7 -4
- data/lib/aikido/zen/internals.rb +4 -0
- data/lib/aikido/zen/libzen-v0.1.31.x86_64.so +0 -0
- data/lib/aikido/zen/middleware/set_context.rb +4 -1
- data/lib/aikido/zen/rails_engine.rb +27 -18
- data/lib/aikido/zen/request/schema/builder.rb +0 -2
- data/lib/aikido/zen/request.rb +6 -0
- data/lib/aikido/zen/runtime_settings.rb +6 -11
- data/lib/aikido/zen/scanners/ssrf_scanner.rb +12 -6
- data/lib/aikido/zen/sinks/action_controller.rb +64 -0
- data/lib/aikido/zen/sinks/http.rb +1 -1
- data/lib/aikido/zen/sinks/pg.rb +13 -12
- data/lib/aikido/zen/sinks/typhoeus.rb +1 -1
- data/lib/aikido/zen/sinks.rb +1 -0
- data/lib/aikido/zen/version.rb +2 -2
- data/lib/aikido/zen/worker.rb +82 -0
- data/lib/aikido/zen.rb +55 -50
- data/tasklib/bench.rake +70 -0
- metadata +20 -9
- data/CODE_OF_CONDUCT.md +0 -132
- data/lib/aikido/zen/libzen-v0.1.26.x86_64.so +0 -0
- data/lib/aikido/zen/stats/routes.rb +0 -53
- data/lib/aikido/zen/stats.rb +0 -171
data/lib/aikido/zen/config.rb
CHANGED
@@ -8,6 +8,13 @@ require_relative "context"
|
|
8
8
|
|
9
9
|
module Aikido::Zen
|
10
10
|
class Config
|
11
|
+
# @return [Boolean] whether Aikido should be turned completely off (no
|
12
|
+
# intercepting calls to protect the app, no agent process running, no
|
13
|
+
# middleware installed). Defaults to false (so, enabled). Can be set
|
14
|
+
# via the AIKIDO_DISABLED environment variable.
|
15
|
+
attr_accessor :disabled
|
16
|
+
alias_method :disabled?, :disabled
|
17
|
+
|
11
18
|
# @return [Boolean] whether Aikido should only report infractions or block
|
12
19
|
# the request by raising an Exception. Defaults to whether AIKIDO_BLOCKING
|
13
20
|
# is set to a non-empty value in your environment, or +false+ otherwise.
|
@@ -45,7 +52,7 @@ module Aikido::Zen
|
|
45
52
|
# into an Object. Defaults to the standard library's JSON.parse method.
|
46
53
|
attr_accessor :json_decoder
|
47
54
|
|
48
|
-
# @
|
55
|
+
# @return [Logger]
|
49
56
|
attr_accessor :logger
|
50
57
|
|
51
58
|
# @return [Integer] maximum number of timing measurements to keep in memory
|
@@ -83,11 +90,9 @@ module Aikido::Zen
|
|
83
90
|
# differentiate different clients. By default this uses the request IP.
|
84
91
|
attr_accessor :rate_limiting_discriminator
|
85
92
|
|
86
|
-
# @return [
|
87
|
-
#
|
88
|
-
|
89
|
-
attr_accessor :api_schema_collection_enabled
|
90
|
-
alias_method :api_schema_collection_enabled?, :api_schema_collection_enabled
|
93
|
+
# @return [Integer] max number of requests we sample per endpoint when
|
94
|
+
# computing the schema.
|
95
|
+
attr_accessor :api_schema_max_samples
|
91
96
|
|
92
97
|
# @api private
|
93
98
|
# @return [Integer] max number of levels deep we want to read a nested
|
@@ -125,7 +130,8 @@ module Aikido::Zen
|
|
125
130
|
attr_accessor :imds_allowed_hosts
|
126
131
|
|
127
132
|
def initialize
|
128
|
-
self.
|
133
|
+
self.disabled = read_boolean_from_env(ENV.fetch("AIKIDO_DISABLED", false))
|
134
|
+
self.blocking_mode = read_boolean_from_env(ENV.fetch("AIKIDO_BLOCKING", false))
|
129
135
|
self.api_timeouts = 10
|
130
136
|
self.api_base_url = ENV.fetch("AIKIDO_BASE_URL", DEFAULT_API_BASE_URL)
|
131
137
|
self.runtime_api_base_url = ENV.fetch("AIKIDO_RUNTIME_URL", DEFAULT_RUNTIME_BASE_URL)
|
@@ -146,11 +152,9 @@ module Aikido::Zen
|
|
146
152
|
self.server_rate_limit_deadline = 1800 # 30 min
|
147
153
|
self.client_rate_limit_period = 3600 # 1 hour
|
148
154
|
self.client_rate_limit_max_events = 100
|
149
|
-
|
150
|
-
self.api_schema_collection_enabled = read_boolean_from_env(ENV.fetch("AIKIDO_FEATURE_COLLECT_API_SCHEMA", false))
|
155
|
+
self.api_schema_max_samples = Integer(ENV.fetch("AIKIDO_MAX_API_DISCOVERY_SAMPLES", 10))
|
151
156
|
self.api_schema_collection_max_depth = 20
|
152
157
|
self.api_schema_collection_max_properties = 20
|
153
|
-
|
154
158
|
self.imds_allowed_hosts = ["metadata.google.internal", "metadata.goog"]
|
155
159
|
end
|
156
160
|
|
@@ -224,6 +228,8 @@ module Aikido::Zen
|
|
224
228
|
end
|
225
229
|
|
226
230
|
# @!visibility private
|
227
|
-
DEFAULT_RATE_LIMITING_DISCRIMINATOR = ->(request) {
|
231
|
+
DEFAULT_RATE_LIMITING_DISCRIMINATOR = ->(request) {
|
232
|
+
request.actor ? "actor:#{request.actor.id}" : request.ip
|
233
|
+
}
|
228
234
|
end
|
229
235
|
end
|
data/lib/aikido/zen/context.rb
CHANGED
@@ -7,13 +7,20 @@ require_relative "payload"
|
|
7
7
|
|
8
8
|
module Aikido::Zen
|
9
9
|
class Context
|
10
|
+
# Build a Context object for the current HTTP request based on the currently
|
11
|
+
# configured request builder.
|
12
|
+
#
|
13
|
+
# @param env [Hash] the Rack env hash.
|
14
|
+
# @param config [Aikido::Zen::Config]
|
15
|
+
# @return [Aikido::Zen::Context]
|
10
16
|
def self.from_rack_env(env, config = Aikido::Zen.config)
|
11
17
|
config.request_builder.call(env)
|
12
18
|
end
|
13
19
|
|
20
|
+
# @return [Aikido::Zen::Request]
|
14
21
|
attr_reader :request
|
15
22
|
|
16
|
-
# @param [Rack::Request] a Request object that implements the
|
23
|
+
# @param request [Rack::Request] a Request object that implements the
|
17
24
|
# Rack::Request API, to which we will delegate behavior.
|
18
25
|
# @param settings [Aikido::Zen::RuntimeSettings]
|
19
26
|
#
|
data/lib/aikido/zen/errors.rb
CHANGED
@@ -3,11 +3,13 @@
|
|
3
3
|
require "forwardable"
|
4
4
|
|
5
5
|
module Aikido
|
6
|
+
# @!visibility private
|
6
7
|
# Support rescuing Aikido::Error without forcing a single base class to all
|
7
8
|
# errors (so things that should be e.g. a TypeError, can have the correct
|
8
9
|
# superclass).
|
9
|
-
module Error; end
|
10
|
+
module Error; end # :nodoc:
|
10
11
|
|
12
|
+
# @!visibility private
|
11
13
|
# Generic error for problems with the Agent.
|
12
14
|
class ZenError < RuntimeError
|
13
15
|
include Error
|
data/lib/aikido/zen/event.rb
CHANGED
@@ -48,17 +48,20 @@ module Aikido::Zen
|
|
48
48
|
end
|
49
49
|
|
50
50
|
class Heartbeat < Event
|
51
|
-
def initialize(stats:, **opts)
|
51
|
+
def initialize(stats:, users:, hosts:, routes:, **opts)
|
52
52
|
super(type: "heartbeat", **opts)
|
53
53
|
@stats = stats
|
54
|
+
@users = users
|
55
|
+
@hosts = hosts
|
56
|
+
@routes = routes
|
54
57
|
end
|
55
58
|
|
56
59
|
def as_json
|
57
60
|
super.update(
|
58
61
|
stats: @stats.as_json,
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
+
users: @users.as_json,
|
63
|
+
routes: @routes.as_json,
|
64
|
+
hostnames: @hosts.as_json
|
62
65
|
)
|
63
66
|
end
|
64
67
|
end
|
data/lib/aikido/zen/internals.rb
CHANGED
@@ -31,6 +31,8 @@ module Aikido::Zen
|
|
31
31
|
attach_function :detect_sql_injection_native, :detect_sql_injection,
|
32
32
|
[:string, :string, :int], :int
|
33
33
|
rescue LoadError, FFI::NotFoundError => err
|
34
|
+
# :nocov:
|
35
|
+
|
34
36
|
# Emit an $stderr warning at startup.
|
35
37
|
warn "Zen could not load its binary extension #{libzen_name}: #{err}"
|
36
38
|
|
@@ -38,6 +40,8 @@ module Aikido::Zen
|
|
38
40
|
attempt = format("%p for SQL injection", query)
|
39
41
|
raise InternalsError.new(attempt, "loading", Internals.libzen_name)
|
40
42
|
end
|
43
|
+
|
44
|
+
# :nocov:
|
41
45
|
else
|
42
46
|
# Analyzes the SQL query to detect if the provided user input is being
|
43
47
|
# passed as-is without escaping.
|
Binary file
|
@@ -3,6 +3,9 @@
|
|
3
3
|
require_relative "../context"
|
4
4
|
|
5
5
|
module Aikido::Zen
|
6
|
+
# @!visibility private
|
7
|
+
ENV_KEY = "aikido.context"
|
8
|
+
|
6
9
|
module Middleware
|
7
10
|
# Rack middleware that keeps the current context in a Thread/Fiber-local
|
8
11
|
# variable so that other parts of the agent/firewall can access it.
|
@@ -14,7 +17,7 @@ module Aikido::Zen
|
|
14
17
|
def call(env)
|
15
18
|
context = Context.from_rack_env(env)
|
16
19
|
|
17
|
-
Aikido::Zen.current_context = context
|
20
|
+
Aikido::Zen.current_context = env[ENV_KEY] = context
|
18
21
|
Aikido::Zen.track_request(context.request)
|
19
22
|
|
20
23
|
@app.call(env)
|
@@ -5,44 +5,53 @@ require "action_dispatch"
|
|
5
5
|
module Aikido::Zen
|
6
6
|
class RailsEngine < ::Rails::Engine
|
7
7
|
config.before_configuration do
|
8
|
-
# Access library configuration at `Rails.application.config.
|
9
|
-
config.
|
8
|
+
# Access library configuration at `Rails.application.config.zen`.
|
9
|
+
config.zen = Aikido::Zen.config
|
10
10
|
end
|
11
11
|
|
12
12
|
initializer "aikido.add_middleware" do |app|
|
13
|
+
next if config.zen.disabled?
|
14
|
+
|
13
15
|
app.middleware.use Aikido::Zen::Middleware::SetContext
|
14
16
|
app.middleware.use Aikido::Zen::Middleware::CheckAllowedAddresses
|
15
|
-
|
16
|
-
|
17
|
-
# Due to how Rails sets up its middleware chain, the routing is evaluated
|
18
|
-
# (and the Request object constructed) in the app that terminates the
|
19
|
-
# chain, so no amount of middleware will be able to access it.
|
20
|
-
#
|
21
|
-
# This way, we overwrite the Request object as early as we can in the
|
22
|
-
# request handling, so that by the time we start evaluating inputs, we
|
23
|
-
# have assigned the request correctly.
|
17
|
+
|
24
18
|
ActiveSupport.on_load(:action_controller) do
|
19
|
+
# Due to how Rails sets up its middleware chain, the routing is evaluated
|
20
|
+
# (and the Request object constructed) in the app that terminates the
|
21
|
+
# chain, so no amount of middleware will be able to access it.
|
22
|
+
#
|
23
|
+
# This way, we overwrite the Request object as early as we can in the
|
24
|
+
# request handling, so that by the time we start evaluating inputs, we
|
25
|
+
# have assigned the request correctly.
|
25
26
|
before_action { Aikido::Zen.current_context.update_request(request) }
|
26
27
|
end
|
27
28
|
end
|
28
29
|
|
29
30
|
initializer "aikido.configuration" do |app|
|
30
|
-
|
31
|
-
|
31
|
+
# Allow the logger to be configured before checking if disabled? so we can
|
32
|
+
# let the user know that the agent is disabled.
|
33
|
+
app.config.zen.logger = ::Rails.logger.tagged("aikido")
|
34
|
+
|
35
|
+
next if config.zen.disabled?
|
36
|
+
|
37
|
+
app.config.zen.request_builder = Aikido::Zen::Context::RAILS_REQUEST_BUILDER
|
32
38
|
|
33
39
|
# Plug Rails' JSON encoder/decoder, but only if the user hasn't changed
|
34
40
|
# them for something else.
|
35
|
-
if app.config.
|
36
|
-
app.config.
|
41
|
+
if app.config.zen.json_encoder == Aikido::Zen::Config::DEFAULT_JSON_ENCODER
|
42
|
+
app.config.zen.json_encoder = ActiveSupport::JSON.method(:encode)
|
37
43
|
end
|
38
44
|
|
39
|
-
if app.config.
|
40
|
-
app.config.
|
45
|
+
if app.config.zen.json_decoder == Aikido::Zen::Config::DEFAULT_JSON_DECODER
|
46
|
+
app.config.zen.json_decoder = ActiveSupport::JSON.method(:decode)
|
41
47
|
end
|
42
48
|
end
|
43
49
|
|
44
50
|
config.after_initialize do
|
45
|
-
|
51
|
+
if config.zen.disabled?
|
52
|
+
config.zen.logger.warn("Zen has been disabled and will not run.")
|
53
|
+
next
|
54
|
+
end
|
46
55
|
|
47
56
|
# Make sure this is run at the end of the initialization process, so
|
48
57
|
# that any gems required after aikido-zen are detected and patched
|
data/lib/aikido/zen/request.rb
CHANGED
@@ -11,6 +11,12 @@ module Aikido::Zen
|
|
11
11
|
# @return [Aikido::Zen::Router]
|
12
12
|
attr_reader :router
|
13
13
|
|
14
|
+
# The current user, if set by the host app.
|
15
|
+
#
|
16
|
+
# @return [Aikido::Zen::Actor, nil]
|
17
|
+
# @see Aikido::Zen.track_user
|
18
|
+
attr_accessor :actor
|
19
|
+
|
14
20
|
def initialize(delegate, framework:, router:)
|
15
21
|
super(delegate)
|
16
22
|
@framework = framework
|
@@ -11,16 +11,11 @@ module Aikido::Zen
|
|
11
11
|
#
|
12
12
|
# You can subscribe to changes with +#add_observer(object, func_name)+, which
|
13
13
|
# will call the function passing the settings as an argument.
|
14
|
-
|
15
|
-
:updated_at, :heartbeat_interval, :endpoints, :blocked_user_ids, :skip_protection_for_ips, :received_any_stats
|
16
|
-
)
|
17
|
-
include Concurrent::Concern::Observable
|
18
|
-
|
14
|
+
RuntimeSettings = Struct.new(:updated_at, :heartbeat_interval, :endpoints, :blocked_user_ids, :skip_protection_for_ips, :received_any_stats) do
|
19
15
|
def initialize(*)
|
20
|
-
self.observers = Concurrent::Collection::CopyOnWriteObserverSet.new
|
21
16
|
super
|
22
|
-
self.endpoints ||= Endpoints.new
|
23
|
-
self.skip_protection_for_ips ||= IPSet.new
|
17
|
+
self.endpoints ||= RuntimeSettings::Endpoints.new
|
18
|
+
self.skip_protection_for_ips ||= RuntimeSettings::IPSet.new
|
24
19
|
end
|
25
20
|
|
26
21
|
# @!attribute [rw] updated_at
|
@@ -56,12 +51,12 @@ module Aikido::Zen
|
|
56
51
|
|
57
52
|
self.updated_at = Time.at(data["configUpdatedAt"].to_i / 1000)
|
58
53
|
self.heartbeat_interval = (data["heartbeatIntervalInMS"].to_i / 1000)
|
59
|
-
self.endpoints = Endpoints.from_json(data["endpoints"])
|
54
|
+
self.endpoints = RuntimeSettings::Endpoints.from_json(data["endpoints"])
|
60
55
|
self.blocked_user_ids = data["blockedUserIds"]
|
61
|
-
self.skip_protection_for_ips = IPSet.from_json(data["allowedIPAddresses"])
|
56
|
+
self.skip_protection_for_ips = RuntimeSettings::IPSet.from_json(data["allowedIPAddresses"])
|
62
57
|
self.received_any_stats = data["receivedAnyStats"]
|
63
58
|
|
64
|
-
|
59
|
+
Aikido::Zen.agent.updated_settings! if updated_at != last_updated_at
|
65
60
|
end
|
66
61
|
end
|
67
62
|
end
|
@@ -112,7 +112,8 @@ module Aikido::Zen
|
|
112
112
|
is_port_relevant = input_uri.port != input_uri.default_port
|
113
113
|
return false if is_port_relevant && input_uri.port != conn_uri.port
|
114
114
|
|
115
|
-
conn_uri.hostname == input_uri.hostname
|
115
|
+
conn_uri.hostname == input_uri.hostname &&
|
116
|
+
conn_uri.port == input_uri.port
|
116
117
|
end
|
117
118
|
|
118
119
|
def private_ip?(hostname)
|
@@ -128,8 +129,11 @@ module Aikido::Zen
|
|
128
129
|
# * The input itself, if it already looks like a URI.
|
129
130
|
# * The input prefixed with http://
|
130
131
|
# * The input prefixed with https://
|
132
|
+
# * The input prefixed with the scheme of the request's URI, to consider
|
133
|
+
# things like an FTP request (to "ftp://localhost") with a plain host
|
134
|
+
# as a user-input ("localhost").
|
131
135
|
#
|
132
|
-
# @return [
|
136
|
+
# @return [Array<URI>] a list of unique URIs based on the above criteria.
|
133
137
|
def uris_from_input
|
134
138
|
input = @input.to_s
|
135
139
|
|
@@ -138,10 +142,12 @@ module Aikido::Zen
|
|
138
142
|
# valid hostname. We should do the same for the input.
|
139
143
|
input = format("[%s]", input) if unescaped_ipv6?(input)
|
140
144
|
|
141
|
-
[
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
+
[
|
146
|
+
input,
|
147
|
+
"http://#{input}",
|
148
|
+
"https://#{input}",
|
149
|
+
"#{@request_uri.scheme}://#{input}"
|
150
|
+
].map { |candidate| as_uri(candidate) }.compact.uniq
|
145
151
|
end
|
146
152
|
|
147
153
|
def as_uri(string)
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aikido::Zen
|
4
|
+
module Sinks
|
5
|
+
module ActionController
|
6
|
+
# Implements the "middleware" for rate limiting in Rails apps, where we
|
7
|
+
# need to check at the end of the `before_action` chain, rather than in
|
8
|
+
# an actual Rack middleware, to allow for calls to Zen.track_user being
|
9
|
+
# made from before_actions in the host app, thus allowing rate-limiting
|
10
|
+
# by user ID rather than solely by IP.
|
11
|
+
class Throttler
|
12
|
+
def initialize(
|
13
|
+
config: Aikido::Zen.config,
|
14
|
+
settings: Aikido::Zen.runtime_settings,
|
15
|
+
rate_limiter: Aikido::Zen::RateLimiter.new
|
16
|
+
)
|
17
|
+
@config = config
|
18
|
+
@settings = settings
|
19
|
+
@rate_limiter = rate_limiter
|
20
|
+
end
|
21
|
+
|
22
|
+
def throttle(controller)
|
23
|
+
context = controller.request.env[Aikido::Zen::ENV_KEY]
|
24
|
+
request = context.request
|
25
|
+
|
26
|
+
if should_throttle?(request)
|
27
|
+
status, headers, body = @config.rate_limited_responder.call(request)
|
28
|
+
controller.headers.update(headers)
|
29
|
+
controller.render plain: Array(body).join, status: status
|
30
|
+
|
31
|
+
return true
|
32
|
+
end
|
33
|
+
|
34
|
+
false
|
35
|
+
end
|
36
|
+
|
37
|
+
private def should_throttle?(request)
|
38
|
+
return false if @settings.skip_protection_for_ips.include?(request.ip)
|
39
|
+
|
40
|
+
@rate_limiter.throttle?(request)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.throttler
|
45
|
+
@throttler ||= Aikido::Zen::Sinks::ActionController::Throttler.new
|
46
|
+
end
|
47
|
+
|
48
|
+
module Extensions
|
49
|
+
def run_callbacks(kind, *)
|
50
|
+
return super unless kind == :process_action
|
51
|
+
|
52
|
+
super do
|
53
|
+
rate_limiter = Aikido::Zen::Sinks::ActionController.throttler
|
54
|
+
throttled = rate_limiter.throttle(self)
|
55
|
+
|
56
|
+
yield if block_given? && !throttled
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
::AbstractController::Callbacks.prepend(Aikido::Zen::Sinks::ActionController::Extensions)
|
data/lib/aikido/zen/sinks/pg.rb
CHANGED
@@ -7,6 +7,17 @@ module Aikido::Zen
|
|
7
7
|
module PG
|
8
8
|
SINK = Sinks.add("pg", scanners: [Scanners::SQLInjectionScanner])
|
9
9
|
|
10
|
+
# For some reason, the ActiveRecord pg adapter does not wrap exceptions in
|
11
|
+
# StatementInvalid, which leads to inconsistent handling. This guarantees
|
12
|
+
# that all Zen errors are wrapped in a StatementInvalid, so documentation
|
13
|
+
# can be consistent.
|
14
|
+
WRAP_EXCEPTIONS = if defined?(ActiveRecord::StatementInvalid)
|
15
|
+
<<~RUBY
|
16
|
+
rescue Aikido::Zen::SQLInjectionError
|
17
|
+
raise ActiveRecord::StatementInvalid
|
18
|
+
RUBY
|
19
|
+
end
|
20
|
+
|
10
21
|
module Extensions
|
11
22
|
%i[
|
12
23
|
send_query exec sync_exec async_exec
|
@@ -16,12 +27,7 @@ module Aikido::Zen
|
|
16
27
|
def #{method}(query, *)
|
17
28
|
SINK.scan(query: query, dialect: :postgresql, operation: :#{method})
|
18
29
|
super
|
19
|
-
|
20
|
-
# The pg adapter does not wrap exceptions in StatementInvalid, which
|
21
|
-
# leads to inconsistent handling. This guarantees that all Aikido
|
22
|
-
# errors are wrapped in a StatementInvalid, so documentation can be
|
23
|
-
# consistent.
|
24
|
-
raise ActiveRecord::StatementInvalid
|
30
|
+
#{WRAP_EXCEPTIONS}
|
25
31
|
end
|
26
32
|
RUBY
|
27
33
|
end
|
@@ -33,12 +39,7 @@ module Aikido::Zen
|
|
33
39
|
def #{method}(_, query, *)
|
34
40
|
SINK.scan(query: query, dialect: :postgresql, operation: :#{method})
|
35
41
|
super
|
36
|
-
|
37
|
-
# The pg adapter does not wrap exceptions in StatementInvalid, which
|
38
|
-
# leads to inconsistent handling. This guarantees that all Aikido
|
39
|
-
# errors are wrapped in a StatementInvalid, so documentation can be
|
40
|
-
# consistent.
|
41
|
-
raise ActiveRecord::StatementInvalid
|
42
|
+
#{WRAP_EXCEPTIONS}
|
42
43
|
end
|
43
44
|
RUBY
|
44
45
|
end
|
data/lib/aikido/zen/sinks.rb
CHANGED
@@ -4,6 +4,7 @@ require_relative "sink"
|
|
4
4
|
|
5
5
|
require_relative "sinks/socket"
|
6
6
|
|
7
|
+
require_relative "sinks/action_controller" if defined?(::ActionController)
|
7
8
|
require_relative "sinks/resolv" if defined?(::Resolv)
|
8
9
|
require_relative "sinks/net_http" if defined?(::Net::HTTP)
|
9
10
|
require_relative "sinks/http" if defined?(::HTTP)
|
data/lib/aikido/zen/version.rb
CHANGED
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "concurrent"
|
4
|
+
|
5
|
+
module Aikido::Zen
|
6
|
+
# @api private
|
7
|
+
#
|
8
|
+
# The worker manages the background thread in which Zen communicates with the
|
9
|
+
# Aikido server.
|
10
|
+
class Worker
|
11
|
+
# @return [Concurrent::ExecutorService]
|
12
|
+
attr_reader :executor
|
13
|
+
|
14
|
+
# @!visibility private
|
15
|
+
attr_reader :timers, :deferrals
|
16
|
+
|
17
|
+
def initialize(config: Aikido::Zen.config)
|
18
|
+
@config = config
|
19
|
+
@timers = []
|
20
|
+
@deferrals = []
|
21
|
+
@executor = Concurrent::SingleThreadExecutor.new
|
22
|
+
end
|
23
|
+
|
24
|
+
# Queue a block to be run asynchronously in the background thread.
|
25
|
+
#
|
26
|
+
# @return [void]
|
27
|
+
def perform(&block)
|
28
|
+
executor.post do
|
29
|
+
yield
|
30
|
+
rescue Exception => err # rubocop:disable Lint/RescueException
|
31
|
+
@config.logger.error "Error in background worker: #{err.inspect}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Queue a block to be run asynchronously after a delay.
|
36
|
+
#
|
37
|
+
# @param interval [Integer] amount of seconds to wait.
|
38
|
+
# @return [void]
|
39
|
+
def delay(interval, &task)
|
40
|
+
Concurrent::ScheduledTask
|
41
|
+
.execute(interval, executor: executor) { perform(&task) }
|
42
|
+
.tap { |deferral| @deferrals << deferral }
|
43
|
+
end
|
44
|
+
|
45
|
+
# Queue a block to run repeatedly on a timer on the background thread. The
|
46
|
+
# timer will consider how long the block takes to run to schedule the next
|
47
|
+
# run. For example, if you schedule a block to run every 10 seconds, and the
|
48
|
+
# block itself takes 2 seconds, the second iteration will be run 8 seconds
|
49
|
+
# after the first one.
|
50
|
+
#
|
51
|
+
# If the block takes longer than the given interval, the second iteration
|
52
|
+
# will be run immediately.
|
53
|
+
#
|
54
|
+
# @param interval [Integer] amount of seconds to wait between runs.
|
55
|
+
# @param run_now [Boolean] whether to run the block immediately, or wait for
|
56
|
+
# +interval+ seconds before the first run. Defaults to +true+.
|
57
|
+
# @return [void]
|
58
|
+
def every(interval, run_now: true, &task)
|
59
|
+
Concurrent::TimerTask
|
60
|
+
.execute(
|
61
|
+
run_now: run_now,
|
62
|
+
executor: executor,
|
63
|
+
interval_type: :fixed_rate,
|
64
|
+
execution_interval: interval
|
65
|
+
) {
|
66
|
+
perform(&task)
|
67
|
+
}
|
68
|
+
.tap { |timer| @timers << timer }
|
69
|
+
end
|
70
|
+
|
71
|
+
# Safely clean up and kill the thread, giving time to kill any ongoing tasks
|
72
|
+
# on the queue.
|
73
|
+
#
|
74
|
+
# @return [void]
|
75
|
+
def shutdown
|
76
|
+
@deferrals.each { |task| task.cancel if task.pending? }
|
77
|
+
@timers.each { |task| task.shutdown }
|
78
|
+
@executor.shutdown
|
79
|
+
@executor.wait_for_termination(30)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|