web-console-compat 3.5.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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.markdown +110 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.markdown +5 -0
  5. data/Rakefile +27 -0
  6. data/lib/web-console-compat.rb +1 -0
  7. data/lib/web-console.rb +1 -0
  8. data/lib/web_console.rb +28 -0
  9. data/lib/web_console/context.rb +43 -0
  10. data/lib/web_console/errors.rb +7 -0
  11. data/lib/web_console/evaluator.rb +33 -0
  12. data/lib/web_console/exception_mapper.rb +33 -0
  13. data/lib/web_console/extensions.rb +44 -0
  14. data/lib/web_console/integration.rb +31 -0
  15. data/lib/web_console/integration/cruby.rb +23 -0
  16. data/lib/web_console/integration/rubinius.rb +39 -0
  17. data/lib/web_console/locales/en.yml +15 -0
  18. data/lib/web_console/middleware.rb +140 -0
  19. data/lib/web_console/railtie.rb +71 -0
  20. data/lib/web_console/request.rb +50 -0
  21. data/lib/web_console/response.rb +23 -0
  22. data/lib/web_console/session.rb +76 -0
  23. data/lib/web_console/tasks/extensions.rake +60 -0
  24. data/lib/web_console/tasks/templates.rake +54 -0
  25. data/lib/web_console/template.rb +23 -0
  26. data/lib/web_console/templates/_inner_console_markup.html.erb +8 -0
  27. data/lib/web_console/templates/_markup.html.erb +5 -0
  28. data/lib/web_console/templates/_prompt_box_markup.html.erb +2 -0
  29. data/lib/web_console/templates/console.js.erb +922 -0
  30. data/lib/web_console/templates/error_page.js.erb +70 -0
  31. data/lib/web_console/templates/index.html.erb +8 -0
  32. data/lib/web_console/templates/layouts/inlined_string.erb +1 -0
  33. data/lib/web_console/templates/layouts/javascript.erb +5 -0
  34. data/lib/web_console/templates/main.js.erb +1 -0
  35. data/lib/web_console/templates/style.css.erb +33 -0
  36. data/lib/web_console/testing/erb_precompiler.rb +25 -0
  37. data/lib/web_console/testing/fake_middleware.rb +39 -0
  38. data/lib/web_console/testing/helper.rb +9 -0
  39. data/lib/web_console/version.rb +3 -0
  40. data/lib/web_console/view.rb +50 -0
  41. data/lib/web_console/whiny_request.rb +31 -0
  42. data/lib/web_console/whitelist.rb +44 -0
  43. metadata +147 -0
