├── .rvmrc ├── lib ├── rack-webconsole.rb └── rack │ ├── webconsole │ ├── version.rb │ ├── railtie.rb │ ├── sandbox.rb │ ├── shell.rb │ ├── asset_helpers.rb │ ├── assets.rb │ └── repl.rb │ └── webconsole.rb ├── public ├── jquery.html ├── webconsole.html ├── webconsole.css └── webconsole.js ├── .gitignore ├── Gemfile ├── spec ├── spec_helper.rb └── rack │ ├── webconsole │ ├── sandbox_spec.rb │ ├── asset_helpers_spec.rb │ ├── shell_spec.rb │ ├── assets_spec.rb │ └── repl_spec.rb │ └── webconsole_spec.rb ├── .travis.yml ├── Rakefile ├── Gemfile.lock ├── History ├── rack-webconsole.gemspec └── Readme.md /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm --create use ruby-1.9.3@rack-webconsole 2 | -------------------------------------------------------------------------------- /lib/rack-webconsole.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'rack' 3 | require 'rack/webconsole' 4 | -------------------------------------------------------------------------------- /public/jquery.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | pkg/* 4 | graph.png 5 | .yardoc/* 6 | doc/* 7 | coverage/* 8 | *.rbc 9 | tags 10 | -------------------------------------------------------------------------------- /lib/rack/webconsole/version.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Rack 3 | class Webconsole 4 | # rack-webconsole version number. 5 | VERSION = "0.1.4" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in rack-webconsole.gemspec 4 | gemspec 5 | gem 'rake' 6 | gem 'mocha' #, :git => 'git://github.com/floehopper/mocha.git' 7 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | gem 'minitest' 4 | require 'minitest/spec' 5 | require 'minitest/autorun' 6 | require 'purdytest' 7 | require 'mocha' 8 | 9 | require 'rack-webconsole' 10 | 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/travis-ci/travis-ci/wiki/.travis.yml-options 2 | rvm: 3 | - 1.8.7 # (current default) 4 | - 1.9.2 5 | - 1.9.3 6 | - ree 7 | - ruby-head 8 | - rbx-18mode 9 | - rbx-19mode 10 | -------------------------------------------------------------------------------- /public/webconsole.html: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /lib/rack/webconsole/railtie.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Rack 3 | class Webconsole 4 | # Railtie loaded in Rails applications. Its purpose is to automatically use 5 | # the middleware in development environment, so that Rails users only have 6 | # to require 'rack-webconsole' in their Gemfile and nothing more than that. 7 | # 8 | class Railtie < Rails::Railtie 9 | initializer 'rack-webconsole.add_middleware' do |app| 10 | app.middleware.use Rack::Webconsole if Rails.env.development? 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | require 'rake/testtask' 5 | desc "Run rack-webconsole specs" 6 | Rake::TestTask.new do |t| 7 | t.libs << "spec" 8 | t.test_files = FileList['spec/**/*_spec.rb'] 9 | t.verbose = true 10 | end 11 | 12 | require 'yard' 13 | YARD::Rake::YardocTask.new(:docs) do |t| 14 | t.files = ['lib/**/*.rb'] 15 | t.options = ['-m', 'markdown', '--no-private', '-r', 'Readme.md', '--title', 'rack-webconsole documentation'] 16 | end 17 | task :doc => [:docs] 18 | 19 | desc "Generate and open class diagram (needs Graphviz installed)" 20 | task :graph do |t| 21 | `bundle exec yard graph -d --full --no-private | dot -Tpng -o graph.png && open graph.png` 22 | end 23 | task :default => [:test] 24 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | rack-webconsole (0.1.4) 5 | multi_json (~> 1.0.3) 6 | rack 7 | ripl (~> 0.5.1) 8 | ripl-multi_line (~> 0.3.0) 9 | 10 | GEM 11 | remote: http://rubygems.org/ 12 | specs: 13 | bluecloth (2.2.0) 14 | bond (0.4.1) 15 | metaclass (0.0.1) 16 | minitest (2.11.4) 17 | mocha (0.10.5) 18 | metaclass (~> 0.0.1) 19 | multi_json (1.0.4) 20 | purdytest (1.0.0) 21 | minitest (~> 2.2) 22 | rack (1.4.1) 23 | rake (0.9.2.2) 24 | ripl (0.5.1) 25 | bond (~> 0.4.0) 26 | ripl-multi_line (0.3.0) 27 | ripl (>= 0.3.6) 28 | yard (0.7.5) 29 | 30 | PLATFORMS 31 | ruby 32 | 33 | DEPENDENCIES 34 | bluecloth 35 | minitest 36 | mocha 37 | purdytest 38 | rack-webconsole! 39 | rake 40 | yard 41 | -------------------------------------------------------------------------------- /History: -------------------------------------------------------------------------------- 1 | === 0.1.4 / 2012-09-14 2 | 3 | + Prepend custom context to the route (Tobias Crawley) 4 | 5 | === 0.1.3 / 2012-03-22 6 | 7 | + Make keycode configurable (Roger Leite) 8 | + Switch to multi_json (Josh Buddy) 9 | ! Fix markup to make it work in more browsers 10 | 11 | === 0.1.2 / 2011-08-01 12 | 13 | + Change field name to avoid conflicts with other forms (Chris Apolzon) 14 | + Prevent visual flash of unstyled content (Rob Cameron) 15 | + Fix minor styling issues (Jeff Kreeftmeijer) 16 | + Avoid conflicts with libraries other than JQuery (Jo Liss) 17 | 18 | === 0.1.1 / 2011-07-27 19 | 20 | ! Fix bug with Content-Length not being calculated appropriately. (Corin Langosch) 21 | + Refactor JavaScript to avoid messing with prototypes (Corin Langosch) 22 | 23 | === 0.1.0 / 2011-07-27 24 | 25 | + The request object is now exposed in the console through #request method 26 | + Various UI enhancements 27 | ! Fix bug where Sandbox locals were much more than those defined by the user. 28 | 29 | === 0.0.5 / 2011-07-26 30 | 31 | ! Protection against CSRF attacks. 32 | -------------------------------------------------------------------------------- /lib/rack/webconsole/sandbox.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Rack 3 | class Webconsole 4 | # A sandbox to evaluate Ruby in. It is responsible for retrieving local 5 | # variables stored in `@locals`, and resetting the environment. 6 | # 7 | class Sandbox 8 | # Catches all the undefined local variables and tries to retrieve them 9 | # from `@locals`. If it doesn't find them, it falls back to the default 10 | # method missing behavior. 11 | def method_missing(method, *args, &block) 12 | @locals ||= {} 13 | @locals[method.to_sym] || super(method, *args, &block) 14 | end 15 | 16 | # Makes the console use a fresh, new {Sandbox} with all local variables 17 | # resetted. 18 | # 19 | # @return [String] 'ok' to make the user notice. 20 | def reload! 21 | $sandbox = Sandbox.new 22 | 'ok' 23 | end 24 | 25 | # Returns the current page request object for inspection purposes. 26 | # 27 | # @return [Rack::Request] the current page request object. 28 | def request 29 | Webconsole::Repl.request 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/rack/webconsole/sandbox_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Rack 4 | describe Webconsole::Sandbox do 5 | 6 | describe "#method_missing" do 7 | describe 'when the method exists in @locals' do 8 | it 'retrieves it' do 9 | @sandbox = Webconsole::Sandbox.new 10 | @sandbox.instance_variable_set(:@locals, {:a => 123}) 11 | 12 | @sandbox.a.must_equal 123 13 | end 14 | end 15 | describe 'otherwise' do 16 | it 'raises a NoMethodError' do 17 | @sandbox = Webconsole::Sandbox.new 18 | 19 | lambda { 20 | @sandbox.a 21 | }.must_raise NoMethodError 22 | end 23 | end 24 | end 25 | 26 | describe "#reload!" do 27 | it 'assigns a new, fresh Sandbox to the global variable' do 28 | old_sandbox = $sandbox = Webconsole::Sandbox.new 29 | 30 | $sandbox.reload! 31 | 32 | $sandbox.wont_equal old_sandbox 33 | end 34 | it 'returns a feedback string' do 35 | Webconsole::Sandbox.new.reload!.must_equal 'ok' 36 | end 37 | end 38 | 39 | describe "request" do 40 | it 'returns the request object' do 41 | @sandbox = Webconsole::Sandbox.new 42 | request = Rack::Request.new({'PATH_INFO' => '/some_path'}) 43 | Webconsole::Repl.request = request 44 | 45 | @sandbox.request.must_equal request 46 | end 47 | end 48 | 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /rack-webconsole.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "rack/webconsole/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "rack-webconsole" 7 | s.version = Rack::Webconsole::VERSION 8 | s.authors = ["Josep M. Bach", "Josep Jaume Rey", "Oriol Gual"] 9 | s.email = ["info@codegram.com"] 10 | s.homepage = "http://github.com/codegram/rack-webconsole" 11 | s.summary = %q{Rack-based console inside your web applications} 12 | s.description = %q{Rack-based console inside your web applications} 13 | 14 | s.rubyforge_project = "rack-webconsole" 15 | 16 | s.add_runtime_dependency 'rack' 17 | s.add_runtime_dependency 'multi_json', '>= 1.0.3', '~> 1.0' 18 | s.add_runtime_dependency 'ripl', '~> 0.5.1' 19 | s.add_runtime_dependency 'ripl-multi_line', '~> 0.3.0' 20 | 21 | s.add_development_dependency 'minitest' 22 | s.add_development_dependency 'purdytest' 23 | 24 | # Since we can't have a git dependency in gemspec, we specify this 25 | # dependency directly in the Gemfile. Once a new mocha version is released, 26 | # we should uncomment this line and remove mocha from the Gemfile. 27 | # s.add_development_dependency 'mocha' 28 | 29 | s.add_development_dependency 'yard' 30 | s.add_development_dependency 'bluecloth' 31 | s.add_development_dependency 'rake' 32 | 33 | s.files = `git ls-files`.split("\n") 34 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 35 | s.require_paths = ["lib"] 36 | end 37 | -------------------------------------------------------------------------------- /spec/rack/webconsole/asset_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'spec_helper' 3 | 4 | class AssetClass 5 | include Rack::Webconsole::AssetHelpers 6 | end 7 | 8 | module Rack 9 | describe Webconsole::AssetHelpers do 10 | 11 | describe '#html_code' do 12 | it 'loads the html code' do 13 | asset_class = AssetClass.new 14 | html = asset_class.html_code 15 | 16 | html.must_match /console/ 17 | html.must_match /results/ 18 | html.must_match /form/ 19 | end 20 | end 21 | 22 | describe '#css_code' do 23 | it 'loads the css code' do 24 | asset_class = AssetClass.new 25 | css = asset_class.css_code 26 | 27 | css.must_match /' 34 | end 35 | 36 | # Loads the JavaScript from a file in `/public`. 37 | # 38 | # It contains the JavaScript logic of the webconsole. 39 | # 40 | # @return [String] the injectable JavaScript. 41 | def js_code 42 | '' 45 | end 46 | 47 | # Inteprolates the given variables inside the javascrpt code 48 | # 49 | # @param [String] javascript The javascript code to insert the variables 50 | # @param [Hash] variables A hash containing the variables names (as keys) 51 | # and its values 52 | # 53 | # @return [String] the javascript code with the interpolated variables 54 | def render(javascript, variables = {}) 55 | javascript_with_variables = javascript.dup 56 | variables.each_pair do |variable, value| 57 | javascript_with_variables.gsub!("$#{variable}", value) 58 | end 59 | javascript_with_variables 60 | end 61 | 62 | private 63 | 64 | def asset(file) 65 | @assets ||= {} 66 | output = ::File.open(::File.join(::File.dirname(__FILE__), '..', '..', '..', 'public', file), 'r:UTF-8') do |f| 67 | f.read 68 | end 69 | @assets[file] ||= output 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/rack/webconsole/assets.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Rack 3 | class Webconsole 4 | # {Assets} is a Rack middleware responsible for injecting view code for the 5 | # console to work properly. 6 | # 7 | # It intercepts HTTP requests, detects successful HTML responses and 8 | # injects HTML, CSS and JavaScript code into those. 9 | # 10 | class Assets 11 | include Webconsole::AssetHelpers 12 | 13 | # Honor the Rack contract by saving the passed Rack application in an ivar. 14 | # 15 | # @param [Rack::Application] app the previous Rack application in the 16 | # middleware chain. 17 | def initialize(app) 18 | @app = app 19 | end 20 | 21 | # Checks for successful HTML responses and injects HTML, CSS and 22 | # JavaScript code into them. 23 | # 24 | # @param [Hash] env a Rack request environment. 25 | def call(env) 26 | status, headers, response = @app.call(env) 27 | return [status, headers, response] unless check_html?(headers, response) && status == 200 28 | 29 | response_body = full_body(response) 30 | 31 | # Regenerate the security token 32 | Webconsole::Repl.reset_token 33 | 34 | # Expose the request object to the Repl 35 | Webconsole::Repl.request = Rack::Request.new(env) 36 | 37 | # Inject the html, css and js code to the view 38 | response_body.gsub!('', "#{code(env)}") 39 | 40 | headers['Content-Length'] = response_body.bytesize.to_s 41 | 42 | [status, headers, [response_body]] 43 | end 44 | 45 | # Returns a string with all the HTML, CSS and JavaScript code needed for 46 | # the view. 47 | # 48 | # It puts the security token inside the JavaScript to make AJAX calls 49 | # secure. 50 | # 51 | # @return [String] the injectable code. 52 | def code(env) 53 | html_code << 54 | css_code << 55 | render(js_code, 56 | :TOKEN => Webconsole::Repl.token, 57 | :KEY_CODE => Webconsole.key_code, 58 | :CONTEXT => env['SCRIPT_NAME'] || "") 59 | end 60 | 61 | private 62 | 63 | def check_html?(headers, response) 64 | return false unless headers['Content-Type'] && headers['Content-Type'].include?('text/html') 65 | 66 | full_body(response) =~ %r{}m 67 | end 68 | 69 | def full_body(response) 70 | response_body = "" 71 | response.each { |part| response_body << part } 72 | response_body 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/rack/webconsole/repl.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'multi_json' 3 | require 'digest/sha1' 4 | 5 | module Rack 6 | class Webconsole 7 | # {Repl} is a Rack middleware acting as a Ruby evaluator application. 8 | # 9 | # In a nutshell, it evaluates a string in a {Sandbox} instance stored in an 10 | # evil global variable. Then, to keep the state, it inspects the local 11 | # variables and stores them in an instance variable for further retrieval. 12 | # 13 | class Repl 14 | @@request = nil 15 | @@token = nil 16 | 17 | class << self 18 | # Returns the autogenerated security token 19 | # 20 | # @return [String] the autogenerated token 21 | def token 22 | @@token 23 | end 24 | 25 | # Regenerates the token. 26 | def reset_token 27 | @@token = Digest::SHA1.hexdigest("#{rand(36**8)}#{Time.now}")[4..20] 28 | end 29 | 30 | # Returns the original request for inspection purposes. 31 | # 32 | # @return [Rack::Request] the original request 33 | def request 34 | @@request 35 | end 36 | 37 | # Sets the original request for inspection purposes. 38 | # 39 | # @param [Rack::Request] the original request 40 | def request=(request) 41 | @@request = request 42 | end 43 | end 44 | 45 | # Honor the Rack contract by saving the passed Rack application in an ivar. 46 | # 47 | # @param [Rack::Application] app the previous Rack application in the 48 | # middleware chain. 49 | def initialize(app) 50 | @app = app 51 | end 52 | 53 | # Evaluates a string as Ruby code and returns the evaluated result as 54 | # JSON. 55 | # 56 | # It also stores the {Sandbox} state in a `$sandbox` global variable, with 57 | # its local variables. 58 | # 59 | # @param [Hash] env the Rack request environment. 60 | # @return [Array] a Rack response with status code 200, HTTP headers 61 | # and the evaluated Ruby result. 62 | def call(env) 63 | status, headers, response = @app.call(env) 64 | 65 | req = Rack::Request.new(env) 66 | params = req.params 67 | 68 | return [status, headers, response] unless check_legitimate(req) 69 | 70 | $sandbox ||= Sandbox.new 71 | hash = Shell.eval_query params['query'] 72 | response_body = MultiJson.encode(hash) 73 | headers = {} 74 | headers['Content-Type'] = 'application/json' 75 | headers['Content-Length'] = response_body.bytesize.to_s 76 | [200, headers, [response_body]] 77 | end 78 | 79 | private 80 | 81 | def check_legitimate(req) 82 | req.post? && !Repl.token.nil? && req.params['token'] == Repl.token 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /public/webconsole.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 3 | var webconsole = { 4 | history:[], 5 | pointer:0, 6 | query:$('#webconsole_query') 7 | } 8 | 9 | $('#rack-webconsole form').submit(function(e){ 10 | e.preventDefault(); 11 | }); 12 | 13 | $("#rack-webconsole form input").keyup(function(event) { 14 | function escapeHTML(string) { 15 | return(string.replace(/&/g,'&'). 16 | replace(/>/g,'>'). 17 | replace(/" + 34 | escapeHTML(data.prompt + webconsole.query.val()) + ""; 35 | if (!data.multi_line) { 36 | result += "
" + escapeHTML("=> " + data.result) + "
"; 37 | } 38 | $("#rack-webconsole .results").append(result); 39 | $("#rack-webconsole .results_wrapper").scrollTop( 40 | $("#rack-webconsole .results").height() 41 | ); 42 | webconsole.query.val(''); 43 | } 44 | }); 45 | } 46 | 47 | // up 48 | if (event.which == 38) { 49 | if (webconsole.pointer < 0) { 50 | webconsole.query.val(''); 51 | } else { 52 | if (webconsole.pointer == webconsole.history.length) { 53 | webconsole.pointer = webconsole.history.length - 1; 54 | } 55 | webconsole.query.val(webconsole.history[webconsole.pointer]); 56 | webconsole.pointer--; 57 | } 58 | } 59 | 60 | // down 61 | if (event.which == 40) { 62 | if (webconsole.pointer == webconsole.history.length) { 63 | webconsole.query.val(''); 64 | } else { 65 | if (webconsole.pointer < 0) { 66 | webconsole.pointer = 0; 67 | } 68 | webconsole.query.val(webconsole.history[webconsole.pointer]); 69 | webconsole.pointer++; 70 | } 71 | } 72 | 73 | }); 74 | 75 | $(document).ready(function() { 76 | $(this).keypress(function(event) { 77 | if (event.which == $KEY_CODE) { 78 | $("#rack-webconsole").slideToggle('fast', function() { 79 | if ($(this).is(':visible')) { 80 | $("#rack-webconsole form input").focus(); 81 | $("#rack-webconsole .results_wrapper").scrollTop( 82 | $("#rack-webconsole .results").height() 83 | ); 84 | } else { 85 | $("#rack-webconsole form input").blur(); 86 | } 87 | }); 88 | event.preventDefault(); 89 | } 90 | }); 91 | }); 92 | })(jQuery); 93 | -------------------------------------------------------------------------------- /lib/rack/webconsole.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'rack/webconsole/repl' 3 | require 'rack/webconsole/asset_helpers' 4 | require 'rack/webconsole/assets' 5 | require 'rack/webconsole/sandbox' 6 | require 'rack/webconsole/shell' 7 | 8 | require 'rack/webconsole/railtie' if defined?(Rails::Railtie) 9 | 10 | # Rack is a modular webserver interface written by Christian Neukirchen. 11 | # 12 | # Learn more at: https://github.com/rack/rack 13 | # 14 | module Rack 15 | # {Rack::Webconsole} is a Rack middleware that provides an interactive 16 | # console à la Rails console, but for any kind of Rack application (Rails, 17 | # Sinatra, Padrino...), accessible from your web application's front-end. 18 | # 19 | # For every request, it normally passes control to the {Assets} middleware, 20 | # which injects needed JavaScript, CSS and HTML code for the console to work 21 | # properly. 22 | # 23 | # It also exposes a special route used by the {Repl}, a Ruby evaluator which 24 | # is responsible of keeping state between requests, remembering local 25 | # variables and giving a true IRB-esque experience. 26 | # 27 | class Webconsole 28 | @@config = {:inject_jquery => false, :key_code => "96"} 29 | 30 | class << self 31 | # Returns whether the Asset injecter must inject JQuery or not. 32 | # 33 | # @return [Boolean] whether to inject JQuery or not. 34 | def inject_jquery 35 | @@config[:inject_jquery] 36 | end 37 | 38 | # Sets whether the Asset injecter must inject JQuery or not. 39 | # 40 | # @param [Boolean] value whether to inject JQuery or not. 41 | def inject_jquery=(value) 42 | @@config[:inject_jquery] = value 43 | end 44 | 45 | # Returns key code used to start web console. 46 | # 47 | # @return [String] key code used at keypress event to start web console. 48 | def key_code 49 | @@config[:key_code] 50 | end 51 | 52 | # Sets key code used to start web console. 53 | # 54 | # @param [String] value key code used at keypress event to start web console. 55 | def key_code=(value) 56 | value = value.to_s unless value.is_a?(String) 57 | @@config[:key_code] = value 58 | end 59 | end 60 | 61 | # Honor the Rack contract by saving the passed Rack application in an ivar. 62 | # 63 | # @param [Rack::Application] app the previous Rack application in the 64 | # middleware chain. 65 | def initialize(app) 66 | @app = app 67 | end 68 | 69 | # Decides where to send the request. In case the path is `/webconsole` 70 | # (e.g. when calling the {Repl} endpoint), pass the request onto the 71 | # {Repl}. Otherwise, pass it onto the {Assets} middleware, which will 72 | # inject the needed assets for the Webconsole to work. 73 | # 74 | # @param [Hash] env a Rack request environment. 75 | def call(env) 76 | if env['PATH_INFO'] == '/webconsole' 77 | Repl.new(@app).call(env) 78 | else 79 | Assets.new(@app).call(env) 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/rack/webconsole/assets_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'spec_helper' 3 | require 'ostruct' 4 | 5 | module Rack 6 | describe Webconsole::Assets do 7 | 8 | it 'initializes with an app' do 9 | @app = stub 10 | @assets = Webconsole::Assets.new(@app) 11 | @assets.instance_variable_get(:@app).must_equal @app 12 | end 13 | 14 | describe "#code" do 15 | it 'injects the token, key_code, and context path' do 16 | Webconsole::Repl.stubs(:token).returns('fake_generated_token') 17 | Webconsole.key_code = "96" 18 | 19 | @assets = Webconsole::Assets.new(nil) 20 | assets_code = @assets.code('SCRIPT_NAME' => '/hambiscuit') 21 | 22 | assets_code.must_match /fake_generated_token/ 23 | assets_code.must_match /event\.which == 96/ 24 | assets_code.must_match %r{/hambiscuit/webconsole} 25 | end 26 | 27 | end 28 | 29 | describe "#call" do 30 | 31 | describe 'when the call is not appropriate to inject the view code' do 32 | # Different invalid cases 33 | [ 34 | [200, {'Content-Type' => 'text/html'}, ['Whatever']], 35 | [200, {'Content-Type' => 'text/plain'}, ['Hello World']], 36 | [404, {'Content-Type' => 'text/html'}, ['Hello World']], 37 | [404, {'Content-Type' => 'text/html'}, ['Hello, World']], 38 | 39 | ].each do |invalid_response| 40 | it 'passes the call untouched' do 41 | @app = lambda { |env| invalid_response } 42 | 43 | assets = Webconsole::Assets.new(@app) 44 | assets.expects(:inject_code).never 45 | 46 | assets.call({}).last.first.must_equal invalid_response.last.first 47 | end 48 | end 49 | end 50 | 51 | describe 'otherwise' do 52 | 53 | it 'injects the view code before the body ending' do 54 | 55 | valid_html = "\n\n\n Testapp\n \n \n \n\n\n\n\n

