rvt 0.9.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Rakefile +25 -0
  4. data/app/assets/javascripts/rvt/application.js +1 -0
  5. data/app/assets/javascripts/rvt/console_sessions.js +182 -0
  6. data/app/assets/stylesheets/rvt/application.css +13 -0
  7. data/app/assets/stylesheets/rvt/console_sessions.css.erb +6 -0
  8. data/app/controllers/rvt/application_controller.rb +13 -0
  9. data/app/controllers/rvt/console_sessions_controller.rb +47 -0
  10. data/app/models/rvt/console_session.rb +109 -0
  11. data/app/views/layouts/rvt/application.html.erb +14 -0
  12. data/app/views/rvt/console_sessions/index.html.erb +17 -0
  13. data/config/routes.rb +11 -0
  14. data/lib/assets/javascripts/rvt.js +41 -0
  15. data/lib/rvt.rb +21 -0
  16. data/lib/rvt/colors.rb +87 -0
  17. data/lib/rvt/colors/light.rb +24 -0
  18. data/lib/rvt/colors/monokai.rb +24 -0
  19. data/lib/rvt/colors/solarized.rb +47 -0
  20. data/lib/rvt/colors/tango.rb +24 -0
  21. data/lib/rvt/colors/xterm.rb +24 -0
  22. data/lib/rvt/engine.rb +85 -0
  23. data/lib/rvt/slave.rb +147 -0
  24. data/lib/rvt/version.rb +3 -0
  25. data/test/controllers/rvt/console_sessions_controller_test.rb +100 -0
  26. data/test/dummy/Rakefile +6 -0
  27. data/test/dummy/app/assets/javascripts/application.js +13 -0
  28. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  29. data/test/dummy/app/controllers/application_controller.rb +5 -0
  30. data/test/dummy/app/helpers/application_helper.rb +2 -0
  31. data/test/dummy/app/views/controller_helper_test/index.html.erb +1 -0
  32. data/test/dummy/app/views/exception_test/xhr.html.erb +1 -0
  33. data/test/dummy/app/views/helper_test/index.html.erb +220 -0
  34. data/test/dummy/app/views/layouts/application.html.erb +16 -0
  35. data/test/dummy/bin/bundle +3 -0
  36. data/test/dummy/bin/rails +4 -0
  37. data/test/dummy/bin/rake +4 -0
  38. data/test/dummy/config.ru +4 -0
  39. data/test/dummy/config/application.rb +44 -0
  40. data/test/dummy/config/boot.rb +5 -0
  41. data/test/dummy/config/database.yml +25 -0
  42. data/test/dummy/config/environment.rb +5 -0
  43. data/test/dummy/config/environments/development.rb +29 -0
  44. data/test/dummy/config/environments/production.rb +80 -0
  45. data/test/dummy/config/environments/test.rb +34 -0
  46. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  47. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  48. data/test/dummy/config/initializers/inflections.rb +16 -0
  49. data/test/dummy/config/initializers/mime_types.rb +5 -0
  50. data/test/dummy/config/initializers/secret_token.rb +12 -0
  51. data/test/dummy/config/initializers/session_store.rb +3 -0
  52. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  53. data/test/dummy/config/locales/en.yml +23 -0
  54. data/test/dummy/config/routes.rb +2 -0
  55. data/test/dummy/db/development.sqlite3 +0 -0
  56. data/test/dummy/db/schema.rb +16 -0
  57. data/test/dummy/db/test.sqlite3 +0 -0
  58. data/test/dummy/log/development.log +247222 -0
  59. data/test/dummy/log/test.log +963 -0
  60. data/test/dummy/public/404.html +58 -0
  61. data/test/dummy/public/422.html +58 -0
  62. data/test/dummy/public/500.html +57 -0
  63. data/test/dummy/public/favicon.ico +0 -0
  64. data/test/dummy/tmp/cache/assets/development/sprockets/0509a6e0e75d9ac5a88bba7291b04686 +0 -0
  65. data/test/dummy/tmp/cache/assets/development/sprockets/2bd9d10dae311aa2dfb6dea9c1e0ad50 +0 -0
  66. data/test/dummy/tmp/cache/assets/development/sprockets/2f41a6a41d0a16db31cabd2b8689ca00 +0 -0
  67. data/test/dummy/tmp/cache/assets/development/sprockets/4de66e84a0d6f009fac50cd608fb8581 +0 -0
  68. data/test/dummy/tmp/cache/assets/development/sprockets/56e8026311075410507152df2aea4307 +0 -0
  69. data/test/dummy/tmp/cache/assets/development/sprockets/9542de15712d45f70221931bf78c00e5 +0 -0
  70. data/test/dummy/tmp/cache/assets/development/sprockets/98cea396a602c53d876e79bf4b89de3b +0 -0
  71. data/test/dummy/tmp/cache/assets/development/sprockets/9df3968b0f171feec749766fffa50b2e +0 -0
  72. data/test/dummy/tmp/cache/assets/development/sprockets/aa5fc4cb46c5294192c9d3af8824f88b +0 -0
  73. data/test/dummy/tmp/cache/assets/development/sprockets/ac7197dc7dd9ab362d915d19e37a8e01 +0 -0
  74. data/test/dummy/tmp/cache/assets/development/sprockets/b238befd6eff46b26d56322c1450ea45 +0 -0
  75. data/test/dummy/tmp/cache/assets/development/sprockets/c69b8b79c21fd48ad84c3fad87945a5c +0 -0
  76. data/test/dummy/tmp/cache/assets/development/sprockets/da4318a4d8364d0616edd706508371bc +0 -0
  77. data/test/dummy/tmp/cache/assets/development/sprockets/dd71b14df42fd6dc95355cded9e4d0f9 +0 -0
  78. data/test/dummy/tmp/cache/assets/development/sprockets/eb06cae1627276e46965809e766aff13 +0 -0
  79. data/test/dummy/tmp/cache/assets/development/sprockets/edffb5017d27ddb65965203636286405 +0 -0
  80. data/test/dummy/tmp/cache/assets/development/sprockets/f0ced5f3d4a75fce1c596d6c3f97a42d +0 -0
  81. data/test/dummy/tmp/cache/assets/development/sprockets/fb9e611526e612ba797f0c11b987826b +0 -0
  82. data/test/models/console_session_test.rb +58 -0
  83. data/test/rvt/colors_test.rb +58 -0
  84. data/test/rvt/engine_test.rb +145 -0
  85. data/test/rvt/slave_test.rb +72 -0
  86. data/test/test_helper.rb +27 -0
  87. data/vendor/assets/javascripts/term.js +5771 -0
  88. metadata +284 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 40c0eb5241121c94ef06e819351e3622dec19f87