@@ -0,0 +1,15 @@
1
+ en:
2
+ errors:
3
+ unavailable_session: |
4
+ Session %{id} is no longer available in memory.
5
+
6
+ If you happen to run on a multi-process server (like Unicorn or Puma) the process
7
+ this request hit doesn't store %{id} in memory. Consider turning the number of
8
+ processes/workers to one (1) or using a different server in development.
9
+
10
+ unacceptable_request: |
11
+ A supported version is expected in the Accept header.
12
+
13
+ connection_refused: |
14
+ Oops! Failed to connect to the Web Console middleware.
15
+ Please make sure a rails development server is running.
@@ -0,0 +1,140 @@
1
+ require 'active_support/core_ext/string/strip'
2
+
3
+ module WebConsole
4
+ class Middleware
5
+ TEMPLATES_PATH = File.expand_path('../templates', __FILE__)
6
+
7
+ cattr_accessor :mount_point
8
+ @@mount_point = '/__web_console'
9
+
10
+ cattr_accessor :whiny_requests
11
+ @@whiny_requests = true
12
+
13
+ def initialize(app)
14
+ @app = app
15
+ end
16
+
17
+ def call(env)
18
+ app_exception = catch :app_exception do
19
+ request = create_regular_or_whiny_request(env)
20
+ return call_app(env) unless request.from_whitelisted_ip?
21
+
22
+ if id = id_for_repl_session_update(request)
23
+ return update_repl_session(id, request)
24
+ elsif id = id_for_repl_session_stack_frame_change(request)
25
+ return change_stack_trace(id, request)
26
+ end
27
+
28
+ status, headers, body = call_app(env)
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)
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 ]
40
+ end
41
+ end
42
+ rescue => e
43
+ WebConsole.logger.error("\n#{e.class}: #{e}\n\tfrom #{e.backtrace.join("\n\tfrom ")}")
44
+ raise e
45
+ ensure
46
+ # Clean up the fiber locals after the session creation. Object#console
47
+ # uses those to communicate the current binding or exception to the middleware.
48
+ Thread.current[:__web_console_exception] = nil
49
+ Thread.current[:__web_console_binding] = nil
50
+
51
+ raise app_exception if Exception === app_exception
52
+ end
53
+
54
+ private
55
+
56
+ def acceptable_content_type?(headers)
57
+ Mime::Type.parse(headers['Content-Type'].to_s).first == Mime[:html]
58
+ end
59
+
60
+ def json_response(opts = {})
61
+ status = opts.fetch(:status, 200)
62
+ headers = { 'Content-Type' => 'application/json; charset = utf-8' }
63
+ body = yield.to_json
64
+
65
+ Rack::Response.new(body, status, headers).finish
66
+ end
67
+
68
+ def json_response_with_session(id, request, opts = {})
69
+ return respond_with_unacceptable_request unless request.acceptable?
70
+ return respond_with_unavailable_session(id) unless session = Session.find(id)
71
+
72
+ json_response(opts) { yield session }
73
+ end
74
+
75
+ def create_regular_or_whiny_request(env)
76
+ request = Request.new(env)
77
+ whiny_requests ? WhinyRequest.new(request) : request
78
+ end
79
+
80
+ def repl_sessions_re
81
+ @_repl_sessions_re ||= %r{#{mount_point}/repl_sessions/(?<id>[^/]+)}
82
+ end
83
+
84
+ def update_re
85
+ @_update_re ||= %r{#{repl_sessions_re}\z}
86
+ end
87
+
88
+ def binding_change_re
89
+ @_binding_change_re ||= %r{#{repl_sessions_re}/trace\z}
90
+ end
91
+
92
+ def id_for_repl_session_update(request)
93
+ if request.xhr? && request.put?
94
+ update_re.match(request.path) { |m| m[:id] }
95
+ end
96
+ end
97
+
98
+ def id_for_repl_session_stack_frame_change(request)
99
+ if request.xhr? && request.post?
100
+ binding_change_re.match(request.path) { |m| m[:id] }
101
+ end
102
+ end
103
+
104
+ def update_repl_session(id, request)
105
+ json_response_with_session(id, request) do |session|
106
+ if input = request.params[:input]
107
+ { output: session.eval(input) }
108
+ elsif input = request.params[:context]
109
+ { context: session.context(input) }
110
+ end
111
+ end
112
+ end
113
+
114
+ def change_stack_trace(id, request)
115
+ json_response_with_session(id, request) do |session|
116
+ session.switch_binding_to(request.params[:frame_id])
117
+
118
+ { ok: true }
119
+ end
120
+ end
121
+
122
+ def respond_with_unavailable_session(id)
123
+ json_response(status: 404) do
124
+ { output: format(I18n.t('errors.unavailable_session'), id: id)}
125
+ end
126
+ end
127
+
128
+ def respond_with_unacceptable_request
129
+ json_response(status: 406) do
130
+ { output: I18n.t('errors.unacceptable_request') }
131
+ end
132
+ end
133
+
134
+ def call_app(env)
135
+ @app.call(env)
136
+ rescue => e
137
+ throw :app_exception, e
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,71 @@
1
+ require 'rails/railtie'
2
+
3
+ module WebConsole
4
+ class Railtie < ::Rails::Railtie
5
+ config.web_console = ActiveSupport::OrderedOptions.new
6
+ config.web_console.whitelisted_ips = %w( 127.0.0.1 ::1 )
7
+
8
+ initializer 'web_console.initialize' do
9
+ require 'web_console/integration'
10
+ require 'web_console/extensions'
11
+
12
+ if logger = ::Rails.logger
13
+ WebConsole.logger = logger
14
+ end
15
+ end
16
+
17
+ initializer 'web_console.development_only' do
18
+ unless (config.web_console.development_only == false) || Rails.env.development?
19
+ abort <<-END.strip_heredoc
20
+ Web Console is activated in the #{Rails.env} environment. This is
21
+ usually a mistake. To ensure it's only activated in development
22
+ mode, move it to the development group of your Gemfile:
23
+
24
+ gem 'web-console', group: :development
25
+
26
+ If you still want to run it in the #{Rails.env} environment (and know
27
+ what you are doing), put this in your Rails application
28
+ configuration:
29
+
30
+ config.web_console.development_only = false
31
+ END
32
+ end
33
+ end
34
+
35
+ initializer 'web_console.insert_middleware' do |app|
36
+ app.middleware.insert_before ActionDispatch::DebugExceptions, Middleware
37
+ end
38
+
39
+ initializer 'web_console.mount_point' do
40
+ if mount_point = config.web_console.mount_point
41
+ Middleware.mount_point = mount_point.chomp('/')
42
+ end
43
+
44
+ if root = Rails.application.config.relative_url_root
45
+ Middleware.mount_point = File.join(root, Middleware.mount_point)
46
+ end
47
+ end
48
+
49
+ initializer 'web_console.template_paths' do
50
+ if template_paths = config.web_console.template_paths
51
+ Template.template_paths.unshift(*Array(template_paths))
52
+ end
53
+ end
54
+
55
+ initializer 'web_console.whitelisted_ips' do
56
+ if whitelisted_ips = config.web_console.whitelisted_ips
57
+ Request.whitelisted_ips = Whitelist.new(whitelisted_ips)
58
+ end
59
+ end
60
+
61
+ initializer 'web_console.whiny_requests' do
62
+ if config.web_console.key?(:whiny_requests)
63
+ Middleware.whiny_requests = config.web_console.whiny_requests
64
+ end
65
+ end
66
+
67
+ initializer 'i18n.load_path' do
68
+ config.i18n.load_path.concat(Dir[File.expand_path('../locales/*.yml', __FILE__)])
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,50 @@
1
+ module WebConsole
2
+ # Web Console tailored request object.
3
+ 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
10
+
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)
17
+ end
18
+
19
+ # Determines the remote IP using our much stricter whitelist.
20
+ 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 }
27
+ end
28
+
29
+ private
30
+
31
+ class GetSecureIp < ActionDispatch::RemoteIp::GetIp
32
+ def initialize(req, proxies)
33
+ # After rails/rails@07b2ff0 ActionDispatch::RemoteIp::GetIp initializes
34
+ # with a ActionDispatch::Request object instead of plain Rack
35
+ # environment hash. Keep both @req and @env here, so we don't if/else
36
+ # on Rails versions.
37
+ @req = req
38
+ @env = req.env
39
+ @check_ip = true
40
+ @proxies = proxies
41
+ end
42
+
43
+ def filter_proxies(ips)
44
+ ips.reject do |ip|
45
+ @proxies.include?(ip)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,23 @@
1
+ module WebConsole
2
+ # A response object that writes content before the closing </body> tag, if
3
+ # possible.
4
+ #
5
+ # The object quacks like Rack::Response.
6
+ class Response < Struct.new(:body, :status, :headers)
7
+ def write(content)
8
+ raw_body = Array(body).first.to_s
9
+
10
+ if position = raw_body.rindex('</body>')
11
+ raw_body.insert(position, content)
12
+ else
13
+ raw_body << content
14
+ end
15
+
16
+ self.body = raw_body
17
+ end
18
+
19
+ def finish
20
+ Rack::Response.new(body, status, headers).finish
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,76 @@
1
+ module WebConsole
2
+ # A session lets you persist an +Evaluator+ instance in memory associated
3
+ # with multiple bindings.
4
+ #
5
+ # Each newly created session is persisted into memory and you can find it
6
+ # later by its +id+.
7
+ #
8
+ # A session may be associated with multiple bindings. This is used by the
9
+ # error pages only, as currently, this is the only client that needs to do
10
+ # that.
11
+ class Session
12
+ cattr_reader :inmemory_storage
13
+ @@inmemory_storage = {}
14
+
15
+ class << self
16
+ # Finds a persisted session in memory by its id.
17
+ #
18
+ # Returns a persisted session if found in memory.
19
+ # Raises NotFound error unless found in memory.
20
+ def find(id)
21
+ inmemory_storage[id]
22
+ end
23
+
24
+ # Create a Session from an binding or exception in a storage.
25
+ #
26
+ # The storage is expected to respond to #[]. The binding is expected in
27
+ # :__web_console_binding and the exception in :__web_console_exception.
28
+ #
29
+ # Can return nil, if no binding or exception have been preserved in the
30
+ # storage.
31
+ def from(storage)
32
+ if exc = storage[:__web_console_exception]
33
+ new(ExceptionMapper.new(exc))
34
+ elsif binding = storage[:__web_console_binding]
35
+ new([binding])
36
+ end
37
+ end
38
+ end
39
+
40
+ # An unique identifier for every REPL.
41
+ attr_reader :id
42
+
43
+ def initialize(bindings)
44
+ @id = SecureRandom.hex(16)
45
+ @bindings = bindings
46
+ @evaluator = Evaluator.new(@current_binding = bindings.first)
47
+
48
+ store_into_memory
49
+ end
50
+
51
+ # Evaluate +input+ on the current Evaluator associated binding.
52
+ #
53
+ # Returns a string of the Evaluator output.
54
+ def eval(input)
55
+ @evaluator.eval(input)
56
+ end
57
+
58
+ # Switches the current binding to the one at specified +index+.
59
+ #
60
+ # Returns nothing.
61
+ def switch_binding_to(index)
62
+ @evaluator = Evaluator.new(@current_binding = @bindings[index.to_i])
63
+ end
64
+
65
+ # Returns context of the current binding
66
+ def context(objpath)
67
+ Context.new(@current_binding).extract(objpath)
68
+ end
69
+
70
+ private
71
+
72
+ def store_into_memory
73
+ inmemory_storage[id] = self
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,60 @@
1
+ namespace :ext do
2
+ rootdir = Pathname('extensions')
3
+
4
+ desc 'Build Chrome Extension'
5
+ task chrome: 'chrome:build'
6
+
7
+ namespace :chrome do
8
+ dist = Pathname('dist/crx')
9
+ extdir = rootdir.join(dist)
10
+ manifest_json = rootdir.join('chrome/manifest.json')
11
+
12
+ directory extdir
13
+
14
+ task build: [ extdir, 'lib:templates' ] do
15
+ cd rootdir do
16
+ cp_r [ 'img/', 'tmp/lib/' ], dist
17
+ `cd chrome && git ls-files`.split("\n").each do |src|
18
+ dest = dist.join(src)
19
+ mkdir_p dest.dirname
20
+ cp Pathname('chrome').join(src), dest
21
+ end
22
+ end
23
+ end
24
+
25
+ # Generate a .crx file.
26
+ task crx: [ :build, :npm ] do
27
+ out = "crx-web-console-#{JSON.parse(File.read(manifest_json))["version"]}.crx"
28
+ cd(extdir) { sh "node \"$(npm bin)/crx\" pack ./ -p ../crx-web-console.pem -o ../#{out}" }
29
+ end
30
+
31
+ # Generate a .zip file for Chrome Web Store.
32
+ task zip: [ :build ] do
33
+ version = JSON.parse(File.read(manifest_json))["version"]
34
+ cd(extdir) { sh "zip -r ../crx-web-console-#{version}.zip ./" }
35
+ end
36
+
37
+ desc 'Launch a browser with the chrome extension.'
38
+ task run: [ :build ] do
39
+ cd(rootdir) { sh "sh ./script/run_chrome.sh --load-extension=#{dist}" }
40
+ end
41
+ end
42
+
43
+ task :npm do
44
+ cd(rootdir) { sh "npm install --silent" }
45
+ end
46
+
47
+ 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'))
51
+ dirs = js_erb.pathmap("%{^#{templates},#{tmplib}}d")
52
+
53
+ task templates: dirs + js_erb.pathmap("%{^#{templates},#{tmplib}}X")
54
+
55
+ dirs.each { |d| directory d }
56
+ rule '.js' => [ "%{^#{tmplib},#{templates}}X.js.erb" ] do |t|
57
+ File.write(t.name, WebConsole::Testing::ERBPrecompiler.new(t.source).build)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,54 @@
1
+ namespace :templates do
2
+ desc 'Run tests for templates'
3
+ task test: [ :daemonize, :npm, :rackup, :wait, :mocha, :kill, :exit ]
4
+ task serve: [ :npm, :rackup ]
5
+
6
+ workdir = Pathname(EXPANDED_CWD).join('test/templates')
7
+ pid = Pathname(Dir.tmpdir).join("web_console_test.pid")
8
+ runner = URI.parse("http://#{ENV['IP'] || '127.0.0.1'}:#{ENV['PORT'] || 29292}/html/test_runner.html")
9
+ rackup = "rackup --host #{runner.host} --port #{runner.port}"
10
+ result = nil
11
+ browser = 'phantomjs'
12
+
13
+ def need_to_wait?(uri)
14
+ Net::HTTP.start(uri.host, uri.port) { |http| http.get(uri.path) }
15
+ rescue Errno::ECONNREFUSED
16
+ retry if yield
17
+ end
18
+
19
+ task :daemonize do
20
+ rackup += " -D --pid #{pid}"
21
+ end
22
+
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
32
+ end
33
+
34
+ task :rackup do
35
+ Dir.chdir(workdir) { system rackup }
36
+ end
37
+
38
+ task :wait do
39
+ cnt = 0
40
+ need_to_wait?(runner) { sleep 1; cnt += 1; cnt < 5 }
41
+ end
42
+
43
+ task :mocha do
44
+ Dir.chdir(workdir) { result = system("#{browser} ./node_modules/mocha-phantomjs-core/mocha-phantomjs-core.js #{runner} dot") }
45
+ end
46
+
47
+ task :kill do
48
+ system "kill #{File.read pid}"
49
+ end
50
+
51
+ task :exit do
52
+ exit result
53
+ end
54
+ end