web-console 3.5.1 → 4.2.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 (39) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.markdown +91 -8
  3. data/MIT-LICENSE +1 -1
  4. data/README.markdown +41 -38
  5. data/Rakefile +14 -12
  6. data/lib/web-console.rb +3 -1
  7. data/lib/web_console/context.rb +8 -6
  8. data/lib/web_console/errors.rb +2 -0
  9. data/lib/web_console/evaluator.rb +14 -5
  10. data/lib/web_console/exception_mapper.rb +33 -10
  11. data/lib/web_console/extensions.rb +12 -23
  12. data/lib/web_console/injector.rb +32 -0
  13. data/lib/web_console/interceptor.rb +17 -0
  14. data/lib/web_console/middleware.rb +21 -24
  15. data/lib/web_console/permissions.rb +42 -0
  16. data/lib/web_console/railtie.rb +36 -19
  17. data/lib/web_console/request.rb +8 -20
  18. data/lib/web_console/session.rb +13 -9
  19. data/lib/web_console/source_location.rb +17 -0
  20. data/lib/web_console/tasks/extensions.rake +15 -13
  21. data/lib/web_console/tasks/templates.rake +9 -13
  22. data/lib/web_console/template.rb +4 -3
  23. data/lib/web_console/templates/console.js.erb +140 -38
  24. data/lib/web_console/templates/error_page.js.erb +7 -8
  25. data/lib/web_console/templates/index.html.erb +4 -0
  26. data/lib/web_console/templates/layouts/inlined_string.erb +1 -1
  27. data/lib/web_console/templates/layouts/javascript.erb +1 -1
  28. data/lib/web_console/templates/regular_page.js.erb +24 -0
  29. data/lib/web_console/templates/style.css.erb +182 -33
  30. data/lib/web_console/testing/erb_precompiler.rb +5 -3
  31. data/lib/web_console/testing/fake_middleware.rb +14 -9
  32. data/lib/web_console/testing/helper.rb +3 -1
  33. data/lib/web_console/version.rb +3 -1
  34. data/lib/web_console/view.rb +11 -3
  35. data/lib/web_console/whiny_request.rb +7 -5
  36. data/lib/web_console.rb +17 -8
  37. metadata +17 -15
  38. data/lib/web_console/response.rb +0 -23
  39. data/lib/web_console/whitelist.rb +0 -44
@@ -1,14 +1,13 @@
1
- require 'active_support/core_ext/string/strip'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string/strip"
2
4
 
3
5
  module WebConsole
4
6
  class Middleware
5
- TEMPLATES_PATH = File.expand_path('../templates', __FILE__)
6
-
7
- cattr_accessor :mount_point
8
- @@mount_point = '/__web_console'
7
+ TEMPLATES_PATH = File.expand_path("../templates", __FILE__)
9
8
 
10
- cattr_accessor :whiny_requests
11
- @@whiny_requests = true
9
+ cattr_accessor :mount_point, default: "/__web_console"
10
+ cattr_accessor :whiny_requests, default: true
12
11
 
13
12
  def initialize(app)
14
13
  @app = app
@@ -17,7 +16,7 @@ module WebConsole
17
16
  def call(env)
18
17
  app_exception = catch :app_exception do
19
18
  request = create_regular_or_whiny_request(env)
20
- return call_app(env) unless request.from_whitelisted_ip?
19
+ return call_app(env) unless request.permitted?
21
20
 
22
21
  if id = id_for_repl_session_update(request)
23
22
  return update_repl_session(id, request)
@@ -25,19 +24,18 @@ module WebConsole
25
24
  return change_stack_trace(id, request)
26
25
  end
27
26
 
27
+
28
28
  status, headers, body = call_app(env)
29
29
 
30
- if session = Session.from(Thread.current) and acceptable_content_type?(headers)
31
- response = Response.new(body, status, headers)
32
- template = Template.new(env, session)
30
+ if (session = Session.from(Thread.current)) && acceptable_content_type?(headers)
31
+ headers["x-web-console-session-id"] = session.id
32
+ headers["x-web-console-mount-point"] = mount_point
33
33
 
34
- response.headers["X-Web-Console-Session-Id"] = session.id
35
- response.headers["X-Web-Console-Mount-Point"] = mount_point
36
- response.write(template.render('index'))
37
- response.finish
38
- else
39
- [ status, headers, body ]
34
+ template = Template.new(env, session)
35
+ body, headers = Injector.new(body, headers).inject(template.render("index"))
40
36
  end
