ultra-safe-rb 0.0.1

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 (53) hide show
  1. checksums.yaml +7 -0
  2. data/better_errors-2.10.1/CHANGELOG.md +3 -0
  3. data/better_errors-2.10.1/Gemfile +11 -0
  4. data/better_errors-2.10.1/LICENSE +22 -0
  5. data/better_errors-2.10.1/README.md +163 -0
  6. data/better_errors-2.10.1/better_errors.gemspec +48 -0
  7. data/better_errors-2.10.1/gemfiles/pry010.gemfile +10 -0
  8. data/better_errors-2.10.1/gemfiles/pry011.gemfile +9 -0
  9. data/better_errors-2.10.1/gemfiles/pry09.gemfile +9 -0
  10. data/better_errors-2.10.1/gemfiles/rack.gemfile +8 -0
  11. data/better_errors-2.10.1/gemfiles/rack_boc.gemfile +9 -0
  12. data/better_errors-2.10.1/gemfiles/rails42.gemfile +10 -0
  13. data/better_errors-2.10.1/gemfiles/rails42_boc.gemfile +11 -0
  14. data/better_errors-2.10.1/gemfiles/rails42_haml.gemfile +11 -0
  15. data/better_errors-2.10.1/gemfiles/rails50.gemfile +9 -0
  16. data/better_errors-2.10.1/gemfiles/rails50_boc.gemfile +10 -0
  17. data/better_errors-2.10.1/gemfiles/rails50_haml.gemfile +10 -0
  18. data/better_errors-2.10.1/gemfiles/rails51.gemfile +9 -0
  19. data/better_errors-2.10.1/gemfiles/rails51_boc.gemfile +10 -0
  20. data/better_errors-2.10.1/gemfiles/rails51_haml.gemfile +10 -0
  21. data/better_errors-2.10.1/gemfiles/rails52.gemfile +9 -0
  22. data/better_errors-2.10.1/gemfiles/rails52_boc.gemfile +10 -0
  23. data/better_errors-2.10.1/gemfiles/rails52_haml.gemfile +10 -0
  24. data/better_errors-2.10.1/gemfiles/rails60.gemfile +8 -0
  25. data/better_errors-2.10.1/gemfiles/rails60_boc.gemfile +9 -0
  26. data/better_errors-2.10.1/gemfiles/rails60_haml.gemfile +9 -0
  27. data/better_errors-2.10.1/gemfiles/rails61.gemfile +8 -0
  28. data/better_errors-2.10.1/gemfiles/rails61_boc.gemfile +9 -0
  29. data/better_errors-2.10.1/gemfiles/rails61_haml.gemfile +9 -0
  30. data/better_errors-2.10.1/lib/better_errors/code_formatter/html.rb +40 -0
  31. data/better_errors-2.10.1/lib/better_errors/code_formatter/text.rb +14 -0
  32. data/better_errors-2.10.1/lib/better_errors/code_formatter.rb +52 -0
  33. data/better_errors-2.10.1/lib/better_errors/editor.rb +103 -0
  34. data/better_errors-2.10.1/lib/better_errors/error_page.rb +169 -0
  35. data/better_errors-2.10.1/lib/better_errors/error_page_style.rb +43 -0
  36. data/better_errors-2.10.1/lib/better_errors/exception_extension.rb +17 -0
  37. data/better_errors-2.10.1/lib/better_errors/exception_hint.rb +29 -0
  38. data/better_errors-2.10.1/lib/better_errors/inspectable_value.rb +45 -0
  39. data/better_errors-2.10.1/lib/better_errors/middleware.rb +237 -0
  40. data/better_errors-2.10.1/lib/better_errors/rails.rb +28 -0
  41. data/better_errors-2.10.1/lib/better_errors/raised_exception.rb +89 -0
  42. data/better_errors-2.10.1/lib/better_errors/repl/basic.rb +20 -0
  43. data/better_errors-2.10.1/lib/better_errors/repl/pry.rb +88 -0
  44. data/better_errors-2.10.1/lib/better_errors/repl.rb +32 -0
  45. data/better_errors-2.10.1/lib/better_errors/stack_frame.rb +136 -0
  46. data/better_errors-2.10.1/lib/better_errors/templates/main.css +1 -0
  47. data/better_errors-2.10.1/lib/better_errors/templates/main.erb +431 -0
  48. data/better_errors-2.10.1/lib/better_errors/templates/text.erb +24 -0
  49. data/better_errors-2.10.1/lib/better_errors/templates/variable_info.erb +82 -0
  50. data/better_errors-2.10.1/lib/better_errors/version.rb +4 -0
  51. data/better_errors-2.10.1/lib/better_errors.rb +144 -0
  52. data/ultra-safe-rb.gemspec +12 -0
  53. metadata +92 -0
