web-console-compat 3.5.1

Sign up to get free protection for your applications and to get access to all the features.
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