37
+
38
+ [ status, headers, body ]
41
39
  end
42
40
  rescue => e
43
41
  WebConsole.logger.error("\n#{e.class}: #{e}\n\tfrom #{e.backtrace.join("\n\tfrom ")}")
@@ -54,19 +52,18 @@ module WebConsole
54
52
  private
55
53
 
56
54
  def acceptable_content_type?(headers)
57
- Mime::Type.parse(headers['Content-Type'].to_s).first == Mime[:html]
55
+ headers[Rack::CONTENT_TYPE].to_s.include?("html")
58
56
  end
59
57
 
60
58
  def json_response(opts = {})
61
59
  status = opts.fetch(:status, 200)
62
- headers = { 'Content-Type' => 'application/json; charset = utf-8' }
60
+ headers = { Rack::CONTENT_TYPE => "application/json; charset = utf-8" }
63
61
  body = yield.to_json
64
62
 
65
- Rack::Response.new(body, status, headers).finish
63
+ [ status, headers, [ body ] ]
66
64
  end
67
65
 
68
66
  def json_response_with_session(id, request, opts = {})
69
- return respond_with_unacceptable_request unless request.acceptable?
70
67
  return respond_with_unavailable_session(id) unless session = Session.find(id)
71
68
 
72
69
  json_response(opts) { yield session }
@@ -113,7 +110,7 @@ module WebConsole
113
110
 
114
111
  def change_stack_trace(id, request)
115
112
  json_response_with_session(id, request) do |session|
116
- session.switch_binding_to(request.params[:frame_id])
113
+ session.switch_binding_to(request.params[:frame_id], request.params[:exception_object_id])
117
114
 
118
115
  { ok: true }
119
116
  end
@@ -121,13 +118,13 @@ module WebConsole
121
118
 
122
119
  def respond_with_unavailable_session(id)
123
120
  json_response(status: 404) do
124
- { output: format(I18n.t('errors.unavailable_session'), id: id)}
121
+ { output: format(I18n.t("errors.unavailable_session"), id: id) }
125
122
  end
126
123
  end
127
124
 
128
125
  def respond_with_unacceptable_request
129
126
  json_response(status: 406) do
130
- { output: I18n.t('errors.unacceptable_request') }
127
+ { output: I18n.t("errors.unacceptable_request") }
131
128
  end
132
129
  end
133
130
 
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+
5
+ module WebConsole
6
+ class Permissions
7
+ # IPv4 and IPv6 localhost should be always allowed.
8
+ ALWAYS_PERMITTED_NETWORKS = %w( 127.0.0.0/8 ::1 )
9
+
10
+ def initialize(networks = nil)
11
+ @networks = normalize_networks(networks).map(&method(:coerce_network_to_ipaddr)).uniq
12
+ end
13
+
14
+ def include?(network)
15
+ @networks.any? { |permission| permission.include?(network.to_s) }
16
+ rescue IPAddr::InvalidAddressError
17
+ false
18
+ end
19
+
20
+ def to_s
21
+ @networks.map(&method(:human_readable_ipaddr)).join(", ")
22
+ end
23
+
24
+ private
25
+
26
+ def normalize_networks(networks)
27
+ Array(networks).concat(ALWAYS_PERMITTED_NETWORKS)
28
+ end
29
+
30
+ def coerce_network_to_ipaddr(network)
31
+ if network.is_a?(IPAddr)
32
+ network
33
+ else
34
+ IPAddr.new(network)
35
+ end
36
+ end
37
+
38
+ def human_readable_ipaddr(ipaddr)
39
+ ipaddr.to_range.to_s.split("..").uniq.join("/")
40
+ end
41
+ end
42
+ end
@@ -1,20 +1,19 @@
1
- require 'rails/railtie'
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
2
4
 
3
5
  module WebConsole
4
6
  class Railtie < ::Rails::Railtie
5
7
  config.web_console = ActiveSupport::OrderedOptions.new
6
- config.web_console.whitelisted_ips = %w( 127.0.0.1 ::1 )
7
8
 
8
- initializer 'web_console.initialize' do
9
- require 'bindex'
10
- require 'web_console/extensions'
9
+ initializer "web_console.initialize" do
10
+ require "bindex"
11
+ require "web_console/extensions"
11
12
 
12
- if logger = ::Rails.logger
13
- WebConsole.logger = logger
14
- end
13
+ ActionDispatch::DebugExceptions.register_interceptor(Interceptor)
15
14
  end
16
15
 
