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.
Files changed (157) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +234 -0
  3. data/README.md +147 -0
  4. data/bin/immunio +5 -0
  5. data/lib/immunio.rb +29 -0
  6. data/lib/immunio/agent.rb +260 -0
  7. data/lib/immunio/authentication.rb +96 -0
  8. data/lib/immunio/blocked_app.rb +38 -0
  9. data/lib/immunio/channel.rb +432 -0
  10. data/lib/immunio/cli.rb +39 -0
  11. data/lib/immunio/context.rb +114 -0
  12. data/lib/immunio/errors.rb +43 -0
  13. data/lib/immunio/immunio_ca.crt +45 -0
  14. data/lib/immunio/logger.rb +87 -0
  15. data/lib/immunio/plugins/action_dispatch.rb +45 -0
  16. data/lib/immunio/plugins/action_view.rb +431 -0
  17. data/lib/immunio/plugins/active_record.rb +707 -0
  18. data/lib/immunio/plugins/active_record_relation.rb +370 -0
  19. data/lib/immunio/plugins/authlogic.rb +80 -0
  20. data/lib/immunio/plugins/csrf.rb +24 -0
  21. data/lib/immunio/plugins/devise.rb +40 -0
  22. data/lib/immunio/plugins/environment_reporter.rb +69 -0
  23. data/lib/immunio/plugins/eval.rb +51 -0
  24. data/lib/immunio/plugins/exception_handler.rb +55 -0
  25. data/lib/immunio/plugins/gems_tracker.rb +5 -0
  26. data/lib/immunio/plugins/haml.rb +36 -0
  27. data/lib/immunio/plugins/http_finisher.rb +50 -0
  28. data/lib/immunio/plugins/http_tracker.rb +203 -0
  29. data/lib/immunio/plugins/io.rb +96 -0
  30. data/lib/immunio/plugins/redirect.rb +42 -0
  31. data/lib/immunio/plugins/warden.rb +66 -0
  32. data/lib/immunio/processor.rb +234 -0
  33. data/lib/immunio/rails.rb +26 -0
  34. data/lib/immunio/request.rb +139 -0
  35. data/lib/immunio/rufus_lua_ext/ref.rb +27 -0
  36. data/lib/immunio/rufus_lua_ext/state.rb +157 -0
  37. data/lib/immunio/rufus_lua_ext/table.rb +137 -0
  38. data/lib/immunio/rufus_lua_ext/utils.rb +13 -0
  39. data/lib/immunio/version.rb +5 -0
  40. data/lib/immunio/vm.rb +291 -0
  41. data/lua-hooks/ext/all.c +78 -0
  42. data/lua-hooks/ext/bitop/README +22 -0
  43. data/lua-hooks/ext/bitop/bit.c +189 -0
  44. data/lua-hooks/ext/extconf.rb +38 -0
  45. data/lua-hooks/ext/libinjection/COPYING +37 -0
  46. data/lua-hooks/ext/libinjection/libinjection.h +65 -0
  47. data/lua-hooks/ext/libinjection/libinjection_html5.c +847 -0
  48. data/lua-hooks/ext/libinjection/libinjection_html5.h +54 -0
  49. data/lua-hooks/ext/libinjection/libinjection_sqli.c +2301 -0
  50. data/lua-hooks/ext/libinjection/libinjection_sqli.h +295 -0
  51. data/lua-hooks/ext/libinjection/libinjection_sqli_data.h +9349 -0
  52. data/lua-hooks/ext/libinjection/libinjection_xss.c +531 -0
  53. data/lua-hooks/ext/libinjection/libinjection_xss.h +21 -0
  54. data/lua-hooks/ext/libinjection/lualib.c +109 -0
  55. data/lua-hooks/ext/lpeg/HISTORY +90 -0
  56. data/lua-hooks/ext/lpeg/lpcap.c +537 -0
  57. data/lua-hooks/ext/lpeg/lpcap.h +43 -0
  58. data/lua-hooks/ext/lpeg/lpcode.c +986 -0
  59. data/lua-hooks/ext/lpeg/lpcode.h +34 -0
  60. data/lua-hooks/ext/lpeg/lpeg-128.gif +0 -0
  61. data/lua-hooks/ext/lpeg/lpeg.html +1429 -0
  62. data/lua-hooks/ext/lpeg/lpprint.c +244 -0
  63. data/lua-hooks/ext/lpeg/lpprint.h +35 -0
  64. data/lua-hooks/ext/lpeg/lptree.c +1238 -0
  65. data/lua-hooks/ext/lpeg/lptree.h +77 -0
  66. data/lua-hooks/ext/lpeg/lptypes.h +149 -0
  67. data/lua-hooks/ext/lpeg/lpvm.c +355 -0
  68. data/lua-hooks/ext/lpeg/lpvm.h +58 -0
  69. data/lua-hooks/ext/lpeg/makefile +55 -0
  70. data/lua-hooks/ext/lpeg/re.html +498 -0
  71. data/lua-hooks/ext/lpeg/test.lua +1409 -0
  72. data/lua-hooks/ext/lua-cmsgpack/CMakeLists.txt +45 -0
  73. data/lua-hooks/ext/lua-cmsgpack/README.md +115 -0
  74. data/lua-hooks/ext/lua-cmsgpack/lua_cmsgpack.c +957 -0
  75. data/lua-hooks/ext/lua-cmsgpack/test.lua +570 -0
  76. data/lua-hooks/ext/lua-snapshot/LICENSE +7 -0
  77. data/lua-hooks/ext/lua-snapshot/Makefile +12 -0
  78. data/lua-hooks/ext/lua-snapshot/README.md +18 -0
  79. data/lua-hooks/ext/lua-snapshot/dump.lua +15 -0
  80. data/lua-hooks/ext/lua-snapshot/snapshot.c +455 -0
  81. data/lua-hooks/ext/lua/COPYRIGHT +34 -0
  82. data/lua-hooks/ext/lua/lapi.c +1087 -0
  83. data/lua-hooks/ext/lua/lapi.h +16 -0
  84. data/lua-hooks/ext/lua/lauxlib.c +652 -0
  85. data/lua-hooks/ext/lua/lauxlib.h +174 -0
  86. data/lua-hooks/ext/lua/lbaselib.c +659 -0
  87. data/lua-hooks/ext/lua/lcode.c +831 -0
  88. data/lua-hooks/ext/lua/lcode.h +76 -0
  89. data/lua-hooks/ext/lua/ldblib.c +398 -0
  90. data/lua-hooks/ext/lua/ldebug.c +638 -0
  91. data/lua-hooks/ext/lua/ldebug.h +33 -0
  92. data/lua-hooks/ext/lua/ldo.c +519 -0
  93. data/lua-hooks/ext/lua/ldo.h +57 -0
  94. data/lua-hooks/ext/lua/ldump.c +164 -0
  95. data/lua-hooks/ext/lua/lfunc.c +174 -0
  96. data/lua-hooks/ext/lua/lfunc.h +34 -0
  97. data/lua-hooks/ext/lua/lgc.c +710 -0
  98. data/lua-hooks/ext/lua/lgc.h +110 -0
  99. data/lua-hooks/ext/lua/linit.c +38 -0
  100. data/lua-hooks/ext/lua/liolib.c +556 -0
  101. data/lua-hooks/ext/lua/llex.c +463 -0
  102. data/lua-hooks/ext/lua/llex.h +81 -0
  103. data/lua-hooks/ext/lua/llimits.h +128 -0
  104. data/lua-hooks/ext/lua/lmathlib.c +263 -0
  105. data/lua-hooks/ext/lua/lmem.c +86 -0
  106. data/lua-hooks/ext/lua/lmem.h +49 -0
  107. data/lua-hooks/ext/lua/loadlib.c +705 -0
  108. data/lua-hooks/ext/lua/loadlib_rel.c +760 -0
  109. data/lua-hooks/ext/lua/lobject.c +214 -0
  110. data/lua-hooks/ext/lua/lobject.h +381 -0
  111. data/lua-hooks/ext/lua/lopcodes.c +102 -0
  112. data/lua-hooks/ext/lua/lopcodes.h +268 -0
  113. data/lua-hooks/ext/lua/loslib.c +243 -0
  114. data/lua-hooks/ext/lua/lparser.c +1339 -0
  115. data/lua-hooks/ext/lua/lparser.h +82 -0
  116. data/lua-hooks/ext/lua/lstate.c +214 -0
  117. data/lua-hooks/ext/lua/lstate.h +169 -0
  118. data/lua-hooks/ext/lua/lstring.c +111 -0
  119. data/lua-hooks/ext/lua/lstring.h +31 -0
  120. data/lua-hooks/ext/lua/lstrlib.c +871 -0
  121. data/lua-hooks/ext/lua/ltable.c +588 -0
  122. data/lua-hooks/ext/lua/ltable.h +40 -0
  123. data/lua-hooks/ext/lua/ltablib.c +287 -0
  124. data/lua-hooks/ext/lua/ltm.c +75 -0
  125. data/lua-hooks/ext/lua/ltm.h +54 -0
  126. data/lua-hooks/ext/lua/lua.c +392 -0
  127. data/lua-hooks/ext/lua/lua.def +131 -0
  128. data/lua-hooks/ext/lua/lua.h +388 -0
  129. data/lua-hooks/ext/lua/lua.rc +28 -0
  130. data/lua-hooks/ext/lua/lua_dll.rc +26 -0
  131. data/lua-hooks/ext/lua/luac.c +200 -0
  132. data/lua-hooks/ext/lua/luac.rc +1 -0
  133. data/lua-hooks/ext/lua/luaconf.h +763 -0
  134. data/lua-hooks/ext/lua/luaconf.h.in +724 -0
  135. data/lua-hooks/ext/lua/luaconf.h.orig +763 -0
  136. data/lua-hooks/ext/lua/lualib.h +53 -0
  137. data/lua-hooks/ext/lua/lundump.c +227 -0
  138. data/lua-hooks/ext/lua/lundump.h +36 -0
  139. data/lua-hooks/ext/lua/lvm.c +767 -0
  140. data/lua-hooks/ext/lua/lvm.h +36 -0
  141. data/lua-hooks/ext/lua/lzio.c +82 -0
  142. data/lua-hooks/ext/lua/lzio.h +67 -0
  143. data/lua-hooks/ext/lua/print.c +227 -0
  144. data/lua-hooks/ext/luautf8/README.md +152 -0
  145. data/lua-hooks/ext/luautf8/lutf8lib.c +1274 -0
  146. data/lua-hooks/ext/luautf8/unidata.h +3064 -0
  147. data/lua-hooks/lib/boot.lua +254 -0
  148. data/lua-hooks/lib/encode.lua +4 -0
  149. data/lua-hooks/lib/lexers/LICENSE +21 -0
  150. data/lua-hooks/lib/lexers/bash.lua +134 -0
  151. data/lua-hooks/lib/lexers/bash_dqstr.lua +62 -0
  152. data/lua-hooks/lib/lexers/css.lua +216 -0
  153. data/lua-hooks/lib/lexers/html.lua +106 -0
  154. data/lua-hooks/lib/lexers/javascript.lua +68 -0
  155. data/lua-hooks/lib/lexers/lexer.lua +1575 -0
  156. data/lua-hooks/lib/lexers/markers.lua +33 -0
  157. metadata +308 -0
