aikido-zen 1.0.2.beta.2-aarch64-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.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/.aikido +6 -0
  3. data/.ruby-version +1 -0
  4. data/.simplecov +26 -0
  5. data/.standard.yml +3 -0
  6. data/LICENSE +674 -0
  7. data/README.md +146 -0
  8. data/Rakefile +67 -0
  9. data/benchmarks/README.md +23 -0
  10. data/benchmarks/rails7.1_sql_injection.js +70 -0
  11. data/docs/banner.svg +202 -0
  12. data/docs/config.md +125 -0
  13. data/docs/proxy.md +10 -0
  14. data/docs/rails.md +114 -0
  15. data/lib/aikido/zen/actor.rb +116 -0
  16. data/lib/aikido/zen/agent/heartbeats_manager.rb +66 -0
  17. data/lib/aikido/zen/agent.rb +179 -0
  18. data/lib/aikido/zen/api_client.rb +145 -0
  19. data/lib/aikido/zen/attack.rb +207 -0
  20. data/lib/aikido/zen/background_worker.rb +52 -0
  21. data/lib/aikido/zen/capped_collections.rb +68 -0
  22. data/lib/aikido/zen/collector/hosts.rb +15 -0
  23. data/lib/aikido/zen/collector/routes.rb +66 -0
  24. data/lib/aikido/zen/collector/sink_stats.rb +95 -0
  25. data/lib/aikido/zen/collector/stats.rb +111 -0
  26. data/lib/aikido/zen/collector/users.rb +30 -0
  27. data/lib/aikido/zen/collector.rb +144 -0
  28. data/lib/aikido/zen/config.rb +282 -0
  29. data/lib/aikido/zen/context/rack_request.rb +24 -0
  30. data/lib/aikido/zen/context/rails_request.rb +44 -0
  31. data/lib/aikido/zen/context.rb +112 -0
  32. data/lib/aikido/zen/detached_agent/agent.rb +78 -0
  33. data/lib/aikido/zen/detached_agent/front_object.rb +37 -0
  34. data/lib/aikido/zen/detached_agent/server.rb +78 -0
  35. data/lib/aikido/zen/detached_agent.rb +2 -0
  36. data/lib/aikido/zen/errors.rb +107 -0
  37. data/lib/aikido/zen/event.rb +71 -0
  38. data/lib/aikido/zen/internals.rb +103 -0
  39. data/lib/aikido/zen/libzen-v0.1.39-aarch64-linux.so +0 -0
  40. data/lib/aikido/zen/middleware/check_allowed_addresses.rb +26 -0
  41. data/lib/aikido/zen/middleware/middleware.rb +11 -0
  42. data/lib/aikido/zen/middleware/rack_throttler.rb +48 -0
  43. data/lib/aikido/zen/middleware/request_tracker.rb +192 -0
  44. data/lib/aikido/zen/middleware/set_context.rb +26 -0
  45. data/lib/aikido/zen/outbound_connection.rb +45 -0
  46. data/lib/aikido/zen/outbound_connection_monitor.rb +23 -0
  47. data/lib/aikido/zen/package.rb +22 -0
  48. data/lib/aikido/zen/payload.rb +50 -0
  49. data/lib/aikido/zen/rails_engine.rb +56 -0
  50. data/lib/aikido/zen/rate_limiter/breaker.rb +61 -0
  51. data/lib/aikido/zen/rate_limiter/bucket.rb +76 -0
  52. data/lib/aikido/zen/rate_limiter/result.rb +31 -0
  53. data/lib/aikido/zen/rate_limiter.rb +50 -0
  54. data/lib/aikido/zen/request/heuristic_router.rb +115 -0
  55. data/lib/aikido/zen/request/rails_router.rb +77 -0
  56. data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
  57. data/lib/aikido/zen/request/schema/auth_schemas.rb +54 -0
  58. data/lib/aikido/zen/request/schema/builder.rb +121 -0
  59. data/lib/aikido/zen/request/schema/definition.rb +107 -0
  60. data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
  61. data/lib/aikido/zen/request/schema.rb +87 -0
  62. data/lib/aikido/zen/request.rb +122 -0
  63. data/lib/aikido/zen/route.rb +39 -0
  64. data/lib/aikido/zen/runtime_settings/endpoints.rb +49 -0
  65. data/lib/aikido/zen/runtime_settings/ip_set.rb +36 -0
  66. data/lib/aikido/zen/runtime_settings/protection_settings.rb +62 -0
  67. data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +47 -0
  68. data/lib/aikido/zen/runtime_settings.rb +65 -0
  69. data/lib/aikido/zen/scan.rb +75 -0
  70. data/lib/aikido/zen/scanners/path_traversal/helpers.rb +65 -0
  71. data/lib/aikido/zen/scanners/path_traversal_scanner.rb +63 -0
  72. data/lib/aikido/zen/scanners/shell_injection/helpers.rb +159 -0
  73. data/lib/aikido/zen/scanners/shell_injection_scanner.rb +64 -0
  74. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +93 -0
  75. data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
  76. data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +97 -0
  77. data/lib/aikido/zen/scanners/ssrf_scanner.rb +265 -0
  78. data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +49 -0
  79. data/lib/aikido/zen/scanners.rb +7 -0
  80. data/lib/aikido/zen/sink.rb +118 -0
  81. data/lib/aikido/zen/sinks/action_controller.rb +83 -0
  82. data/lib/aikido/zen/sinks/async_http.rb +80 -0
  83. data/lib/aikido/zen/sinks/curb.rb +113 -0
  84. data/lib/aikido/zen/sinks/em_http.rb +83 -0
  85. data/lib/aikido/zen/sinks/excon.rb +118 -0
  86. data/lib/aikido/zen/sinks/file.rb +112 -0
  87. data/lib/aikido/zen/sinks/http.rb +93 -0
  88. data/lib/aikido/zen/sinks/httpclient.rb +95 -0
  89. data/lib/aikido/zen/sinks/httpx.rb +78 -0
  90. data/lib/aikido/zen/sinks/kernel.rb +33 -0
  91. data/lib/aikido/zen/sinks/mysql2.rb +31 -0
  92. data/lib/aikido/zen/sinks/net_http.rb +101 -0
  93. data/lib/aikido/zen/sinks/patron.rb +103 -0
  94. data/lib/aikido/zen/sinks/pg.rb +72 -0
  95. data/lib/aikido/zen/sinks/resolv.rb +62 -0
  96. data/lib/aikido/zen/sinks/socket.rb +78 -0
  97. data/lib/aikido/zen/sinks/sqlite3.rb +46 -0
  98. data/lib/aikido/zen/sinks/trilogy.rb +31 -0
  99. data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
  100. data/lib/aikido/zen/sinks.rb +36 -0
  101. data/lib/aikido/zen/sinks_dsl.rb +250 -0
  102. data/lib/aikido/zen/synchronizable.rb +24 -0
  103. data/lib/aikido/zen/system_info.rb +84 -0
  104. data/lib/aikido/zen/version.rb +10 -0
  105. data/lib/aikido/zen/worker.rb +87 -0
  106. data/lib/aikido/zen.rb +246 -0
  107. data/lib/aikido-zen.rb +3 -0
  108. data/placeholder/.gitignore +4 -0
  109. data/placeholder/README.md +11 -0
  110. data/placeholder/Rakefile +75 -0
  111. data/placeholder/lib/placeholder.rb.template +3 -0
  112. data/placeholder/placeholder.gemspec.template +20 -0
  113. data/tasklib/bench.rake +94 -0
  114. data/tasklib/libzen.rake +133 -0
  115. data/tasklib/wrk.rb +88 -0
  116. metadata +205 -0