17
- initializer 'web_console.development_only' do
16
+ initializer "web_console.development_only" do
18
17
  unless (config.web_console.development_only == false) || Rails.env.development?
19
18
  abort <<-END.strip_heredoc
20
19
  Web Console is activated in the #{Rails.env} environment. This is
@@ -32,13 +31,13 @@ module WebConsole
32
31
  end
33
32
  end
34
33
 
35
- initializer 'web_console.insert_middleware' do |app|
34
+ initializer "web_console.insert_middleware" do |app|
36
35
  app.middleware.insert_before ActionDispatch::DebugExceptions, Middleware
37
36
  end
38
37
 
39
- initializer 'web_console.mount_point' do
38
+ initializer "web_console.mount_point" do
40
39
  if mount_point = config.web_console.mount_point
41
- Middleware.mount_point = mount_point.chomp('/')
40
+ Middleware.mount_point = mount_point.chomp("/")
42
41
  end
43
42
 
44
43
  if root = Rails.application.config.relative_url_root
@@ -46,26 +45,44 @@ module WebConsole
46
45
  end
47
46
  end
48
47
 
49
- initializer 'web_console.template_paths' do
48
+ initializer "web_console.template_paths" do
50
49
  if template_paths = config.web_console.template_paths
51
50
  Template.template_paths.unshift(*Array(template_paths))
52
51
  end
53
52
  end
54
53
 
55
- initializer 'web_console.whitelisted_ips' do
56
- if whitelisted_ips = config.web_console.whitelisted_ips
57
- Request.whitelisted_ips = Whitelist.new(whitelisted_ips)
54
+ initializer "web_console.deprecator" do |app|
55
+ app.deprecators[:web_console] = WebConsole.deprecator if app.respond_to?(:deprecators)
56
+ end
57
+
58
+ initializer "web_console.permissions" do
59
+ permissions = web_console_permissions
60
+ Request.permissions = Permissions.new(permissions)
61
+ end
62
+
63
+ def web_console_permissions
64
+ case
65
+ when config.web_console.permissions
66
+ config.web_console.permissions
67
+ when config.web_console.allowed_ips
68
+ config.web_console.allowed_ips
69
+ when config.web_console.whitelisted_ips
70
+ WebConsole.deprecator.warn(<<-MSG.squish)
71
+ The config.web_console.whitelisted_ips is deprecated and will be ignored in future release of web_console.
72
+ Please use config.web_console.allowed_ips instead.
73
+ MSG
74
+ config.web_console.whitelisted_ips
58
75
  end
59
76
  end
60
77
 
61
- initializer 'web_console.whiny_requests' do
78
+ initializer "web_console.whiny_requests" do
62
79
  if config.web_console.key?(:whiny_requests)
63
80
  Middleware.whiny_requests = config.web_console.whiny_requests
64
81
  end
65
82
  end
66
83
 
67
- initializer 'i18n.load_path' do
68
- config.i18n.load_path.concat(Dir[File.expand_path('../locales/*.yml', __FILE__)])
84
+ initializer "i18n.load_path" do
85
+ config.i18n.load_path.concat(Dir[File.expand_path("../locales/*.yml", __FILE__)])
69
86
  end
70
87
  end
71
88
  end
@@ -1,29 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module WebConsole
2
- # Web Console tailored request object.
3
4
  class Request < ActionDispatch::Request
4
- # Configurable set of whitelisted networks.
5
- cattr_accessor :whitelisted_ips
6
- @@whitelisted_ips = Whitelist.new
7
-
8
- # Define a vendor MIME type. We can call it using Mime[:web_console_v2].
9
- Mime::Type.register 'application/vnd.web-console.v2', :web_console_v2
5
+ cattr_accessor :permissions, default: Permissions.new
10
6
 
11
- # Returns whether a request came from a whitelisted IP.
12
- #
13
- # For a request to hit Web Console features, it needs to come from a white
14
- # listed IP.
15
- def from_whitelisted_ip?
16
- whitelisted_ips.include?(strict_remote_ip)
7
+ def permitted?
8
+ permissions.include?(strict_remote_ip)
17
9
  end
18
10
 
19
- # Determines the remote IP using our much stricter whitelist.
20
11
  def strict_remote_ip
21
- GetSecureIp.new(self, whitelisted_ips).to_s
22
- end
23
-
24
- # Returns whether the request is acceptable.
25
- def acceptable?
26
- xhr? && accepts.any? { |mime| Mime[:web_console_v2] == mime }
12
+ GetSecureIp.new(self, permissions).to_s
13
+ rescue ActionDispatch::RemoteIp::IpSpoofAttackError
14
+ "[Spoofed]"
27
15
  end