Hello bitches

\n\n

Lorem ipsum dolor sit amet.

\n\n\n\n\n" 56 | 57 | html = valid_html.split(//) 58 | 59 | @app = lambda { |env| [200, {'Content-Type' => 'text/html'}, html] } 60 | 61 | assets = Webconsole::Assets.new(@app) 62 | response = assets.call({}).last.first 63 | 64 | response.must_match /input name/m # html 65 | response.must_match /text\/css/m # css 66 | response.must_match /escapeHTML/m # js 67 | end 68 | 69 | it 'exposes the request object to the console' do 70 | valid_html = "\n\n\n Testapp\n \n \n \n\n\n\n\n

Hello bitches

\n\n

Lorem ipsum dolor sit amet.

\n\n\n\n\n" 71 | 72 | @app = lambda { |env| [200, {'Content-Type' => 'text/html'}, [valid_html]] } 73 | 74 | env = {'PATH_INFO' => '/some_path'} 75 | assets = Webconsole::Assets.new(@app) 76 | 77 | assets.call(env) 78 | 79 | Webconsole::Repl.request.env['PATH_INFO'].must_equal '/some_path' 80 | end 81 | 82 | end 83 | end 84 | 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /spec/rack/webconsole/repl_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'ostruct' 3 | 4 | module Rack 5 | describe Webconsole::Repl do 6 | 7 | it 'initializes with an app' do 8 | @app = stub 9 | @repl = Webconsole::Repl.new(@app) 10 | 11 | @repl.instance_variable_get(:@app).must_equal @app 12 | end 13 | 14 | describe "#call" do 15 | it 'evaluates the :query param in a sandbox and returns the result' do 16 | @app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['hello world']] } 17 | env = {} 18 | Webconsole::Repl.stubs(:token).returns('abc') 19 | request = OpenStruct.new(:params => {'query' => 'a = 4; a * 2', 'token' => 'abc'}, :post? => true) 20 | Rack::Request.stubs(:new).returns request 21 | 22 | @repl = Webconsole::Repl.new(@app) 23 | 24 | response = @repl.call(env).last.first 25 | 26 | MultiJson.decode(response)['result'].must_equal "8" 27 | end 28 | 29 | it 'maintains local state in subsequent calls thanks to an evil global variable' do 30 | @app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['hello world']] } 31 | env = {} 32 | Webconsole::Repl.stubs(:token).returns('abc') 33 | request = OpenStruct.new(:params => {'query' => 'a = 4', 'token' => 'abc'}, :post? => true) 34 | Rack::Request.stubs(:new).returns request 35 | @repl = Webconsole::Repl.new(@app) 36 | 37 | @repl.call(env) # call 1 sets a to 4 38 | 39 | request = OpenStruct.new(:params => {'query' => 'a * 8', 'token' => 'abc'}, :post? => true) 40 | Rack::Request.stubs(:new).returns request 41 | 42 | response = @repl.call(env).last.first # call 2 retrieves a and multiplies it by 8 43 | 44 | MultiJson.decode(response)['result'].must_equal "32" 45 | $sandbox.instance_variable_get(:@locals)[:a].must_equal 4 46 | $sandbox.instance_variable_get(:@locals).size.must_equal 1 47 | end 48 | 49 | it "returns any found errors prepended with 'Error:'" do 50 | @app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['hello world']] } 51 | env = {} 52 | Webconsole::Repl.stubs(:token).returns('abc') 53 | request = OpenStruct.new(:params => {'query' => 'unknown_method', 'token' => 'abc'}, :post? => true) 54 | Rack::Request.stubs(:new).returns request 55 | @repl = Webconsole::Repl.new(@app) 56 | 57 | response = @repl.call(env).last.first 58 | 59 | MultiJson.decode(response)['result'].must_match /Error:/ 60 | end 61 | 62 | it 'rejects non-post requests' do 63 | @app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['hello world']] } 64 | env = {} 65 | Webconsole::Repl.stubs(:token).returns('abc') 66 | request = OpenStruct.new(:params => {'query' => 'unknown_method', 'token' => 'abc'}, :post? => false) 67 | Rack::Request.stubs(:new).returns request 68 | @repl = Webconsole::Repl.new(@app) 69 | 70 | $sandbox.expects(:instance_eval).never 71 | 72 | @repl.call(env).must_equal @app.call(env) 73 | end 74 | 75 | it 'rejects requests with invalid token' do 76 | @app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['hello world']] } 77 | env = {} 78 | Webconsole::Repl.stubs(:token).returns('abc') 79 | request = OpenStruct.new(:params => {'query' => 'unknown_method', 'token' => 'cba'}, :post? => true) 80 | Rack::Request.stubs(:new).returns request 81 | @repl = Webconsole::Repl.new(@app) 82 | 83 | $sandbox.expects(:instance_eval).never 84 | 85 | @repl.call(env).must_equal @app.call(env) 86 | end 87 | end 88 | 89 | describe 'class methods' do 90 | describe '#reset_token and #token' do 91 | it 'returns the security token' do 92 | Webconsole::Repl.reset_token 93 | Webconsole::Repl.token.must_be_kind_of String 94 | end 95 | end 96 | describe '#request= and #request' do 97 | it 'returns the request object' do 98 | request = stub 99 | Webconsole::Repl.request = request 100 | Webconsole::Repl.request.must_equal request 101 | end 102 | end 103 | end 104 | 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # rack-webconsole [![Build Status](http://travis-ci.org/codegram/rack-webconsole.png)](http://travis-ci.org/codegram/rack-webconsole.png) 2 | 3 | Rack-webconsole is a Rack-based interactive console (à la Rails console) in 4 | your web application's frontend. That means you can interact with your 5 | application's backend from within the browser itself! 6 | 7 | To get a clearer idea, you can check out this video showing a live example :) 8 | 9 | [![YouTube video](http://img.youtube.com/vi/yKK5J01Dqts/0.jpg)](http://youtu.be/yKK5J01Dqts?hd=1) 10 | 11 | Rack-webconsole is a Rack middleware designed to be unobtrusive. With Rails 3, 12 | for example, you only have to include the gem in your Gemfile and it already 13 | works. Without any configuration. 14 | 15 | Tested with MRI versions 1.8.7, 1.9.2, ruby-head, and JRuby 1.6.3. 16 | 17 | **SECURITY NOTE**: From version v0.0.5 rack-webconsole uses a token system to 18 | protect against cross-site request forgery. 19 | 20 | ## Resources 21 | 22 | * [Example video](http://youtu.be/yKK5J01Dqts?hd=1) 23 | * [Documentation](http://rubydoc.info/github/codegram/rack-webconsole) 24 | 25 | 26 | ## Install 27 | 28 | In your Gemfile: 29 | 30 | ```ruby 31 | gem 'rack-webconsole' 32 | ``` 33 | 34 | Rack-webconsole **needs JQuery**. If you are using Rails 3, JQuery is loaded by 35 | default. In case you don't want to use JQuery in your application, 36 | **rack-webconsole can inject it for you** only when it needs it. To do that you 37 | should put this line somewhere in your application (a Rails initializer, or 38 | some configuration file): 39 | 40 | ```ruby 41 | Rack::Webconsole.inject_jquery = true 42 | ``` 43 | 44 | You can also change the javascript key_code used to start webconsole: 45 | 46 | ```ruby 47 | # ` = 96 (default), ^ = 94, ç = 231 ... etc. 48 | Rack::Webconsole.key_code = "231" 49 | ``` 50 | 51 | ## Usage with Rails 3 52 | 53 | If you are using Rails 3, you have no further steps to do. It works! To give 54 | it a try, fire up the Rails server and go to any page, press the ` ` ` key and 55 | the console will show :) 56 | 57 | ## Usage with Sinatra/Padrino 58 | 59 | With Sinatra and Padrino you have to tell your application to use the 60 | middleware: 61 | 62 | ```ruby 63 | require 'sinatra' 64 | require 'rack/webconsole' 65 | 66 | class MySinatraApp < Sinatra::Application 67 | use Rack::Webconsole 68 | # . . . 69 | end 70 | 71 | class SamplePadrino < Padrino::Application 72 | use Rack::Webconsole 73 | # . . . 74 | end 75 | ``` 76 | 77 | NOTE: If you are using Bundler and initializing it from config.ru, you don't 78 | have to `require 'rack/webconsole'` manually, otherwise you have to. 79 | 80 | And it works! Fire up the server, go to any page and press the ` ` ` key. 81 | 82 | ## Usage with Rails 2 83 | 84 | You need to add the following code to an intializer (i.e. config/initializers/webconsole.rb): 85 | 86 | ```ruby 87 | require 'rack/webconsole' 88 | ActionController::Dispatcher.middleware.insert_after 1, Rack::Webconsole 89 | ``` 90 | 91 | ## Commands 92 | 93 | In the console you can issue whatever Ruby commands you want, except multiline commands. Local variables are kept, so you can get a more IRB-esque feeling. 94 | 95 | * `reload!` resets all local variables 96 | * `request` returns the current page request object 97 | 98 | ## Under the hood 99 | 100 | Run the test suite by typing: 101 | 102 | rake 103 | 104 | You can also build the documentation with the following command: 105 | 106 | rake docs 107 | 108 | ## Note on Patches/Pull Requests 109 | 110 | * Fork the project. 111 | * Make your feature addition or bug fix. 112 | * Add tests for it. This is important so we don't break it in a 113 | future version unintentionally. 114 | * Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself we can ignore when we pull) 115 | * Send us a pull request. Bonus points for topic branches. 116 | 117 | ## Released under the MIT License 118 | 119 | Copyright (c) 2011 Codegram. 120 | 121 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 122 | 123 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 124 | 125 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 126 | --------------------------------------------------------------------------------