tobias-rack-webconsole 0.1.4
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.
- data/.gitignore +9 -0
- data/.rvmrc +1 -0
- data/.travis.yml +9 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +40 -0
- data/History +27 -0
- data/Rakefile +23 -0
- data/Readme.md +125 -0
- data/lib/rack-webconsole.rb +3 -0
- data/lib/rack/webconsole.rb +83 -0
- data/lib/rack/webconsole/asset_helpers.rb +73 -0
- data/lib/rack/webconsole/assets.rb +75 -0
- data/lib/rack/webconsole/railtie.rb +14 -0
- data/lib/rack/webconsole/repl.rb +86 -0
- data/lib/rack/webconsole/sandbox.rb +33 -0
- data/lib/rack/webconsole/shell.rb +74 -0
- data/lib/rack/webconsole/version.rb +7 -0
- data/public/jquery.html +1 -0
- data/public/webconsole.css +86 -0
- data/public/webconsole.html +15 -0
- data/public/webconsole.js +92 -0
- data/rack-webconsole.gemspec +36 -0
- data/spec/rack/webconsole/asset_helpers_spec.rb +56 -0
- data/spec/rack/webconsole/assets_spec.rb +101 -0
- data/spec/rack/webconsole/repl_spec.rb +106 -0
- data/spec/rack/webconsole/sandbox_spec.rb +50 -0
- data/spec/rack/webconsole/shell_spec.rb +73 -0
- data/spec/rack/webconsole_spec.rb +56 -0
- data/spec/spec_helper.rb +10 -0
- metadata +190 -0
@@ -0,0 +1,14 @@
|
|
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
|
@@ -0,0 +1,86 @@
|
|
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
|
@@ -0,0 +1,33 @@
|
|
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
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'ripl'
|
2
|
+
|
3
|
+
class Rack::Webconsole
|
4
|
+
module Shell
|
5
|
+
def self.eval_query(query)
|
6
|
+
# Initialize ripl plugins
|
7
|
+
@before_loop_called ||= Ripl.shell.before_loop
|
8
|
+
|
9
|
+
Ripl.shell.input = query
|
10
|
+
Ripl.shell.loop_once
|
11
|
+
{}.tap do |hash|
|
12
|
+
hash[:result] = Ripl.shell.return_result
|
13
|
+
hash[:multi_line] = Ripl.shell.multi_line?
|
14
|
+
hash[:previous_multi_line] = Ripl.shell.previous_multi_line?
|
15
|
+
hash[:prompt] = Ripl.shell.previous_multi_line? ?
|
16
|
+
Ripl.config[:multi_line_prompt] : Ripl.config[:prompt]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# TODO: move to plugin
|
21
|
+
def multi_line?
|
22
|
+
@buffer.is_a?(Array)
|
23
|
+
end
|
24
|
+
|
25
|
+
def previous_multi_line?
|
26
|
+
@old_buffer.is_a?(Array)
|
27
|
+
end
|
28
|
+
|
29
|
+
def get_input
|
30
|
+
@old_buffer = @buffer
|
31
|
+
history << @input
|
32
|
+
@input
|
33
|
+
end
|
34
|
+
|
35
|
+
def loop_eval(query)
|
36
|
+
# Force conversion to symbols due to issues with lovely 1.8.7
|
37
|
+
boilerplate = local_variables.map(&:to_sym) + [:ls, :result]
|
38
|
+
|
39
|
+
$sandbox.instance_eval """
|
40
|
+
result = (#{query})
|
41
|
+
ls = (local_variables.map(&:to_sym) - [#{boilerplate.map(&:inspect).join(', ')}])
|
42
|
+
@locals ||= {}
|
43
|
+
@locals.update(ls.inject({}) do |hash, value|
|
44
|
+
hash.update({value => eval(value.to_s)})
|
45
|
+
end)
|
46
|
+
result
|
47
|
+
"""
|
48
|
+
end
|
49
|
+
|
50
|
+
def print_eval_error(err)
|
51
|
+
@result = "Error: " + err.message
|
52
|
+
end
|
53
|
+
|
54
|
+
def return_result
|
55
|
+
@error_raised ? result : result.inspect
|
56
|
+
end
|
57
|
+
|
58
|
+
def print_result(result) end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Disable readline's #get_input
|
63
|
+
Ripl.config[:readline] = false
|
64
|
+
Ripl.config[:multi_line_prompt] = '|| '
|
65
|
+
Ripl.config[:prompt] = '>> '
|
66
|
+
Ripl.config[:irbrc] = false
|
67
|
+
# Let ripl users detect web shells
|
68
|
+
Ripl.config[:web] = true
|
69
|
+
|
70
|
+
Ripl::Shell.include Rack::Webconsole::Shell
|
71
|
+
# must come after Webconsole plugin
|
72
|
+
require 'ripl/multi_line'
|
73
|
+
|
74
|
+
at_exit { Ripl.shell.after_loop }
|
data/public/jquery.html
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
<script type="text/javascript" src="http://code.jquery.com/jquery-1.6.2.min.js"></script>
|
@@ -0,0 +1,86 @@
|
|
1
|
+
#rack-webconsole {
|
2
|
+
opacity: 0.9;
|
3
|
+
z-index: 999;
|
4
|
+
background: #000;
|
5
|
+
color: #DDD;
|
6
|
+
font-family: monospace;
|
7
|
+
height: 40%;
|
8
|
+
position: fixed;
|
9
|
+
width: 100%;
|
10
|
+
bottom: 0px;
|
11
|
+
left: 0px;
|
12
|
+
right:0px;
|
13
|
+
outline-top: 3px solid #DEDEDE;
|
14
|
+
box-shadow: 0px -4px 5px rgba(0,0,0,0.5);
|
15
|
+
-moz-box-shadow: 0px -4px 5px rgba(0,0,0,0.5);
|
16
|
+
-webkit-box-shadow: 0px -4px 5px rgba(0,0,0,0.5);
|
17
|
+
font-size: 11px;
|
18
|
+
}
|
19
|
+
#rack-webconsole div.query{
|
20
|
+
margin-top: 10px;
|
21
|
+
font-weight: bold;
|
22
|
+
padding-top: 10px;
|
23
|
+
border-top: 1px dashed #333;
|
24
|
+
margin-bottom: 5px;
|
25
|
+
}
|
26
|
+
#rack-webconsole div.query_multiline{
|
27
|
+
font-weight: bold;
|
28
|
+
margin-bottom: 5px;
|
29
|
+
}
|
30
|
+
#rack-webconsole div.query:first-child{
|
31
|
+
margin-top: 0px;
|
32
|
+
padding-top: 0px;
|
33
|
+
border-top: none;
|
34
|
+
}
|
35
|
+
#rack-webconsole div.result{
|
36
|
+
font-weight: normal;
|
37
|
+
}
|
38
|
+
#rack-webconsole form div, #console form span {
|
39
|
+
font-size: 14px;
|
40
|
+
border: 0px;
|
41
|
+
font-family: monospace;
|
42
|
+
color: #FFF;
|
43
|
+
}
|
44
|
+
#rack-webconsole form div.results_wrapper{
|
45
|
+
width: 100%;
|
46
|
+
position: absolute;
|
47
|
+
overflow-x: auto;
|
48
|
+
top: 0;
|
49
|
+
bottom: 40px;
|
50
|
+
}
|
51
|
+
#rack-webconsole form div.results{
|
52
|
+
padding: 10px;
|
53
|
+
}
|
54
|
+
#rack-webconsole .prompt{
|
55
|
+
width: 30px;
|
56
|
+
text-align: center;
|
57
|
+
display: block;
|
58
|
+
float: left;
|
59
|
+
height: 25px;
|
60
|
+
line-height: 25px;
|
61
|
+
}
|
62
|
+
#rack-webconsole form div.input{
|
63
|
+
width: 100%;
|
64
|
+
position: absolute;
|
65
|
+
bottom: 0px;
|
66
|
+
background: #000;
|
67
|
+
}
|
68
|
+
#rack-webconsole form div.input input{
|
69
|
+
-webkit-box-sizing: border-box;
|
70
|
+
-moz-box-sizing: border-box;
|
71
|
+
box-sizing: border-box;
|
72
|
+
margin-top: 0px;
|
73
|
+
margin-bottom: 0px;
|
74
|
+
padding: 0px;
|
75
|
+
width: 100%;
|
76
|
+
font-size: 14px;
|
77
|
+
background: transparent;
|
78
|
+
border: 0px;
|
79
|
+
font-family: monospace;
|
80
|
+
color: #FFF;
|
81
|
+
}
|
82
|
+
#rack-webconsole .input .input_box{
|
83
|
+
margin-left: 30px;
|
84
|
+
margin-right: 10px;
|
85
|
+
display: block;
|
86
|
+
}
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<div id="rack-webconsole" style="display:none">
|
2
|
+
<form accept-charset="UTF-8" action="/webconsole" method="post">
|
3
|
+
<input name="utf8" type="hidden" value="✓"/>
|
4
|
+
<div class="results_wrapper">
|
5
|
+
<div class="results">
|
6
|
+
</div>
|
7
|
+
</div>
|
8
|
+
<div class="input">
|
9
|
+
<span class="prompt">>></span>
|
10
|
+
<span class="input_box">
|
11
|
+
<input id="webconsole_query" name="webconsole_query" type="text" />
|
12
|
+
</span>
|
13
|
+
</div>
|
14
|
+
</form>
|
15
|
+
</div>
|
@@ -0,0 +1,92 @@
|
|
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(/</g,'<').
|
18
|
+
replace(/"/g,'"')
|
19
|
+
);
|
20
|
+
};
|
21
|
+
|
22
|
+
// enter
|
23
|
+
if (event.which == 13) {
|
24
|
+
webconsole.history.push(webconsole.query.val());
|
25
|
+
webconsole.pointer = webconsole.history.length - 1;
|
26
|
+
$.ajax({
|
27
|
+
url: '$CONTEXT/webconsole',
|
28
|
+
type: 'POST',
|
29
|
+
dataType: 'json',
|
30
|
+
data: ({query: webconsole.query.val(), token: "$TOKEN"}),
|
31
|
+
success: function (data) {
|
32
|
+
var query_class = data.previous_multi_line ? 'query_multiline' : 'query';
|
33
|
+
var result = "<div class='" + query_class + "'>" +
|
34
|
+
escapeHTML(data.prompt + webconsole.query.val()) + "</div>";
|
35
|
+
if (!data.multi_line) {
|
36
|
+
result += "<div class='result'>" + escapeHTML("=> " + data.result) + "</div>";
|
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);
|
@@ -0,0 +1,36 @@
|
|
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 = "tobias-rack-webconsole"
|
7
|
+
s.version = Rack::Webconsole::VERSION
|
8
|
+
s.authors = ["Josep M. Bach", "Josep Jaume Rey", "Oriol Gual"]
|
9
|
+
s.email = ["toby@tcrawley.org"]
|
10
|
+
s.homepage = "http://github.com/tobias/rack-webconsole"
|
11
|
+
s.summary = %q{Rack-based console inside your web applications - this is a fork of http://github.com/codegram/rack-webconsole}
|
12
|
+
s.description = s.summary
|
13
|
+
|
14
|
+
s.rubyforge_project = "rack-webconsole"
|
15
|
+
|
16
|
+
s.add_runtime_dependency 'rack'
|
17
|
+
s.add_runtime_dependency 'multi_json', '~> 1.0.3'
|
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
|