28
16
 
29
17
  private
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module WebConsole
2
4
  # A session lets you persist an +Evaluator+ instance in memory associated
3
5
  # with multiple bindings.
@@ -9,8 +11,7 @@ module WebConsole
9
11
  # error pages only, as currently, this is the only client that needs to do
10
12
  # that.
11
13
  class Session
12
- cattr_reader :inmemory_storage
13
- @@inmemory_storage = {}
14
+ cattr_reader :inmemory_storage, default: {}
14
15
 
15
16
  class << self
16
17
  # Finds a persisted session in memory by its id.
@@ -30,9 +31,9 @@ module WebConsole
30
31
  # storage.
31
32
  def from(storage)
32
33
  if exc = storage[:__web_console_exception]
33
- new(ExceptionMapper.new(exc))
34
+ new(ExceptionMapper.follow(exc))
34
35
  elsif binding = storage[:__web_console_binding]
35
- new([binding])
36
+ new([[binding]])
36
37
  end
37
38
  end
38
39
  end
@@ -40,10 +41,11 @@ module WebConsole
40
41
  # An unique identifier for every REPL.
41
42
  attr_reader :id
42
43
 
43
- def initialize(bindings)
44
+ def initialize(exception_mappers)
44
45
  @id = SecureRandom.hex(16)
45
- @bindings = bindings
46
- @evaluator = Evaluator.new(@current_binding = bindings.first)
46
+
47
+ @exception_mappers = exception_mappers
48
+ @evaluator = Evaluator.new(@current_binding = exception_mappers.first.first)
47
49
 
48
50
  store_into_memory
49
51
  end
@@ -58,8 +60,10 @@ module WebConsole
58
60
  # Switches the current binding to the one at specified +index+.
59
61
  #
60
62
  # Returns nothing.
61
- def switch_binding_to(index)
62
- @evaluator = Evaluator.new(@current_binding = @bindings[index.to_i])
63
+ def switch_binding_to(index, exception_object_id)
64
+ bindings = ExceptionMapper.find_binding(@exception_mappers, exception_object_id)
65
+
66
+ @evaluator = Evaluator.new(@current_binding = bindings[index.to_i])
63
67
  end
64
68
 
65
69
  # Returns context of the current binding
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebConsole
4
+ class SourceLocation
5
+ def initialize(binding)
6
+ @binding = binding
7
+ end
8
+
9
+ if RUBY_VERSION >= "2.6"
10
+ def path() @binding.source_location.first end
11
+ def lineno() @binding.source_location.last end
12
+ else
13
+ def path() @binding.eval("__FILE__") end
14
+ def lineno() @binding.eval("__LINE__") end
15
+ end
16
+ end
17
+ end
@@ -1,23 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  namespace :ext do
2
- rootdir = Pathname('extensions')
4
+ rootdir = Pathname("extensions")
3
5
 
4
- desc 'Build Chrome Extension'
5
- task chrome: 'chrome:build'
6
+ desc "Build Chrome Extension"
7
+ task chrome: "chrome:build"
6
8
 
7
9
  namespace :chrome do
8
- dist = Pathname('dist/crx')
10
+ dist = Pathname("dist/crx")
9
11
  extdir = rootdir.join(dist)
10
- manifest_json = rootdir.join('chrome/manifest.json')
12
+ manifest_json = rootdir.join("chrome/manifest.json")
11
13
 
12
14
  directory extdir
13
15
 
14
- task build: [ extdir, 'lib:templates' ] do
16
+ task build: [ extdir, "lib:templates" ] do
15
17
  cd rootdir do
16
- cp_r [ 'img/', 'tmp/lib/' ], dist
18
+ cp_r [ "img/", "tmp/lib/" ], dist
17
19
  `cd chrome && git ls-files`.split("\n").each do |src|
18
20
  dest = dist.join(src)
19
21
  mkdir_p dest.dirname
20
- cp Pathname('chrome').join(src), dest
22
+ cp Pathname("chrome").join(src), dest
21
23
  end
22
24
  end
23
25
  end
@@ -34,7 +36,7 @@ namespace :ext do
34
36
  cd(extdir) { sh "zip -r ../crx-web-console-#{version}.zip ./" }
35
37
  end
36
38
 
37
- desc 'Launch a browser with the chrome extension.'
39
+ desc "Launch a browser with the chrome extension."
38
40
  task run: [ :build ] do