@@ -0,0 +1,40 @@
1
+ # Register callbacks using Devise internals.
2
+
3
+ begin
4
+ require "devise"
5
+ rescue LoadError # rubocop:disable Lint/HandleExceptions
6
+ # Ignore
7
+ end
8
+
9
+ if defined? Devise
10
+ module Immunio
11
+ # Hook into password recovery feature to trigger the `framework_password_reset` hook.
12
+ module DeviseRecoverableHooks
13
+ extend ActiveSupport::Concern
14
+
15
+ included do
16
+ alias_method_chain :send_reset_password_instructions, :immunio
17
+ end
18
+
19
+ def send_reset_password_instructions_with_immunio(attributes={})
20
+ Request.time "plugin", "#{Module.nesting[0]}::#{__method__}" do
21
+ Immunio.logger.debug "Devise instrumentation fired for send_reset_password_instructions"
22
+
23
+ recoverable = find_or_initialize_with_errors(reset_password_keys, attributes, :not_found)
24
+
25
+ if recoverable.persisted? # Found
26
+ Immunio.password_reset user_record: recoverable, plugin: 'devise'
27
+ else
28
+ Immunio.failed_password_reset email: recoverable.email, plugin: 'devise'
29
+ end
30
+
31
+ Request.pause "plugin", "#{Module.nesting[0]}::#{__method__}" do
32
+ send_reset_password_instructions_without_immunio(attributes)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ Devise::Models::Recoverable::ClassMethods.send :include, Immunio::DeviseRecoverableHooks
40
+ end
@@ -0,0 +1,69 @@
1
+ # Report environment data on the first request.
2
+ #
3
+ # XXX: Completely untested outside of MRI (official Ruby runtime) 1.9.3 and up!
4
+
5
+ module Immunio
6
+ class EnvironmentReporter
7
+ def initialize(app)
8
+ @app = app
9
+
10
+ # No need for mutex, reporting environment is idempotent
11
+ @reported = false
12
+ end
13
+
14
+ def call(env)
15
+ response = @app.call(env)
16
+
17
+ # Report gem usage at the end so that everything as been loaded.
18
+ Request.time "plugin", "#{Module.nesting[0]}::#{__method__}" do
19
+ report if !@reported
20
+ end
21
+
22
+ response
23
+ end
24
+
25
+ def runtime_name
26
+ if Object.const_defined?(:RUBY_DESCRIPTION) && RUBY_DESCRIPTION =~ /Enterprise/
27
+ 'ree'
28
+ elsif Object.const_defined? :RUBY_ENGINE
29
+ RUBY_ENGINE.to_s
30
+ else
31
+ 'unknown'
32
+ end
33
+ end
34
+
35
+ def runtime_version
36
+ case runtime_name
37
+ when 'ruby' then RUBY_VERSION
38
+ when 'jruby' then JRUBY_VERSION
39
+ when 'rbx' then Rubinius::VERSION
40
+ else 'unknown'
41
+ end
42
+ end
43
+
44
+ def report
45
+ @reported = true
46
+
47
+ info = {
48
+ runtime: {
49
+ name: runtime_name,
50
+ version: runtime_version
51
+ },
52
+ language: {
53
+ name: 'ruby',
54
+ version: RUBY_VERSION
55
+ },
56
+ platform: {
57
+ description: RUBY_PLATFORM
58
+ },
59
+ dependencies: {}
60
+ }
61
+
62
+ Gem.loaded_specs.each_pair do |name, spec|
63
+ info[:dependencies][name] = spec.version.to_s
64
+ end
65
+
66
+ Immunio.agent.environment = info
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,51 @@
1
+ require_relative '../context'
2
+ # We use DebugInspector to execute eval() in the callers frame.
3
+
4
+ require 'binding_of_caller'
5
+ module Immunio
6
+ module KernelEvalHook
7
+ extend ActiveSupport::Concern
8
+
9
+ def self.extended(base)
10
+ base.singleton_class.alias_method_chain :eval, :immunio
11
+ end
12
+
13
+ def self.included(base)
14
+ base.alias_method_chain :eval, :immunio
15
+ end
16
+
17
+ protected
18
+ def eval_with_immunio(*args)
19
+ Immunio.logger.debug {"Eval_with_immunio with args: #{args}"}
20
+ Request.time "plugin", "#{Module.nesting[0]}::#{__method__}" do
21
+ strict_context, loose_context, stack, loose_stack = Immunio::Context.context()
22
+ Immunio.run_hook! "eval", "eval",
23
+ parameters: args,
24
+ context_key: loose_context,
25
+ stack: loose_stack
26
+ Request.pause "plugin", "#{Module.nesting[0]}::#{__method__}" do
27
+ if args.length == 1 then
28
+ # eval call did not include a binding, so we eval it in the caller's binding
29
+ # NOTE: the 7 here is the number of frames above us the caller was. Changing
30
+ # the call stack will break this...
31
+ rval = eval_without_immunio(args[0], binding.of_caller(7))
32
+ # XXX We can and should log here, but it breaks a ruby test (atom_feed_helper)
33
+ #Immunio.logger.debug {"Eval_without_immunio (binding 7) returned: #{rval.inspect}\nEval stack was: #{stack}"}
34
+ else
35
+ # eval call included a binding, so we still eval in the binding
36
+ rval = eval_without_immunio(*args)
37
+ # XXX We can and should log here, but it breaks a ruby test (atom_feed_helper)
38
+ #Immunio.logger.debug {"Eval_without_immunio returned: #{rval.inspect}"}
39
+ end
40
+ rval
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ if Immunio::agent.plugin_enabled?("eval") then
48
+ Kernel.send :include, Immunio::KernelEvalHook
49
+ Kernel.extend Immunio::KernelEvalHook
50
+ Immunio.logger.debug "Eval: All hooks installed."
51
+ end
@@ -0,0 +1,55 @@
1
+ require_relative "../blocked_app"
2
+ require_relative "../errors"
3
+
4
+ module Immunio
5
+ # Rack middleware trigger proper hook whenever an exception is raised in the app.
6
+ class ExceptionHandler
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(env)
12
+ @app.call(env)
13
+ rescue RequestBlocked
14
+ Request.time "plugin", "#{Module.nesting[0]}::#{__method__}[RequestBlocked]" do
15
+ # Avoid bubbling exception to logging middlewares.
16
+ Immunio.blocked_app.call(env)
17
+ end
18
+ rescue OverrideResponse => override
19
+ status, headers, body = Immunio.override_response.call(env, override)
20
+
21
+ Immunio.run_hook "http_tracker", "http_response_start",
22
+ status: status, headers: headers
23
+ [status, headers, body]
24
+ rescue StandardError => e
25
+ Request.time "plugin", "#{Module.nesting[0]}::#{__method__}[#{e.class}]" do
26
+ # Check for template, DB errors and such generated by the agent.
27
+ original_exception = unwrap_exception(e)
28
+ if RequestBlocked === original_exception || original_exception.message =~ /^Immunio::RequestBlocked:/
29
+ return Immunio.blocked_app.call(env)
30
+ end
31
+ if OverrideResponse === original_exception # We can't do anything with just the message:
32
+ status, headers, body = Immunio.override_response.call(env, original_exception)
33
+
34
+ Immunio.run_hook "http_tracker", "http_response_start",
35
+ status: status, headers: headers
36
+ return [status, headers, body]
37
+ end
38
+
39
+ # A real app exception, not generated by the agent.
40
+ Immunio.run_hook! "exception_handler", "exception", exception: "#{e.class.name}: #{e}"
41
+
42
+ # Re-raise
43
+ raise
44
+ end
45
+ end
46
+
47
+ # Unwrap the innermost original exception.
48
+ def unwrap_exception(e)
49
+ while e.respond_to? :original_exception
50
+ e = e.original_exception
51
+ end
52
+ e
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,5 @@
1
+ require_relative 'environment_reporter'
2
+
3
+ module Immunio
4
+ GemsTracker = EnvironmentReporter
5
+ end
@@ -0,0 +1,36 @@
1
+ # Haml compiles templates to something like:
2
+ #
3
+ #begin;extend Haml::Helpers;_hamlout = @haml_buffer = Haml::Buffer.new(haml_buffer, {:autoclose=>["area", "base", "basefont", "br", "col", "command", "embed", "frame", "hr", "img", "input", "isindex", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr"], :preserve=>["textarea", "pre", "code"], :attr_wrapper=>"'", :ugly=>true, :format=>:html5, :encoding=>"UTF-8", :escape_html=>true, :escape_attrs=>true, :hyphenate_data_attrs=>true, :cdata=>false});_erbout = _hamlout.buffer;@output_buffer = output_buffer ||= ActionView::OutputBuffer.new rescue nil;;_hamlout.buffer << "<script>console.log('test')</script>\n";::Haml::Util.html_safe(_erbout);ensure;@haml_buffer = @haml_buffer.upper if @haml_buffer;end;
4
+ #
5
+ # In there is a call to ::Haml::Util.html_safe, which calls the real html_safe.
6
+ # But lots of haml code paths call ::Haml::Util.html_safe, so we can't mark it
7
+ # as safe. Instead, we need to modify the compiled template code to call the
8
+ # method in a marked-safe context.
9
+ #
10
+ # But haml doesn't make it easy, as it alias-method-chains its own methods. This
11
+ # module continues the chain to subvert the
12
+ # precompiled_method_return_value_with_haml_xss method and instead call our own
13
+ # method, which calls html_safe while in a safe context.
14
+
15
+ module Immunio
16
+ module Haml
17
+ module Compiler
18
+ extend ActiveSupport::Concern
19
+
20
+ included do
21
+ alias_method :precompiled_method_return_value_without_haml_xss_without_immunio, :precompiled_method_return_value_without_haml_xss
22
+ alias_method :precompiled_method_return_value, :precompiled_method_return_value_with_haml_xss_with_immunio
23
+ end
24
+
25
+ def self.immunio_html_safe(string)
26
+ Immunio::UnsafeBufferDetection.in_safe_context { ::Haml::Util.html_safe(string) }
27
+ end
28
+
29
+ def precompiled_method_return_value_with_haml_xss_with_immunio
30
+ "Immunio::Haml::Compiler.immunio_html_safe(#{precompiled_method_return_value_without_haml_xss_without_immunio})"
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ ::Haml::Compiler.send :include, Immunio::Haml::Compiler if defined? ::Haml::Compiler
@@ -0,0 +1,50 @@
1
+ require "delegate"
2
+
3
+ module Immunio
4
+ # Rack middleware running at the very end of the stack to finish HTTP requests.
5
+ class HTTPFinisher
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ status, headers, body = @app.call(env)
12
+ if Request.current
13
+ Immunio.logger.debug "Finishing request in HTTPFinisher"
14
+ [status, headers, BodyWrapper.new(body)]
15
+ else
16
+ [status, headers, body]
17
+ end
18
+ end
19
+ end
20
+
21
+ class BodyWrapper < SimpleDelegator
22
+ def initialize(body)
23
+ super body
24
+ @body = body
25
+ end
26
+
27
+ def each
28
+ begin
29
+ @body.each do |chunk|
30
+ Immunio.run_hook "http_finisher", "http_response_body_chunk",
31
+ chunk: chunk
32
+ yield chunk
33
+ end
34
+ rescue RequestBlocked
35
+ # We need to catch this exception here as access to static files is
36
+ # deferred. If one of our hooks, such as file_io throws when the Rake
37
+ # stack has unwound this far it will kill the request with a 500
38
+ yield "Request Blocked"
39
+ end
40
+ Immunio.run_hook "http_finisher", "http_request_finish"
41
+
42
+ # Reset current request
43
+ Immunio.finish_request
44
+ end
45
+
46
+ def respond_to?(*args)
47
+ @body.respond_to?(*args)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,203 @@
1
+ require_relative "../blocked_app"
2
+ require_relative "../errors"
3
+
4
+ module Immunio
5
+ # Rack middleware tracking HTTP requests and responses and triggers the proper hooks.
6
+ class HTTPTracker
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(env)
12
+ request = Request.new(env)
13
+ request.time "plugin", "#{Module.nesting[0]}::#{__method__}" do
14
+ Immunio.logger.debug "Creating new request in HTTPTracker"
15
+ Immunio.new_request(request)
16
+
17
+ Immunio.run_hook! "http_tracker", "http_request_start", meta_from_env(env)
18
+
19
+ env['rack.input'] = InputWrapper.new(env['rack.input'])
20
+
21
+ status, headers, body = Request.pause "plugin", "#{Module.nesting[0]}::#{__method__}" do
22
+ @app.call(env)
23
+ end
24
+
25
+ # Run hook for the session only if it was loaded
26
+ session = env["rack.session"]
27
+ if session_was_loaded?(session)
28
+ Immunio.run_hook! "http_tracker", "framework_session",
29
+ session_id: extract_session_id(session)
30
+ end
31
+
32
+ # Immunio expects response headers as a list of tuples.
33
+ list_headers = headers_to_list(headers)
34
+
35
+ result = Immunio.run_hook! "http_tracker", "http_response_start",
36
+ status: status, headers: list_headers
37
+
38
+ # If new headers are specified, convert them back to the Ruby hash format.
39
+ if result["headers"] != nil
40
+ # The new_headers completely replace the originals.
41
+ headers = list_to_headers(result["headers"].to_a)
42
+ end
43
+
44
+ # Send the response
45
+ [status, headers, body]
46
+ end
47
+
48
+ rescue RequestBlocked
49
+ Request.time "plugin", "#{Module.nesting[0]}::#{__method__}[RequestBlocked]" do
50
+ status, headers, body = Immunio.blocked_app.call(env)
51
+ # Do not allow blocking the request here
52
+ Immunio.run_hook "http_tracker", "http_response_start",
53
+ status: status, headers: headers
54
+
55
+ [status, headers, body]
56
+ end
57
+ rescue OverrideResponse => override
58
+ status, headers, body = Immunio.override_response.call(env, override)
59
+
60
+ Immunio.run_hook "http_tracker", "http_response_start",
61
+ status: status, headers: headers
62
+ [status, headers, body]
63
+ end
64
+
65
+ private
66
+ def headers_to_list(headers)
67
+ list_headers = []
68
+ headers.each do |name, value|
69
+ # Ruby treats the `Set-Cookie` header specially. If there are multiple
70
+ # Set-Cookie headers to send, they are joined into a single field,
71
+ # separated by line-feeds.
72
+ if name == "Set-Cookie"
73
+ value.split("\n").each do |cookie_val|
74
+ list_headers.push(["Set-Cookie", cookie_val])
75
+ end
76
+ else
77
+ list_headers.push([name, value])
78
+ end
79
+ end
80
+ list_headers
81
+ end
82
+
83
+ def list_to_headers(list)
84
+ new_headers = {}
85
+ list.each do |name, value|
86
+ # If this header is already in `new_headers`, append to the
87
+ # existing value with a linefeed separator.
88
+ if new_headers.has_key?(name)
89
+ new_headers[name] += ("\n" + value)
90
+ else
91
+ new_headers[name] = value
92
+ end
93
+ end
94
+ new_headers
95
+ end
96
+
97
+ def meta_from_env(env)
98
+ request = Rack::Request.new(env)
99
+
100
+ # Extract request headers from `env`.
101
+ headers = env.select { |k| k.starts_with? "HTTP_" }.
102
+ each_with_object({}) { |(k, v), h| h.store k[5..-1].downcase.tr('_', '-'), v }
103
+
104
+ # Determine scheme (http://www.rubydoc.info/github/rack/rack/master/file/SPEC)
105
+ # There are also some HTTP headers from proxies that may affect the
106
+ # scheme seen by the end user. We process those in the hooks.
107
+ scheme = env["rack.url_scheme"]
108
+ if env["HTTPS"] == "on"
109
+ # Some servers will set the HTTPS var explicity. If set, use it
110
+ scheme = "https"
111
+ end
112
+
113
+ # Determine the route name in controller#action format:
114
+ route_name = nil
115
+
116
+ if defined?(Rails.application)
117
+ begin
118
+ path = request.env['PATH_INFO']
119
+ method = request.env['REQUEST_METHOD'].downcase.to_sym
120
+ url = Rails.application.routes.recognize_path(path, method: method)
121
+ route_name = "#{url[:controller]}##{url[:action]}"
122
+ rescue ActionController::RoutingError
123
+ route_name = nil
124
+ end
125
+ end
126
+
127
+ {
128
+ protocol: env["SERVER_PROTOCOL"],
129
+ scheme: scheme,
130
+ uri: env["REQUEST_URI"],
131
+ server_name: env["SERVER_NAME"],
132
+ # SERVER_ADDR is non-standard, but rack uses it as a fallback, so
133
+ # include it here as well so we can access it from Lua.
134
+ server_addr: env["SERVER_ADDR"],
135
+ server_port: env["SERVER_PORT"],
136
+ route_name: route_name,
137
+ querystring: request.query_string,
138
+ method: request.request_method,
139
+ path: request.path_info,
140
+ socket_ip: request.ip,
141
+ socket_port: request.port,
142
+ headers: headers
143
+ }
144
+ end
145
+
146
+ def session_was_loaded?(session)
147
+ session && (session.respond_to?(:loaded?) ? session.loaded? : true)
148
+ end
149
+
150
+ def extract_session_id(session)
151
+ case
152
+ when session.respond_to?(:id)
153
+ session.id
154
+ when session[:id]
155
+ session[:id]
156
+ when session[:session_id]
157
+ session[:session_id]
158
+ else
159
+ nil
160
+ end
161
+ end
162
+ end
163
+
164
+ class InputWrapper < SimpleDelegator
165
+ def initialize(input)
166
+ super input
167
+ @input = input
168
+ end
169
+
170
+ def gets(*)
171
+ v = super
172
+ Request.time "plugin", "#{Module.nesting[0]}::#{__method__}" do
173
+ report_chunk v
174
+ end
175
+ v
176
+ end
177
+
178
+ def read(*)
179
+ v = super
180
+ Request.time "plugin", "#{Module.nesting[0]}::#{__method__}" do
181
+ report_chunk v
182
+ end
183
+ v
184
+ end
185
+
186
+ def each
187
+ Request.time "plugin", "#{Module.nesting[0]}::#{__method__}" do
188
+ @input.each do |chunk|
189
+ report_chunk chunk
190
+ Request.pause "plugin", "#{Module.nesting[0]}::#{__method__}" do
191
+ yield chunk
192
+ end
193
+ end
194
+ end
195
+ end
196
+
197
+ private
198
+ def report_chunk(chunk)
199
+ Immunio.run_hook! "http_tracker", "http_request_body_chunk",
200
+ chunk: chunk
201
+ end
202
+ end
203
+ end