@@ -0,0 +1,103 @@
1
+ require "uri"
2
+
3
+ module BetterErrors
4
+ class Editor
5
+ KNOWN_EDITORS = [
6
+ { symbols: [:atom], sniff: /atom/i, url: "atom://core/open/file?filename=%{file}&line=%{line}" },
7
+ { symbols: [:emacs, :emacsclient], sniff: /emacs/i, url: "emacs://open?url=file://%{file}&line=%{line}" },
8
+ { symbols: [:idea], sniff: /idea/i, url: "idea://open?file=%{file}&line=%{line}" },
9
+ { symbols: [:macvim, :mvim], sniff: /vim/i, url: "mvim://open?url=file://%{file_unencoded}&line=%{line}" },
10
+ { symbols: [:rubymine], sniff: /mine/i, url: "x-mine://open?file=%{file}&line=%{line}" },
11
+ { symbols: [:sublime, :subl, :st], sniff: /subl/i, url: "subl://open?url=file://%{file}&line=%{line}" },
12
+ { symbols: [:textmate, :txmt, :tm], sniff: /mate/i, url: "txmt://open?url=file://%{file}&line=%{line}" },
13
+ { symbols: [:vscode, :code], sniff: /code/i, url: "vscode://file/%{file}:%{line}" },
14
+ { symbols: [:vscodium, :codium], sniff: /codium/i, url: "vscodium://file/%{file}:%{line}" },
15
+ ]
16
+
17
+ def self.for_formatting_string(formatting_string)
18
+ new proc { |file, line|
19
+ formatting_string % { file: URI.encode_www_form_component(file), file_unencoded: file, line: line }
20
+ }
21
+ end
22
+
23
+ def self.for_proc(url_proc)
24
+ new url_proc
25
+ end
26
+
27
+ # Automatically sniffs a default editor preset based on
28
+ # environment variables.
29
+ #
30
+ # @return [Symbol]
31
+ def self.default_editor
32
+ editor_from_environment_formatting_string ||
33
+ editor_from_environment_editor ||
34
+ editor_from_symbol(:textmate)
35
+ end
36
+
37
+ def self.editor_from_environment_editor
38
+ if ENV["BETTER_ERRORS_EDITOR"]
39
+ editor = editor_from_command(ENV["BETTER_ERRORS_EDITOR"])
40
+ return editor if editor
41
+ puts "BETTER_ERRORS_EDITOR environment variable is not recognized as a supported Better Errors editor."
42
+ end
43
+ if ENV["EDITOR"]
44
+ editor = editor_from_command(ENV["EDITOR"])
45
+ return editor if editor
46
+ puts "EDITOR environment variable is not recognized as a supported Better Errors editor. Using TextMate by default."
47
+ else
48
+ puts "Since there is no EDITOR or BETTER_ERRORS_EDITOR environment variable, using Textmate by default."
49
+ end
50
+ end
51
+
52
+ def self.editor_from_command(editor_command)
53
+ env_preset = KNOWN_EDITORS.find { |preset| editor_command =~ preset[:sniff] }
54
+ for_formatting_string(env_preset[:url]) if env_preset
55
+ end
56
+
57
+ def self.editor_from_environment_formatting_string
58
+ return unless ENV['BETTER_ERRORS_EDITOR_URL']
59
+
60
+ for_formatting_string(ENV['BETTER_ERRORS_EDITOR_URL'])
61
+ end
62
+
63
+ def self.editor_from_symbol(symbol)
64
+ KNOWN_EDITORS.each do |preset|
65
+ return for_formatting_string(preset[:url]) if preset[:symbols].include?(symbol)
66
+ end
67
+ end
68
+
69
+ def initialize(url_proc)
70
+ @url_proc = url_proc
71
+ end
72
+
73
+ def url(raw_path, line)
74
+ if virtual_path && raw_path.start_with?(virtual_path)
75
+ if host_path
76
+ file = raw_path.sub(%r{\A#{virtual_path}}, host_path)
77
+ else
78
+ file = raw_path.sub(%r{\A#{virtual_path}/}, '')
79
+ end
80
+ else
81
+ file = raw_path
82
+ end
83
+
84
+ url_proc.call(file, line)
85
+ end
86
+
87
+ def scheme
88
+ url('/fake', 42).sub(/:.*/, ':')
89
+ end
90
+
91
+ private
92
+
93
+ attr_reader :url_proc
94
+
95
+ def virtual_path
96
+ @virtual_path ||= ENV['BETTER_ERRORS_VIRTUAL_PATH']
97
+ end
98
+
99
+ def host_path
100
+ @host_path ||= ENV['BETTER_ERRORS_HOST_PATH']
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,169 @@
1
+ require "cgi"
2
+ require "json"
3
+ require "securerandom"
4
+ require "rouge"
5
+ require "better_errors/error_page_style"
6
+
7
+ module BetterErrors
8
+ # @private
9
+ class ErrorPage
10
+ VariableInfo = Struct.new(:frame, :editor_url, :rails_params, :rack_session, :start_time)
11
+
12
+ def self.template_path(template_name)
13
+ File.expand_path("../templates/#{template_name}.erb", __FILE__)
14
+ end
15
+
16
+ def self.template(template_name)
17
+ Erubi::Engine.new(File.read(template_path(template_name)), escape: true)
18
+ end
19
+
20
+ def self.render_template(template_name, locals)
21
+ locals.send(:eval, self.template(template_name).src)
22
+ rescue => e
23
+ # Fix the backtrace, which doesn't identify the template that failed (within Better Errors).
24
+ # We don't know the line number, so just injecting the template path has to be enough.
25
+ e.backtrace.unshift "#{self.template_path(template_name)}:0"
26
+ raise
27
+ end
28
+
29
+ attr_reader :exception, :env, :repls
30
+
31
+ def initialize(exception, env)
32
+ @exception = RaisedException.new(exception)
33
+ @env = env
34
+ @start_time = Time.now.to_f
35
+ @repls = []
36
+ end
37
+
38
+ def id
39
+ @id ||= SecureRandom.hex(8)
40
+ end
41
+
42
+ def render_main(csrf_token, csp_nonce)
43
+ frame = backtrace_frames[0]
44
+ first_frame_variable_info = VariableInfo.new(frame, editor_url(frame), rails_params, rack_session, Time.now.to_f)
45
+ self.class.render_template('main', binding)
46
+ end
47
+
48
+ def render_text
49
+ self.class.render_template('text', binding)
50
+ end
51
+
52
+ def do_variables(opts)
53
+ index = opts["index"].to_i
54
+ frame = backtrace_frames[index]
55
+ variable_info = VariableInfo.new(frame, editor_url(frame), rails_params, rack_session, Time.now.to_f)
56
+ { html: self.class.render_template("variable_info", variable_info) }
57
+ end
58
+
59
+ def do_eval(opts)
60
+ index = opts["index"].to_i
61
+ code = opts["source"]
62
+
63
+ unless (binding = backtrace_frames[index].frame_binding)
64
+ return { error: "REPL unavailable in this stack frame" }
65
+ end
66
+
67
+ @repls[index] ||= REPL.provider.new(binding, exception)
68
+
69
+ eval_and_respond(index, code)
70
+ end
71
+
72
+ def backtrace_frames
73
+ exception.backtrace
74
+ end
75
+
76
+ def exception_type
77
+ exception.type
78
+ end
79
+
80
+ def exception_message
81
+ exception.message.strip.gsub(/(\r?\n\s*\r?\n)+/, "\n")
82
+ end
83
+
84
+ def exception_hint
85
+ exception.hint
86
+ end
87
+
88
+ def active_support_actions
89
+ return [] unless defined?(ActiveSupport::ActionableError)
90
+
91
+ ActiveSupport::ActionableError.actions(exception.type)
92
+ end
93
+
94
+ def action_dispatch_action_endpoint
95
+ return unless defined?(ActionDispatch::ActionableExceptions)
96
+
97
+ ActionDispatch::ActionableExceptions.endpoint
98
+ end
99
+
100
+ def application_frames
101
+ backtrace_frames.select(&:application?)
102
+ end
103
+
104
+ def first_frame
105
+ application_frames.first || backtrace_frames.first
106
+ end
107
+
108
+ private
109
+
110
+ def editor_url(frame)
111
+ BetterErrors.editor.url(frame.filename, frame.line)
112
+ end
113
+
114
+ def rack_session
115
+ env['rack.session']
116
+ end
117
+
118
+ def rails_params
119
+ env['action_dispatch.request.parameters']
120
+ end
121
+
122
+ def uri_prefix
123
+ env["SCRIPT_NAME"] || ""
124
+ end
125
+
126
+ def request_path
127
+ env["PATH_INFO"]
128
+ end
129
+
130
+ def self.html_formatted_code_block(frame)
131
+ CodeFormatter::HTML.new(frame.filename, frame.line).output
132
+ end
133
+
134
+ def self.text_formatted_code_block(frame)
135
+ CodeFormatter::Text.new(frame.filename, frame.line).output
136
+ end
137
+
138
+ def text_heading(char, str)
139
+ str + "\n" + char*str.size
140
+ end
141
+
142
+ def self.inspect_value(obj)
143
+ if BetterErrors.ignored_classes.include? obj.class.name
144
+ "<span class='unsupported'>(Instance of ignored class. "\
145
+ "#{obj.class.name ? "Remove #{CGI.escapeHTML(obj.class.name)} from" : "Modify"}"\
146
+ " BetterErrors.ignored_classes if you need to see it.)</span>"
147
+ else
148
+ InspectableValue.new(obj).to_html
149
+ end
150
+ rescue BetterErrors::ValueLargerThanConfiguredMaximum
151
+ "<span class='unsupported'>(Object too large. "\
152
+ "#{obj.class.name ? "Modify #{CGI.escapeHTML(obj.class.name)}#inspect or a" : "A"}"\
153
+ "djust BetterErrors.maximum_variable_inspect_size if you need to see it.)</span>"
154
+ rescue Exception => e
155
+ "<span class='unsupported'>(exception #{CGI.escapeHTML(e.class.to_s)} was raised in inspect)</span>"
156
+ end
157
+
158
+ def eval_and_respond(index, code)
159
+ result, prompt, prefilled_input = @repls[index].send_input(code)
160
+
161
+ {
162
+ highlighted_input: Rouge::Formatters::HTML.new.format(Rouge::Lexers::Ruby.lex(code)),
163
+ prefilled_input: prefilled_input,
164
+ prompt: prompt,
165
+ result: result
166
+ }
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,43 @@
1
+ module BetterErrors
2
+ # @private
3
+ module ErrorPageStyle
4
+ def self.compiled_css(for_deployment = false)
5
+ begin
6
+ require "sassc"
7
+ rescue LoadError
8
+ raise LoadError, "The `sassc` gem is required when developing the `better_errors` gem. "\
9
+ "If you're using a release of `better_errors`, the compiled CSS is missing from the released gem"
10
+ # If you arrived here because sassc is not in your project's Gemfile,
11
+ # the issue here is that the release of the better_errors gem
12
+ # is supposed to contain the compiled CSS, but that file is missing from the release.
13
+ # So better_errors is trying to build the CSS on the fly, which requires the sassc gem.
14
+ #
15
+ # If you're developing the better_errors gem locally, and you're running a project
16
+ # that does not have sassc in its bundle, run `rake style:build` in the better_errors
17
+ # project to compile the CSS file.
18
+ end
19
+
20
+ style_dir = File.expand_path("style", File.dirname(__FILE__))
21
+ style_file = "#{style_dir}/main.scss"
22
+
23
+ engine = SassC::Engine.new(
24
+ File.read(style_file),
25
+ filename: style_file,
26
+ style: for_deployment ? :compressed : :expanded,
27
+ line_comments: !for_deployment,
28
+ load_paths: [style_dir],
29
+ )
30
+ engine.render
31
+ end
32
+
33
+ def self.style_tag(csp_nonce)
34
+ style_file = File.expand_path("templates/main.css", File.dirname(__FILE__))
35
+ css = if File.exist?(style_file)
36
+ File.open(style_file).read
37
+ else
38
+ compiled_css(false)
39
+ end
40
+ "<style type='text/css' nonce='#{csp_nonce}'>\n#{css}\n</style>"
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,17 @@
1
+ module BetterErrors
2
+ module ExceptionExtension
3
+ prepend_features Exception
4
+
5
+ def set_backtrace(*)
6
+ if caller_locations.none? { |loc| loc.path == __FILE__ }
7
+ @__better_errors_bindings_stack = ::Kernel.binding.callers.drop(1)
8
+ end
9
+
10
+ super
11
+ end
12
+
13
+ def __better_errors_bindings_stack
14
+ @__better_errors_bindings_stack || []
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,29 @@
1
+ module BetterErrors
2
+ class ExceptionHint
3
+ def initialize(exception)
4
+ @exception = exception
5
+ end
6
+
7
+ def hint
8
+ case exception
9
+ when NoMethodError
10
+ /\Aundefined method `(?<method>[^']+)' for (?<val>[^:]+):(?<klass>\w+)/.match(exception.message) do |match|
11
+ if match[:val] == "nil"
12
+ return "Something is `nil` when it probably shouldn't be."
13
+ elsif !match[:klass].start_with? '0x'
14
+ return "`#{match[:method]}` is being called on a `#{match[:klass]}` object, "\
15
+ "which might not be the type of object you were expecting."
16
+ end
17
+ end
18
+ when NameError
19
+ /\Aundefined local variable or method `(?<method>[^']+)' for/.match(exception.message) do |match|
20
+ return "`#{match[:method]}` is probably misspelled."
21
+ end
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :exception
28
+ end
29
+ end
@@ -0,0 +1,45 @@
1
+ require "cgi"
2
+ require "objspace" rescue nil
3
+
4
+ module BetterErrors
5
+ class ValueLargerThanConfiguredMaximum < StandardError; end
6
+
7
+ class InspectableValue
8
+ def initialize(value)
9
+ @original_value = value
10
+ end
11
+
12
+ def to_html
13
+ raise ValueLargerThanConfiguredMaximum unless value_small_enough_to_inspect?
14
+ value_as_html
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :original_value
20
+
21
+ def value_as_html
22
+ @value_as_html ||= CGI.escapeHTML(value)
23
+ end
24
+
25
+ def value
26
+ @value ||= begin
27
+ if original_value.respond_to? :inspect
28
+ original_value.inspect
29
+ else
30
+ original_value
31
+ end
32
+ end
33
+ end
34
+
35
+ def value_small_enough_to_inspect?
36
+ return true if BetterErrors.maximum_variable_inspect_size.nil?
37
+
38
+ if defined?(ObjectSpace) && defined?(ObjectSpace.memsize_of) && ObjectSpace.memsize_of(value)
39
+ ObjectSpace.memsize_of(value) <= BetterErrors.maximum_variable_inspect_size
40
+ else
41
+ value_as_html.length <= BetterErrors.maximum_variable_inspect_size
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,237 @@
1
+ require "json"
2
+ require "ipaddr"
3
+ require "securerandom"
4
+ require "set"
5
+ require "rack"
6
+
7
+ module BetterErrors
8
+ # Better Errors' error handling middleware. Including this in your middleware
9
+ # stack will show a Better Errors error page for exceptions raised below this
10
+ # middleware.
11
+ #
12
+ # If you are using Ruby on Rails, you do not need to manually insert this
13
+ # middleware into your middleware stack.
14
+ #
15
+ # @example Sinatra
16
+ # require "better_errors"
17
+ #
18
+ # if development?
19
+ # use BetterErrors::Middleware
20
+ # end
21
+ #
22
+ # @example Rack
23
+ # require "better_errors"
24
+ # if ENV["RACK_ENV"] == "development"
25
+ # use BetterErrors::Middleware
26
+ # end
27
+ #
28
+ class Middleware
29
+ # The set of IP addresses that are allowed to access Better Errors.
30
+ #
31
+ # Set to `{ "127.0.0.1/8", "::1/128" }` by default.
32
+ ALLOWED_IPS = Set.new
33
+
34
+ # Adds an address to the set of IP addresses allowed to access Better
35
+ # Errors.
36
+ def self.allow_ip!(addr)
37
+ ALLOWED_IPS << (addr.is_a?(IPAddr) ? addr : IPAddr.new(addr))
38
+ end
39
+
40
+ allow_ip! "127.0.0.0/8"
41
+ allow_ip! "::1/128" rescue nil # windows ruby doesn't have ipv6 support
42
+
43
+ CSRF_TOKEN_COOKIE_NAME = "BetterErrors-#{BetterErrors::VERSION}-CSRF-Token"
44
+
45
+ # A new instance of BetterErrors::Middleware
46
+ #
47
+ # @param app The Rack app/middleware to wrap with Better Errors
48
+ # @param handler The error handler to use.
49
+ def initialize(app, handler = ErrorPage)
50
+ @app = app
51
+ @handler = handler
52
+ end
53
+
54
+ # Calls the Better Errors middleware
55
+ #
56
+ # @param [Hash] env
57
+ # @return [Array]
58
+ def call(env)
59
+ if allow_ip? env
60
+ better_errors_call env
61
+ else
62
+ @app.call env
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def allow_ip?(env)
69
+ request = Rack::Request.new(env)
70
+ return true unless request.ip and !request.ip.strip.empty?
71
+ ip = IPAddr.new request.ip.split("%").first
72
+ ALLOWED_IPS.any? { |subnet| subnet.include? ip }
73
+ end
74
+
75
+ def better_errors_call(env)
76
+ case env["PATH_INFO"]
77
+ when %r{/__better_errors/(?<id>.+?)/(?<method>\w+)\z}
78
+ internal_call(env, $~[:id], $~[:method])
79
+ when %r{/__better_errors/?\z}
80
+ show_error_page env
81
+ else
82
+ protected_app_call env
83
+ end
84
+ end
85
+
86
+ def protected_app_call(env)
87
+ @app.call env
88
+ rescue Exception => ex
89
+ @error_page = @handler.new ex, env
90
+ log_exception
91
+ show_error_page(env, ex)
92
+ end
93
+
94
+ def show_error_page(env, exception=nil)
95
+ request = Rack::Request.new(env)
96
+ csrf_token = request.cookies[CSRF_TOKEN_COOKIE_NAME] || SecureRandom.uuid
97
+ csp_nonce = SecureRandom.base64(12)
98
+
99
+ type, content = if @error_page
100
+ if text?(env)
101
+ [ 'plain', @error_page.render_text ]
102
+ else
103
+ [ 'html', @error_page.render_main(csrf_token, csp_nonce) ]
104
+ end
105
+ else
106
+ [ 'html', no_errors_page ]
107
+ end
108
+
109
+ status_code = 500
110
+ if defined?(ActionDispatch::ExceptionWrapper) && exception
111
+ status_code = ActionDispatch::ExceptionWrapper.new(env, exception).status_code
112
+ end
113
+
114
+ headers = {
115
+ "Content-Type" => "text/#{type}; charset=utf-8",
116
+ "Content-Security-Policy" => [
117
+ "default-src 'none'",
118
+ # Specifying nonce makes a modern browser ignore 'unsafe-inline' which could still be set
119
+ # for older browsers without nonce support.
120
+ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src
121
+ "script-src 'self' 'nonce-#{csp_nonce}' 'unsafe-inline'",
122
+ "style-src 'self' 'nonce-#{csp_nonce}' 'unsafe-inline'",
123
+ "img-src data:",
124
+ "connect-src 'self'",
125
+ "navigate-to 'self' #{BetterErrors.editor.scheme}",
126
+ ].join('; '),
127
+ }
128
+
129
+ response = Rack::Response.new(content, status_code, headers)
130
+
131
+ unless request.cookies[CSRF_TOKEN_COOKIE_NAME]
132
+ response.set_cookie(CSRF_TOKEN_COOKIE_NAME, value: csrf_token, path: "/", httponly: true, same_site: :strict)
133
+ end
134
+
135
+ # In older versions of Rack, the body returned here is actually a Rack::BodyProxy which seems to be a bug.
136
+ # (It contains status, headers and body and does not act like an array of strings.)
137
+ # Since we already have status code and body here, there's no need to use the ones in the Rack::Response.
138
+ (_status_code, headers, _body) = response.finish
139
+ [status_code, headers, [content]]
140
+ end
141
+
142
+ def text?(env)
143
+ env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest" ||
144
+ !env["HTTP_ACCEPT"].to_s.include?('html')
145
+ end
146
+
147
+ def log_exception
148
+ return unless BetterErrors.logger
149
+
150
+ message = "\n#{@error_page.exception_type} - #{@error_page.exception_message}:\n"
151
+ message += backtrace_frames.map { |frame| " #{frame}\n" }.join
152
+
153
+ BetterErrors.logger.fatal message
154
+ end
155
+
156
+ def backtrace_frames
157
+ if defined?(Rails) && defined?(Rails.backtrace_cleaner)
158
+ Rails.backtrace_cleaner.clean @error_page.backtrace_frames.map(&:to_s)
159
+ else
160
+ @error_page.backtrace_frames
161
+ end
162
+ end
163
+
164
+ def internal_call(env, id, method)
165
+ return not_found_json_response unless %w[variables eval].include?(method)
166
+ return no_errors_json_response unless @error_page
167
+ return invalid_error_json_response if id != @error_page.id
168
+
169
+ request = Rack::Request.new(env)
170
+ return invalid_csrf_token_json_response unless request.cookies[CSRF_TOKEN_COOKIE_NAME]
171
+
172
+ request.body.rewind
173
+ body = JSON.parse(request.body.read)
174
+ return invalid_csrf_token_json_response unless request.cookies[CSRF_TOKEN_COOKIE_NAME] == body['csrfToken']
175
+
176
+ return not_acceptable_json_response unless request.content_type == 'application/json'
177
+
178
+ response = @error_page.send("do_#{method}", body)
179
+ [200, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(response)]]
180
+ end
181
+
182
+ def no_errors_page
183
+ "<h1>No errors</h1><p>No errors have been recorded yet.</p><hr>" +
184
+ "<code>Better Errors v#{BetterErrors::VERSION}</code>"
185
+ end
186
+
187
+ def no_errors_json_response
188
+ explanation = if defined? Middleman
189
+ "Middleman reloads all dependencies for each request, " +
190
+ "which breaks Better Errors."
191
+ elsif defined?(Shotgun) && defined?(Hanami)
192
+ "Hanami is likely running with code-reloading enabled, which is the default. " +
193
+ "You can disable this by running hanami with the `--no-code-reloading` option."
194
+ elsif defined? Shotgun
195
+ "The shotgun gem causes everything to be reloaded for every request. " +
196
+ "You can disable shotgun in the Gemfile temporarily to use Better Errors."
197
+ else
198
+ "The application has been restarted since this page loaded, " +
199
+ "or the framework is reloading all gems before each request "
200
+ end
201
+ [200, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(
202
+ error: 'No exception information available',
203
+ explanation: explanation,
204
+ )]]
205
+ end
206
+
207
+ def invalid_error_json_response
208
+ [200, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(
209
+ error: "Session expired",
210
+ explanation: "This page was likely opened from a previous exception, " +
211
+ "and the exception is no longer available in memory.",
212
+ )]]
213
+ end
214
+
215
+ def invalid_csrf_token_json_response
216
+ [200, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(
217
+ error: "Invalid CSRF Token",
218
+ explanation: "The browser session might have been cleared, " +
219
+ "or something went wrong.",
220
+ )]]
221
+ end
222
+
223
+ def not_found_json_response
224
+ [404, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(
225
+ error: "Not found",
226
+ explanation: "Not a recognized internal call.",
227
+ )]]
228
+ end
229
+
230
+ def not_acceptable_json_response
231
+ [406, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(
232
+ error: "Request not acceptable",
233
+ explanation: "The internal request did not match an acceptable content type.",
234
+ )]]
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,28 @@
1
+ module BetterErrors
2
+ # @private
3
+ class Railtie < Rails::Railtie
4
+ initializer "better_errors.configure_rails_initialization" do
5
+ if use_better_errors?
6
+ insert_middleware
7
+ BetterErrors.logger = Rails.logger
8
+ BetterErrors.application_root = Rails.root.to_s
9
+ end
10
+ end
11
+
12
+ def insert_middleware
13
+ if defined? ActionDispatch::DebugExceptions
14
+ app.middleware.insert_after ActionDispatch::DebugExceptions, BetterErrors::Middleware
15
+ else
16
+ app.middleware.use BetterErrors::Middleware
17
+ end
18
+ end
19
+
20
+ def use_better_errors?
21
+ !Rails.env.production? and app.config.consider_all_requests_local
22
+ end
23
+
24
+ def app
25
+ Rails.application
26
+ end
27
+ end
28
+ end