39
41
  cd(rootdir) { sh "sh ./script/run_chrome.sh --load-extension=#{dist}" }
40
42
  end
@@ -45,15 +47,15 @@ namespace :ext do
45
47
  end
46
48
 
47
49
  namespace :lib do
48
- templates = Pathname('lib/web_console/templates')
49
- tmplib = rootdir.join('tmp/lib/')
50
- js_erb = FileList.new(templates.join('**/*.js.erb'))
50
+ templates = Pathname("lib/web_console/templates")
51
+ tmplib = rootdir.join("tmp/lib/")
52
+ js_erb = FileList.new(templates.join("**/*.js.erb"))
51
53
  dirs = js_erb.pathmap("%{^#{templates},#{tmplib}}d")
52
54
 
53
55
  task templates: dirs + js_erb.pathmap("%{^#{templates},#{tmplib}}X")
54
56
 
55
57
  dirs.each { |d| directory d }
56
- rule '.js' => [ "%{^#{tmplib},#{templates}}X.js.erb" ] do |t|
58
+ rule ".js" => [ "%{^#{tmplib},#{templates}}X.js.erb" ] do |t|
57
59
  File.write(t.name, WebConsole::Testing::ERBPrecompiler.new(t.source).build)
58
60
  end
59
61
  end
@@ -1,14 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+
1
5
  namespace :templates do
2
- desc 'Run tests for templates'
6
+ desc "Run tests for templates"
3
7
  task test: [ :daemonize, :npm, :rackup, :wait, :mocha, :kill, :exit ]
4
8
  task serve: [ :npm, :rackup ]
5
9
 
6
- workdir = Pathname(EXPANDED_CWD).join('test/templates')
10
+ workdir = Pathname(EXPANDED_CWD).join("test/templates")
7
11
  pid = Pathname(Dir.tmpdir).join("web_console_test.pid")
8
12
  runner = URI.parse("http://#{ENV['IP'] || '127.0.0.1'}:#{ENV['PORT'] || 29292}/html/test_runner.html")
9
13
  rackup = "rackup --host #{runner.host} --port #{runner.port}"
10
14
  result = nil
11
- browser = 'phantomjs'
12
15
 
13
16
  def need_to_wait?(uri)
14
17
  Net::HTTP.start(uri.host, uri.port) { |http| http.get(uri.path) }
@@ -20,15 +23,8 @@ namespace :templates do
20
23
  rackup += " -D --pid #{pid}"
21
24
  end
22
25
 
23
- task :npm => [ :phantomjs ] do
24
- Dir.chdir(workdir) { system 'npm install --silent' }
25
- end
26
-
27
- task :phantomjs do
28
- unless system("which #{browser} >/dev/null")
29
- browser = './node_modules/.bin/phantomjs'
30
- Dir.chdir(workdir) { system("test -f #{browser} || npm install --silent phantomjs-prebuilt") }
31
- end
26
+ task :npm do
27
+ Dir.chdir(workdir) { system "npm install --silent" }
32
28
  end
33
29
 
34
30
  task :rackup do
@@ -41,7 +37,7 @@ namespace :templates do
41
37
  end
42
38
 
43
39
  task :mocha do
44
- Dir.chdir(workdir) { result = system("#{browser} ./node_modules/mocha-phantomjs-core/mocha-phantomjs-core.js #{runner} dot") }
40
+ Dir.chdir(workdir) { result = system("npx mocha-headless-chrome -f #{runner} -r dot") }
45
41
  end
46
42
 
47
43
  task :kill do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module WebConsole
2
4
  # A facade that handles template rendering and composition.
3
5
  #
@@ -5,8 +7,7 @@ module WebConsole
5
7
  # Rails error pages.
6
8
  class Template
7
9
  # Lets you customize the default templates folder location.
8
- cattr_accessor :template_paths
9
- @@template_paths = [ File.expand_path('../templates', __FILE__) ]
10
+ cattr_accessor :template_paths, default: [ File.expand_path("../templates", __FILE__) ]
10
11
 
11
12
  def initialize(env, session)
12
13
  @env = env
@@ -16,7 +17,7 @@ module WebConsole
16
17
 
17
18
  # Render a template (inferred from +template_paths+) as a plain string.
18
19
  def render(template)
19
- view = View.new(template_paths, instance_values)
20
+ view = View.with_empty_template_cache.with_view_paths(template_paths, instance_values)
20
21
  view.render(template: template, layout: false)
21
22
  end
22
23
  end