4
+ data.tar.gz: 33f5a3b57e0f16bddda6cf8568d0a126403fd9e9
5
+ SHA512:
6
+ metadata.gz: 473f3b32b24b91a76f45a46b19f557b0f96a3bb8f5ec3040bf2d1a09f265d662d3952875a8aec3cf45e045bb3683c7e6c8d9229382cd063399627c0ab5033344
7
+ data.tar.gz: 8decfdb59c9293d84bac29d98fe546d3da4ef8e944d76c688e158e3284db8a6289933113c3aaed683f02c94c3d7e1451b720e9888e30c1a6711ecde5e7b9f85d
@@ -0,0 +1,20 @@
1
+ Copyright 2015 Genadi Samokovarov, Guillermo Iguaran
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,25 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'socket'
8
+ require 'active_support/core_ext/string/strip'
9
+ require 'rake/testtask'
10
+
11
+ EXPANDED_CWD = File.expand_path(File.dirname(__FILE__))
12
+
13
+ Rake::TestTask.new(:test) do |t|
14
+ t.libs << 'lib'
15
+ t.libs << 'test'
16
+ t.pattern = 'test/**/*_test.rb'
17
+ t.verbose = false
18
+ end
19
+
20
+ # Just ignore this if rake is not runned from the current directory, as is the
21
+ # case with docker's container. BUNDLE_GEMFILE won't do for our case, since the
22
+ # Gemfile references gemspec.
23
+ Bundler::GemHelper.install_tasks if defined? Bundler::GemHelper
24
+
25
+ task default: :test
@@ -0,0 +1 @@
1
+ //= require_tree .
@@ -0,0 +1,182 @@
1
+ //= require rvt
2
+
3
+ var AJAXTransport = (function(RVT) {
4
+
5
+ var inherits = RVT.inherits;
6
+ var EventEmitter = RVT.EventEmitter;
7
+
8
+ var FORM_MIME_TYPE = 'application/x-www-form-urlencoded; charset=utf-8';
9
+
10
+ var AJAXTransport = function(options) {
11
+ EventEmitter.call(this);
12
+ options || (options = {});
13
+
14
+ this.url = (typeof options.url === 'string') ? {
15
+ input: options.url,
16
+ pendingOutput: options.url,
17
+ configuration: options.url
18
+ } : options.url;
19
+
20
+ this.uid = options.uid;
21
+ this.pendingInput = '';
22
+
23
+ this.initializeEventHandlers();
24
+ };
25
+
26
+ inherits(AJAXTransport, EventEmitter);
27
+
28
+ // Initializes the default event handlers.
29
+ AJAXTransport.prototype.initializeEventHandlers = function() {
30
+ this.on('input', this.sendInput);
31
+ this.on('configuration', this.sendConfiguration);
32
+ this.once('initialization', function(cols, rows) {
33
+ this.emit('configuration', cols, rows);
34
+ this.pollForPendingOutput();
35
+ });
36
+ };
37
+
38
+ // Shorthand for creating XHR requests.
39
+ AJAXTransport.prototype.createRequest = function(method, url, options) {
40
+ options || (options = {});
41
+
42
+ var params = '';
43
+
44
+ if (typeof options.form === 'object') {
45
+ var content = [], form = options.form;
46
+
47
+ for (var key in form) {
48
+ var value = form[key];
49
+ content.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
50
+ }
51
+
52
+ params = content.join('&');
53
+ if (method === 'GET' ? '?' : '') params = '?' + params;
54
+ }
55
+
56
+ var request = new XMLHttpRequest;
57
+ request.open(method, method === 'GET' ? url + params : url);
58
+
59
+ if (params && method != 'GET') {
60
+ request.setRequestHeader('Content-Type', FORM_MIME_TYPE);
61
+ request.data = params;
62
+ }
63
+
64
+ return request;
65
+ };
66
+
67
+ AJAXTransport.prototype.pollForPendingOutput = function() {
68
+ var request = this.createRequest('GET', this.url.pendingOutput, {
69
+ form: { uid: this.uid }
70
+ });
71
+
72
+ var self = this;
73
+ request.onreadystatechange = function() {
74
+ if (request.readyState === XMLHttpRequest.DONE) {
75
+ if (request.status === 200) {
76
+ self.emit('pendingOutput', request.responseText);
77
+ self.pollForPendingOutput();
78
+ } else {
79
+ self.emit('disconnect', request);
80
+ }
81
+ }
82
+ };
83
+
84
+ request.send(null);
85
+ };
86
+
87
+ // Send the input to the server.
88
+ //
89
+ // Each key press is encoded to an intermediate format, before it is sent to
90
+ // the server.
91
+ //
92
+ // RVT#keysPressed is an alias for RVT#sendInput.
93
+ AJAXTransport.prototype.sendInput = function(input) {
94
+ input || (input = '');
95
+
96
+ if (this.disconnected) return;
97
+ if (this.sendingInput) return this.pendingInput += input;
98
+
99
+ // Indicate that we are starting to send input.
100
+ this.sendingInput = true;
101
+
102
+ var request = this.createRequest('PUT', this.url.input, {
103
+ form: { input: this.pendingInput + input, uid: this.uid }
104
+ });
105
+
106
+ // Clear the pending input.
107
+ this.pendingInput = '';
108
+
109
+ var self = this;
110
+ request.onreadystatechange = function() {
111
+ if (request.readyState === XMLHttpRequest.DONE) {
112
+ self.sendingInput = false;
113
+ if (self.pendingInput) self.sendInput();
114
+ }
115
+ };
116
+
117
+ request.send(request.data);
118
+ };
119
+
120
+ // Send the terminal configuration to the server.
121
+ //
122
+ // Right now by configuration, we understand the terminal widht and terminal
123
+ // height.
124
+ //
125
+ // RVT#resized is an alias for RVT#sendconfiguration.
126
+ AJAXTransport.prototype.sendConfiguration = function(cols, rows) {
127
+ if (this.disconnected) return;
128
+
129
+ var request = this.createRequest('PUT', this.url.configuration, {
130
+ form: { width: cols, height: rows, uid: this.uid }
131
+ });
132
+
133
+ // Just send the configuration and don't care about any output.
134
+ request.send(request.data);
135
+ };
136
+
137
+ return AJAXTransport;
138
+
139
+ }).call(this, RVT);
140
+
141
+ window.addEventListener('load', function() {
142
+ var geometry = calculateFitScreenGeometry();
143
+ config.terminal.cols = geometry[0];
144
+ config.terminal.rows = geometry[1];
145
+
146
+ var terminal = window.terminal = new RVT.Terminal(config.terminal);
147
+
148
+ terminal.on('data', function(data) {
149
+ transport.emit('input', data);
150
+ });
151
+
152
+ var transport = new AJAXTransport(config.transport);
153
+
154
+ transport.on('pendingOutput', function(response) {
155
+ var json = JSON.parse(response);
156
+ if (json.output) terminal.write(json.output);
157
+ });
158
+
159
+ transport.on('disconnect', function() {
160
+ terminal.destroy();
161
+ });
162
+
163
+ transport.emit('initialization', terminal.cols, terminal.rows);
164
+
165
+ // Utilities
166
+ // ---------
167
+
168
+ function calculateFitScreenGeometry() {
169
+ // Currently, resizing term.js is broken. E.g. opening vi causes it to go
170
+ // back to 80x24 and fail with off-by-one error. Other stuff, like chip8
171
+ // are rendered incorrectly and so on.
172
+ //
173
+ // To work around it, create a temporary terminal, just so we can get the
174
+ // best dimensions for the screen.
175
+ var temporary = new RVT.Terminal;
176
+ try {
177
+ return temporary.fitScreen();
178
+ } finally {
179
+ temporary.destroy();
180
+ }
181
+ }
182
+ });
@@ -0,0 +1,13 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the top of the
9
+ * compiled file, but it's generally better to create a new file per style scope.
10
+ *
11
+ *= require_self
12
+ *= require_tree .
13
+ */
@@ -0,0 +1,6 @@
1
+ <% RVT.config.style.instance_eval do %>
2
+ body { color: <%= colors.foreground %>; background: <%= colors.background %>; margin: 0; padding: 0; }
3
+
4
+ .terminal { float: left; overflow: hidden; font: <%= font %>; }
5
+ .terminal-cursor { color: <%= colors.background %>; background: <%= colors.foreground %>; }
6
+ <% end %>
@@ -0,0 +1,13 @@
1
+ module RVT
2
+ class ApplicationController < ActionController::Base
3
+ before_action :prevent_unauthorized_requests!
4
+
5
+ private
6
+
7
+ def prevent_unauthorized_requests!
8
+ unless request.remote_ip.in?(RVT.config.whitelisted_ips)
9
+ render nothing: true, status: :unauthorized
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,47 @@
1
+ require_dependency 'rvt/application_controller'
2
+
3
+ module RVT
4
+ class ConsoleSessionsController < ApplicationController
5
+ rescue_from ConsoleSession::Unavailable do |exception|
6
+ render json: exception, status: :gone
7
+ end
8
+
9
+ rescue_from ConsoleSession::Invalid do |exception|
10
+ render json: exception, status: :unprocessable_entity
11
+ end
12
+
13
+ rescue_from ConsoleSession::Unauthorized do |exception|
14
+ render json: exception, status: :unauthorized
15
+ end
16
+
17
+ def index
18
+ @console_session = ConsoleSession.create
19
+ end
20
+
21
+ def input
22
+ @console_session = ConsoleSession.find_by_pid_and_uid(params[:id], params[:uid])
23
+ @console_session.send_input(console_session_params[:input])
24
+
25
+ render nothing: true
26
+ end
27
+
28
+ def configuration
29
+ @console_session = ConsoleSession.find_by_pid_and_uid(params[:id], params[:uid])
30
+ @console_session.configure(console_session_params)
31
+
32
+ render nothing: true
33
+ end
34
+
35
+ def pending_output
36
+ @console_session = ConsoleSession.find_by_pid_and_uid(params[:id], params[:uid])
37
+
38
+ render json: { output: @console_session.pending_output }
39
+ end
40
+
41
+ private
42
+
43
+ def console_session_params
44
+ params.permit(:id, :uid, :input, :width, :height)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,109 @@
1
+ module RVT
2
+ # Manage and persist (in memory) RVT::Slave instances.
3
+ class ConsoleSession
4
+ include ActiveModel::Model
5
+
6
+ # In-memory storage for the console sessions. Session preservation is
7
+ # troubled on servers with multiple workers and threads.
8
+ INMEMORY_STORAGE = {}
9
+
10
+ # Base error class for ConsoleSession specific exceptions.
11
+ #
12
+ # Provides #to_json implementation, so all subclasses are JSON
13
+ # serializable.
14
+ class Error < StandardError
15
+ def as_json(*)
16
+ { error: to_s }
17
+ end
18
+ end
19
+
20
+ # Raised when trying to find a session that is no longer in the in-memory
21
+ # session storage or when the slave process exited.
22
+ Unavailable = Class.new(Error)
23
+
24
+ # Raised when an operation transition to an invalid state.
25
+ Invalid = Class.new(Error)
26
+
27
+ # Raised when a request doesn't know the slave process uid.
28
+ Unauthorized = Class.new(Error)
29
+
30
+ class << self
31
+ # Finds a session by its pid.
32
+ #
33
+ # Raises RVT::ConsoleSession::Expired if there is no such session.
34
+ def find(pid)
35
+ INMEMORY_STORAGE[pid.to_i] or raise Unavailable, 'Session unavailable'
36
+ end
37
+
38
+ # Finds a session by its pid.
39
+ #
40
+ # Raises RVT::ConsoleSession::Expired if there is no such session.
41
+ # Raises RVT::ConsoleSession::Unauthorized if uid doesn't match.
42
+ def find_by_pid_and_uid(pid, uid)
43
+ find(pid).tap do |console_session|
44
+ raise Unauthorized if console_session.uid != uid
45
+ end
46
+ end
47
+
48
+ # Creates an already persisted consolse session.
49
+ #
50
+ # Use this method if you need to persist a session, without providing it
51
+ # any input.
52
+ def create
53
+ new.persist
54
+ end
55
+ end
56
+
57
+ def initialize
58
+ @slave = Slave.new
59
+ end
60
+
61
+ # Explicitly persist the model in the in-memory storage.
62
+ def persist
63
+ INMEMORY_STORAGE[pid] = self
64
+ end
65
+
66
+ # Returns true if the current session is persisted in the in-memory storage.
67
+ def persisted?
68
+ self == INMEMORY_STORAGE[pid]
69
+ end
70
+
71
+ # Returns an Enumerable of all key attributes if any is set, regardless if
72
+ # the object is persisted or not.
73
+ def to_key
74
+ [pid] if persisted?
75
+ end
76
+
77
+ private
78
+
79
+ def delegate_and_call_slave_method(name, *args, &block)
80
+ # Cache the delegated method, so we don't have to hit #method_missing
81
+ # on every call.
82
+ define_singleton_method(name) do |*inner_args, &inner_block|
83
+ begin
84
+ @slave.public_send(name, *inner_args, &inner_block)
85
+ rescue ArgumentError => exc
86
+ raise Invalid, exc
87
+ rescue Slave::Closed => exc
88
+ raise Unavailable, exc
89
+ end
90
+ end
91
+
92
+ # Now call the method, since that's our most common use case. Delegate
93
+ # the method and than call it.
94
+ public_send(name, *args, &block)
95
+ end
96
+
97
+ def method_missing(name, *args, &block)
98
+ if @slave.respond_to?(name)
99
+ delegate_and_call_slave_method(name, *args, &block)
100
+ else
101
+ super
102
+ end
103
+ end
104
+
105
+ def respond_to_missing?(name, include_all = false)
106
+ @slave.respond_to?(name) or super
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>RVT</title>
5
+
6
+ <%= stylesheet_link_tag 'rvt/application', media: 'all' %>
7
+ <%= javascript_include_tag 'rvt/application' %>
8
+ <%= csrf_meta_tags %>
9
+ </head>
10
+
11
+ <body>
12
+ <%= yield %>
13
+ </body>
14
+ </html>