data/docs/rails.md ADDED
@@ -0,0 +1,114 @@
1
+ # Setting up Zen on a Ruby on Rails application
2
+
3
+ To install Zen, add the gem:
4
+
5
+ ```sh
6
+ bundle add aikido-zen
7
+ ```
8
+
9
+ And require it before `Bundler.require` in `config/application.rb`:
10
+
11
+ ```ruby
12
+ # config/application.rb
13
+ require_relative "boot"
14
+
15
+ require "rails/all"
16
+
17
+ require "aikido-zen"
18
+ Aikido::Zen.protect!
19
+
20
+ # Require the gems listed in Gemfile, including any gems
21
+ # you've limited to :test, :development, or :production.
22
+ Bundler.require(*Rails.groups)
23
+
24
+ ...
25
+ ```
26
+
27
+ That's it! Zen will start to run inside your app when it starts getting
28
+ requests.
29
+
30
+ ## Rate limiting and user blocking
31
+
32
+ If you want to add the rate limiting feature to your app, modify your code like this:
33
+
34
+ ```ruby
35
+ # app/controllers/application_controller.rb
36
+ class ApplicationController < ActionController::Base
37
+ private
38
+
39
+ def current_user
40
+ return unless session[:user_id]
41
+ User.find(session[:user_id])
42
+ end
43
+
44
+ def authenticate_user!
45
+ # Your authentication logic here
46
+ # ...
47
+ # Optional, if you want to use user based rate limiting or block specific users
48
+ Aikido::Zen.set_user(
49
+ id: current_user.id,
50
+ name: current_user.name
51
+ )
52
+ end
53
+ end
54
+ ```
55
+
56
+ ## Configuration
57
+
58
+ Zen exposes its configuration object to the Rails configuration, which you can
59
+ modify in an initializer if desired:
60
+
61
+ ```ruby
62
+ # config/initializers/zen.rb
63
+ Rails.application.config.zen.api_timeouts = 20
64
+ ```
65
+
66
+ You can access the configuration object both as `Aikido::Zen.config` or
67
+ `Rails.configuration.zen`.
68
+
69
+ See our [configuration guide](./config.md) for more details.
70
+
71
+ ## Using Rails encrypted credentials
72
+
73
+ If you're using Rails' [encrypted credentials][creds], and prefer not storing
74
+ sensitive values in your env vars, you can easily configure Zen for it. For
75
+ example, assuming the following credentials structure:
76
+
77
+ ```yaml
78
+ # config/credentials.yml.enc
79
+ zen:
80
+ token: "AIKIDO_RUNTIME_..."
81
+ ```
82
+
83
+ You can just tell Zen to use it like so:
84
+
85
+ ```ruby
86
+ # config/initializers/zen.rb
87
+ Rails.application.config.zen.token = Rails.application.credentials.zen.token
88
+ ```
89
+
90
+ [creds]: https://guides.rubyonrails.org/security.html#environmental-security
91
+
92
+ ## Blocking mode
93
+
94
+ By default, Zen will only detect and log attacks, but will not block them. You
95
+ can enable blocking mode by setting the `AIKIDO_BLOCK` environment variable
96
+ to `true`.
97
+
98
+ When in blocking mode, Zen will raise an exception when it detects an attack.
99
+ These exceptions depend on the type of attack, but all inherit from
100
+ `Aikido::Zen::UnderAttackError`, if you wish to handle these exceptions in any
101
+ way.
102
+
103
+ ## Logging
104
+
105
+ By default, Zen will use the Rails logger, prefixing messages with `[aikido]`.
106
+ You can redirect the log to a separate stream by overriding the logger:
107
+
108
+ ```ruby
109
+ # config/initializers/zen.rb
110
+ Rails.application.config.zen.logger = Logger.new(...)
111
+ ```
112
+
113
+ You should supply an instance of ruby's [Logger](https://github.com/ruby/logger)
114
+ class.
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+ require_relative "config"
5
+
6
+ module Aikido::Zen
7
+ # Converts an object into an Actor for reporting back to the Aikido Dashboard.
8
+ #
9
+ # @overload Actor(actor)
10
+ # @param actor [#to_aikido_actor] anything that implements #to_aikido_actor
11
+ # will have that method called and its value returned.
12
+ # @return Aikido::Zen::Actor
13
+ #
14
+ # @overload Actor(data)
15
+ # @param data [Hash<Symbol, String>]
16
+ # @option data [String] :id a unique identifier for this user.
17
+ # @option data [String, nil] :name an optional name to display in the UI.
18
+ # @return Aikido::Zen::Actor
19
+ def self.Actor(data)
20
+ return if data.nil?
21
+ return data.to_aikido_actor if data.respond_to?(:to_aikido_actor)
22
+
23
+ attrs = {}
24
+ if data.respond_to?(:to_hash)
25
+ attrs = data.to_hash
26
+ .slice("id", "name", :id, :name)
27
+ .compact
28
+ .transform_keys(&:to_sym)
29
+ .transform_values(&:to_s)
30
+ else
31
+ return nil
32
+ end
33
+
34
+ return nil if attrs[:id].nil? || attrs[:id].to_s.strip.empty?
35
+
36
+ Actor.new(**attrs)
37
+ end
38
+
39
+ # Represents someone connecting to the application and making requests.
40
+ class Actor
41
+ # @return [String] a unique identifier for this user.
42
+ attr_reader :id
43
+
44
+ # @return [String, nil] an optional name to display in the Aikido UI.
45
+ attr_reader :name
46
+
47
+ # @return [Time]
48
+ attr_reader :first_seen_at
49
+
50
+ # @param id [String]
51
+ # @param name [String, nil]
52
+ # @param ip [String, nil]
53
+ # @param seen_at [Time]
54
+ def initialize(
55
+ id:,
56
+ name: nil,
57
+ ip: Aikido::Zen.current_context&.request&.ip,
58
+ seen_at: Time.now.utc
59
+ )
60
+ @id = id
61
+ @name = name
62
+ @first_seen_at = seen_at
63
+ @last_seen_at = Concurrent::AtomicReference.new(seen_at)
64
+ @ip = Concurrent::AtomicReference.new(ip)
65
+ end
66
+
67
+ # @return [Time]
68
+ def last_seen_at
69
+ @last_seen_at.get
70
+ end
71
+
72
+ # @return [String, nil] the IP address last used by this user, if available.
73
+ def ip
74
+ @ip.get
75
+ end
76
+
77
+ # Atomically update the last IP used by the user, and the last time they've
78
+ # been "seen" connecting to the app.
79
+ #
80
+ # @param ip [String, nil] the last-seen IP address for the user. If +nil+
81
+ # and we had a non-empty value before, we won't update it. Defaults to
82
+ # the current HTTP request's IP address, if any.
83
+ # @param seen_at [Time] the time at which we're making the update. We will
84
+ # always keep the most recent time if this conflicts with the current
85
+ # value.
86
+ # @return [void]
87
+ def update(seen_at: Time.now.utc, ip: Aikido::Zen.current_context&.request&.ip)
88
+ @last_seen_at.try_update { |last_seen_at| [last_seen_at, seen_at].max }
89
+ @ip.try_update { |last_ip| [ip, last_ip].compact.first }
90
+ end
91
+
92
+ # @return [self]
93
+ def to_aikido_actor
94
+ self
95
+ end
96
+
97
+ def ==(other)
98
+ other.is_a?(Actor) && id == other.id
99
+ end
100
+ alias_method :eql?, :==
101
+
102
+ def hash
103
+ id.hash
104
+ end
105
+
106
+ def as_json
107
+ {
108
+ id: id,
109
+ name: name,
110
+ lastIpAddress: ip,
111
+ firstSeenAt: first_seen_at.to_i * 1000,
112
+ lastSeenAt: last_seen_at.to_i * 1000
113
+ }.compact
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ # Handles scheduling the heartbeats we send to the Aikido servers, managing
5
+ # runtime changes to the heartbeat interval.
6
+ class Agent::HeartbeatsManager
7
+ def initialize(worker:, settings: Aikido::Zen.runtime_settings, config: Aikido::Zen.config)
8
+ @settings = settings
9
+ @config = config
10
+ @worker = worker
11
+
12
+ @timer = nil
13
+ end
14
+
15
+ # @return [Boolean]
16
+ def running?
17
+ !!@timer&.running?
18
+ end
19
+
20
+ # @return [Boolean] whether the currently running heartbeat matches the
21
+ # expected interval in the runtime settings.
22
+ def stale_settings?
23
+ running? && @timer.execution_interval != @settings.heartbeat_interval
24
+ end
25
+
26
+ # Sets up the the timer to run the given block at the appropriate interval.
27
+ # Re-entrant, and does nothing if already running.
28
+ #
29
+ # @return [void]
30
+ def start(&task)
31
+ return if running?
32
+
33
+ if @settings.heartbeat_interval&.nonzero?
34
+ @config.logger.debug "Scheduling heartbeats every #{@settings.heartbeat_interval} seconds"
35
+ @timer = @worker.every(@settings.heartbeat_interval, run_now: false, &task)
36
+ else
37
+ @config.logger.warn(format("Heartbeat could not be set up (interval: %p)", @settings.heartbeat_interval))
38
+ end
39
+ end
40
+
41
+ # Cleans up the timer.
42
+ #
43
+ # @return [void]
44
+ def stop
45
+ return unless running?
46
+
47
+ @timer.shutdown
48
+ @timer = nil
49
+ end
50
+
51
+ # Resets the timer to start with any new settings, if needed.
52
+ #
53
+ # @return [void]
54
+ def restart(&task)
55
+ stop
56
+ start(&task)
57
+ end
58
+
59
+ # @api private
60
+ #
61
+ # @return [Integer] the current delay between events.
62
+ def interval
63
+ @settings.heartbeat_interval
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+ require_relative "event"
5
+ require_relative "config"
6
+ require_relative "system_info"
7
+
8
+ module Aikido::Zen
9
+ # Handles the background processes that communicate with the Aikido servers,
10
+ # including managing the runtime settings that keep the app protected.
11
+ class Agent
12
+ # Initialize and start an agent instance.
13
+ #
14
+ # @return [Aikido::Zen::Agent]
15
+ def self.start(**opts)
16
+ new(**opts).tap(&:start!)
17
+ end
18
+
19
+ def initialize(
20
+ config: Aikido::Zen.config,
21
+ collector: Aikido::Zen.collector,
22
+ detached_agent: Aikido::Zen.detached_agent,
23
+ worker: Aikido::Zen::Worker.new(config: config),
24
+ api_client: Aikido::Zen::APIClient.new(config: config)
25
+ )
26
+ @started_at = nil
27
+
28
+ @config = config
29
+ @worker = worker
30
+ @api_client = api_client
31
+ @collector = collector
32
+ @detached_agent = detached_agent
33
+ end
34
+
35
+ def started?
36
+ !!@started_at
37
+ end
38
+
39
+ def start!
40
+ @config.logger.info "Starting Aikido agent v#{Aikido::Zen::VERSION}"
41
+
42
+ raise Aikido::ZenError, "Aikido Agent already started!" if started?
43
+ @started_at = Time.now.utc
44
+ @collector.start(at: @started_at)
45
+
46
+ if @config.blocking_mode?
47
+ @config.logger.info "Requests identified as attacks will be blocked"
48
+ else
49
+ @config.logger.warn "Non-blocking mode enabled! No requests will be blocked."
50
+ end
51
+
52
+ if @api_client.can_make_requests?
53
+ @config.logger.info "API Token set! Reporting has been enabled."
54
+ else
55
+ @config.logger.warn "No API Token set! Reporting has been disabled."
56
+ return
57
+ end
58
+
59
+ at_exit { stop! if started? }
60
+
61
+ report(Events::Started.new(time: @started_at)) do |response|
62
+ updated_settings! if Aikido::Zen.runtime_settings.update_from_json(response)
63
+ @config.logger.info "Updated runtime settings."
64
+ rescue => err
65
+ @config.logger.error(err.message)
66
+ end
67
+
68
+ poll_for_setting_updates
69
+
70
+ @worker.delay(@config.initial_heartbeat_delay) do
71
+ send_heartbeat if @collector.stats.any?
72
+ end
73
+ end
74
+
75
+ # Clean up any ongoing threads, and reset the state. Called automatically
76
+ # when the process exits.
77
+ #
78
+ # @return [void]
79
+ def stop!
80
+ @config.logger.info "Stopping Aikido agent"
81
+ @started_at = nil
82
+ @worker.shutdown
83
+ end
84
+
85
+ # Respond to the runtime settings changing after being fetched from the
86
+ # Aikido servers.
87
+ #
88
+ # @return [void]
89
+ def updated_settings!
90
+ if !heartbeats.running?
91
+ heartbeats.start { send_heartbeat }
92
+ elsif heartbeats.stale_settings?
93
+ heartbeats.restart { send_heartbeat }
94
+ end
95
+ end
96
+
97
+ # Given an Attack, report it to the Aikido server, and/or block the request
98
+ # depending on configuration.
99
+ #
100
+ # @param attack [Attack] a detected attack.
101
+ # @return [void]
102
+ #
103
+ # @raise [Aikido::Zen::UnderAttackError] if the firewall is configured
104
+ # to block requests.
105
+ def handle_attack(attack)
106
+ attack.will_be_blocked! if @config.blocking_mode?
107
+
108
+ @config.logger.error(
109
+ format("Zen has %s a %s: %s", attack.blocked? ? "blocked" : "detected", attack.humanized_name, attack.as_json.to_json)
110
+ )
111
+ report(Events::Attack.new(attack: attack)) if @api_client.can_make_requests?
112
+
113
+ @collector.track_attack(attack)
114
+ raise attack if attack.blocked?
115
+ end
116
+
117
+ # Asynchronously reports an Event of any kind to the Aikido dashboard. If
118
+ # given a block, the API response will be passed to the block for handling.
119
+ #
120
+ # @param event [Aikido::Zen::Event]
121
+ # @yieldparam response [Object] the response from the reporting API in case
122
+ # of a successful request.
123
+ #
124
+ # @return [void]
125
+ def report(event)
126
+ @worker.perform do
127
+ response = @api_client.report(event)
128
+ yield response if response && block_given?
129
+ rescue Aikido::Zen::APIError, Aikido::Zen::NetworkError => err
130
+ @config.logger.error(err.message)
131
+ end
132
+ end
133
+
134
+ # @api private
135
+ #
136
+ # Atomically flushes all the stats stored by the agent, and sends a
137
+ # heartbeat event. Scheduled to run automatically on a recurring schedule
138
+ # when reporting is enabled.
139
+ #
140
+ # @param at [Time] the event time. Defaults to now.
141
+ # @return [void]
142
+ # @see Aikido::Zen::RuntimeSettings#heartbeat_interval
143
+ def send_heartbeat(at: Time.now.utc)
144
+ return unless @api_client.can_make_requests?
145
+
146
+ @collector.flush_heartbeats.each do |heartbeat|
147
+ report(heartbeat) do |response|
148
+ updated_settings! if Aikido::Zen.runtime_settings.update_from_json(response)
149
+ @config.logger.info "Updated runtime settings after heartbeat"
150
+ end
151
+ end
152
+ end
153
+
154
+ # @api private
155
+ #
156
+ # Sets up the timer task that polls the Aikido Runtime API for updates to
157
+ # the runtime settings every minute.
158
+ #
159
+ # @return [void]
160
+ # @see Aikido::Zen::RuntimeSettings
161
+ def poll_for_setting_updates
162
+ @worker.every(@config.polling_interval) do
163
+ if @api_client.should_fetch_settings?
164
+ updated_settings! if Aikido::Zen.runtime_settings.update_from_json(@api_client.fetch_settings)
165
+ @config.logger.info "Updated runtime settings after polling"
166
+ end
167
+ end
168
+ end
169
+
170
+ private def heartbeats
171
+ @heartbeats ||= Aikido::Zen::Agent::HeartbeatsManager.new(
172
+ config: @config,
173
+ worker: @worker
174
+ )
175
+ end
176
+ end
177
+ end
178
+
179
+ require_relative "agent/heartbeats_manager"
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require_relative "rate_limiter"
5
+
6
+ module Aikido::Zen
7
+ # Implements all communication with the Aikido servers.
8
+ class APIClient
9
+ def initialize(
10
+ config: Aikido::Zen.config,
11
+ rate_limiter: Aikido::Zen::RateLimiter::Breaker.new,
12
+ system_info: Aikido::Zen.system_info
13
+ )
14
+ @config = config
15
+ @system_info = system_info
16
+ @rate_limiter = rate_limiter
17
+ end
18
+
19
+ # @return [Boolean] whether we have a configured token.
20
+ def can_make_requests?
21
+ @config.api_token.to_s.size > 0
22
+ end
23
+
24
+ # Checks with the Aikido Runtime API the timestamp of the last settings
25
+ # update, and compares against the given value.
26
+ #
27
+ # @param last_updated_at [Time]
28
+ #
29
+ # @return [Boolean]
30
+ # @raise (see #request)
31
+ def should_fetch_settings?(last_updated_at = Aikido::Zen.runtime_settings.updated_at)
32
+ @config.logger.debug("Polling for new runtime settings to fetch")
33
+
34
+ return false unless can_make_requests?
35
+ return true if last_updated_at.nil?
36
+
37
+ response = request(
38
+ Net::HTTP::Get.new("/config", default_headers),
39
+ base_url: @config.realtime_endpoint
40
+ )
41
+
42
+ new_updated_at = Time.at(response["configUpdatedAt"].to_i / 1000)
43
+ new_updated_at > last_updated_at
44
+ end
45
+
46
+ # Fetches the runtime settings from the server. In case of a timeout or
47
+ # other low-lever error, the request will be automatically retried up to two
48
+ # times, after which it will raise an error.
49
+ #
50
+ # @return [Hash] decoded JSON response from the server with the runtime
51
+ # settings.
52
+ # @raise (see #request)
53
+ def fetch_settings
54
+ @config.logger.debug("Fetching new runtime settings")
55
+
56
+ request(Net::HTTP::Get.new("/api/runtime/config", default_headers))
57
+ end
58
+
59
+ # @overload report(event)
60
+ # Reports an event to the server.
61
+ #
62
+ # @param event [Aikido::Zen::Event]
63
+ # @return [void]
64
+ # @raise (see #request)
65
+ #
66
+ # @overload report(settings_updating_event)
67
+ # Reports an event that responds with updated runtime settings, and
68
+ # requires us to update settings afterwards.
69
+ #
70
+ # @param settings_updating_event [Aikido::Zen::Events::Started,
71
+ # Aikido::Zen::Events::Heartbeat]
72
+ # @return (see #fetch_settings)
73
+ # @raise (see #request)
74
+ def report(event)
75
+ event_type = if event.respond_to?(:type)
76
+ event.type
77
+ else
78
+ event[:type]
79
+ end
80
+
81
+ if @rate_limiter.throttle?(event_type)
82
+ @config.logger.error("Not reporting #{event_type.upcase} event due to rate limiting")
83
+ return
84
+ end
85
+
86
+ @config.logger.debug("Reporting #{event_type.upcase} event")
87
+
88
+ req = Net::HTTP::Post.new("/api/runtime/events", default_headers)
89
+ req.content_type = "application/json"
90
+ req.body = if event.respond_to?(:as_json)
91
+ @config.json_encoder.call(event.as_json)
92
+ else
93
+ @config.json_encoder.call(event)
94
+ end
95
+
96
+ request(req)
97
+ rescue Aikido::Zen::RateLimitedError
98
+ @rate_limiter.open!
99
+ raise
100
+ end
101
+
102
+ # Perform an HTTP request against one of our API endpoints, and process the
103
+ # response.
104
+ #
105
+ # @param request [Net::HTTPRequest]
106
+ # @param base_url [URI] which API to use. Defaults to +Config#api_endpoint+.
107
+ #
108
+ # @return [Object] the result of decoding the JSON response from the server.
109
+ #
110
+ # @raise [Aikido::Zen::APIError] in case of a 4XX or 5XX response.
111
+ # @raise [Aikido::Zen::NetworkError] if an error occurs trying to make the
112
+ # request.
113
+ private def request(request, base_url: @config.api_endpoint)
114
+ Net::HTTP.start(base_url.host, base_url.port, http_settings(base_url)) do |http|
115
+ response = http.request(request)
116
+
117
+ case response
118
+ when Net::HTTPSuccess
119
+ @config.json_decoder.call(response.body)
120
+ when Net::HTTPTooManyRequests
121
+ raise RateLimitedError.new(request, response)
122
+ else
123
+ raise APIError.new(request, response)
124
+ end
125
+ end
126
+ rescue Timeout::Error, IOError, SystemCallError, OpenSSL::OpenSSLError => err
127
+ raise NetworkError.new(request, err)
128
+ end
129
+
130
+ private def http_settings(base_url)
131
+ @http_settings ||= {
132
+ use_ssl: base_url.scheme == "https",
133
+ max_retries: 2
134
+ }.merge(@config.api_timeouts)
135
+ end
136
+
137
+ private def default_headers
138
+ @default_headers ||= {
139
+ "Authorization" => @config.api_token,
140
+ "Accept" => "application/json",
141
+ "User-Agent" => "#{@system_info.library_name} v#{@system_info.library_version}"
142
+ }
143
+ end
144
+ end
145
+ end