pssh 0.2.2

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.
@@ -0,0 +1,134 @@
1
+ /*
2
+ WSSH Javascript Client
3
+
4
+ Usage:
5
+
6
+ var client = new WSSHClient();
7
+
8
+ client.connect({
9
+ // Connection and authentication parameters
10
+ username: 'root',
11
+ hostname: 'localhost',
12
+ authentication_method: 'password', // can either be password or private_key
13
+ password: 'secretpassword', // do not provide when using private_key
14
+ key_passphrase: 'secretpassphrase', // *may* be provided if the private_key is encrypted
15
+
16
+ // Callbacks
17
+ onError: function(error) {
18
+ // Called upon an error
19
+ console.error(error);
20
+ },
21
+ onConnect: function() {
22
+ // Called after a successful connection to the server
23
+ console.debug('Connected!');
24
+
25
+ client.send('ls\n'); // You can send data back to the server by using WSSHClient.send()
26
+ },
27
+ onClose: function() {
28
+ // Called when the remote closes the connection
29
+ console.debug('Connection Reset By Peer');
30
+ },
31
+ onData: function(data) {
32
+ // Called when data is received from the server
33
+ console.debug('Received: ' + data);
34
+ }
35
+ });
36
+
37
+ */
38
+
39
+ function WSSHClient(uuid) {
40
+ this.uuid=uuid;
41
+ };
42
+
43
+ WSSHClient.prototype._generateEndpoint = function(options) {
44
+ if (window.location.protocol == 'https:') {
45
+ var protocol = 'wss://';
46
+ } else {
47
+ var protocol = 'ws://';
48
+ }
49
+ var endpoint = protocol + window.location.host +
50
+ '/socket?uuid=' + this.uuid; ///' + encodeURIComponent(options.hostname) + '/' +
51
+ //encodeURIComponent(options.username);
52
+ /*if (options.authentication_method == 'password') {
53
+ endpoint += '?password=' + encodeURIComponent(options.password);
54
+ } else if (options.authentication_method == 'private_key') {
55
+ endpoint += '?private_key=' + encodeURIComponent(options.private_key);
56
+ if (options.key_passphrase !== undefined)
57
+ endpoint += '&key_passphrase=' + encodeURIComponent(
58
+ options.key_passphrase);
59
+ }*/
60
+ return endpoint;
61
+ };
62
+
63
+ WSSHClient.prototype.startSocket = function() {
64
+
65
+ var ping;
66
+ var _self = this;
67
+ var connected = true;
68
+
69
+ if (window.WebSocket) {
70
+ this._connection = new WebSocket(this.endpoint);
71
+ }
72
+ else if (window.MozWebSocket) {
73
+ this._connection = MozWebSocket(this.endpoint);
74
+ }
75
+ else {
76
+ this.options.onError('WebSocket Not Supported');
77
+ return ;
78
+ }
79
+
80
+ this._connection.onopen = function() {
81
+ console.log("connected");
82
+ ping = window.setInterval(function() {
83
+ _self._connection.send('p');
84
+ }, 10000);
85
+ _self.options.onConnect();
86
+ };
87
+
88
+ this._connection.onmessage = function (evt) {
89
+ console.log(evt.data);
90
+ var data = JSON.parse(evt.data.toString());
91
+ if (data.error !== undefined) {
92
+ _self.options.onError(data.error);
93
+ } else if (data.close !== undefined) {
94
+ connected = false;
95
+ } else {
96
+ _self.options.onData(data.data);
97
+ }
98
+ };
99
+
100
+ this._connection.onclose = function(evt) {
101
+ if (connected) {
102
+ window.clearInterval(ping);
103
+ _self.reconnect();
104
+ } else {
105
+ _self.options.onClose();
106
+ }
107
+ };
108
+
109
+ };
110
+
111
+ WSSHClient.prototype.connect = function(options) {
112
+
113
+ this.endpoint = this._generateEndpoint(options);
114
+ this.options = options;
115
+
116
+ this.startSocket();
117
+
118
+ };
119
+
120
+ WSSHClient.prototype.send = function(data) {
121
+ this._connection.send('d' + data);
122
+ };
123
+
124
+ WSSHClient.prototype.start = function(width, height) {
125
+ this._connection.send('s' + width + ',' + height);
126
+ };
127
+
128
+ WSSHClient.prototype.resize = function(width, height) {
129
+ this._connection.send('r' + width + ',' + height);
130
+ };
131
+
132
+ WSSHClient.prototype.reconnect = function() {
133
+ this.startSocket();
134
+ };
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
3
+
4
+ require 'pssh'
5
+
6
+ Pssh::CLI.run(ARGV)
7
+
@@ -0,0 +1,114 @@
1
+ require 'haml'
2
+ require 'io/console'
3
+ require 'json'
4
+ require 'optparse'
5
+ require 'pty'
6
+ require 'readline'
7
+ require 'tilt/haml'
8
+ require 'thin'
9
+ require 'rack/websocket'
10
+
11
+ require 'pssh/cli'
12
+ require 'pssh/client'
13
+ require 'pssh/console'
14
+ require 'pssh/socket'
15
+ require 'pssh/version'
16
+ require 'pssh/web_console'
17
+
18
+ module Pssh
19
+
20
+ DEFAULT_IO_MODE = 'rw'
21
+ DEFAULT_SOCKET_PREFIX = '/tmp/pssh'
22
+ DEFAULT_PORT = 8022
23
+
24
+ class << self
25
+
26
+ attr_writer :io_mode
27
+ attr_writer :socket_prefix
28
+ attr_writer :command
29
+ attr_writer :open_sessions
30
+ attr_writer :port
31
+ attr_writer :prompt
32
+ attr_accessor :client
33
+ attr_accessor :socket
34
+ attr_accessor :pty
35
+ attr_accessor :web
36
+
37
+ def base_path
38
+ File.dirname(__FILE__) + "/.."
39
+ end
40
+
41
+ def port
42
+ @port ||= DEFAULT_PORT
43
+ end
44
+
45
+ def open_sessions
46
+ @open_sessions ||= {}
47
+ end
48
+
49
+ # Public: This sets whether the connecting user can just view or can
50
+ # also write to the screen. Values are 'rw, 'r', and 'w'.
51
+ #
52
+ # Returns a String.
53
+ def io_mode
54
+ @io_mode ||= DEFAULT_IO_MODE
55
+ end
56
+
57
+ # Public: This is the prefix that will be used to set up the socket for tmux or screen.
58
+ #
59
+ # Returns a String.
60
+ def socket_prefix
61
+ @socket_prefix ||= DEFAULT_SOCKET_PREFIX
62
+ end
63
+
64
+ # Public: This is the default socket path that will be used if one is not
65
+ # provided in the command line arguments.
66
+ #
67
+ # Returns a String.
68
+ def default_socket_path
69
+ @socket ||= "#{socket_prefix}-#{SecureRandom.uuid}"
70
+ end
71
+
72
+ # Public: This is the tool which we are going to use for our multiplexing. If
73
+ # we're currently in a tmux or screen session, that is the first option,
74
+ # then it checks if tmux or screen is installed, and then it resorts to
75
+ # a plain old shell.
76
+ #
77
+ # Returns a Symbol.
78
+ def command
79
+ @command ||=
80
+ (ENV['TMUX'] && :tmux) ||
81
+ (ENV['STY'] && :screen) ||
82
+ (`which tmux` && :tmux) ||
83
+ (`which screen` && :screen) ||
84
+ :shell
85
+ end
86
+
87
+ # Public: Allow configuring details of Pssh by making use of a block.
88
+ #
89
+ # Returns True.
90
+ def configure
91
+ yield self
92
+ true
93
+ end
94
+
95
+ # Public: This is the prompt character that shows up at the beginning of
96
+ # Pssh's console.
97
+ #
98
+ # Returns a String.
99
+ def prompt
100
+ @prompt ||= "\u26a1 "
101
+ end
102
+
103
+ # Public: Generates a random id for a session and stores it to a list.
104
+ #
105
+ # Returns a String.
106
+ def create_session(username=nil)
107
+ id = SecureRandom.uuid
108
+ self.open_sessions[id] = username
109
+ id
110
+ end
111
+
112
+ end
113
+
114
+ end
@@ -0,0 +1,60 @@
1
+ module Pssh
2
+ class CLI
3
+
4
+ BANNER = <<-BANNER
5
+ Usage: pssh [options]
6
+
7
+ Description:
8
+
9
+ Remote pair programming made easy by allowing access via a web
10
+ browser. Supports HTTP Basic Auth to handle users, and can be
11
+ combined with tmux, screen, or just a plain shell.
12
+
13
+ BANNER
14
+
15
+ def self.parse_options(args)
16
+ options = {}
17
+
18
+ @opts = OptionParser.new do |opts|
19
+ opts.banner = BANNER.gsub(/^ {4}/, '')
20
+
21
+ opts.separator ''
22
+ opts.separator 'Options:'
23
+
24
+ opts.on('-p PORT', '--port PORT', Integer, 'Set the port that Pssh will run on') do |port|
25
+ options[:port] = port.to_i
26
+ end
27
+
28
+ opts.on('-c PATH', '--command COMMAND', [:tmux, :screen, :shell], 'Set the tool that will be used to initialize the web session (tmux, screen, or shell)') do |command|
29
+ options[:command] = command
30
+ end
31
+
32
+ opts.on('-s PATH', '--socket PATH', String, 'Set the socket that will be used for connecting (/path/to/socket)') do |socket|
33
+ options[:socket] = socket
34
+ end
35
+
36
+ opts.on( '-h', '--help', 'Display this help.' ) do
37
+ puts opts
38
+ exit
39
+ end
40
+
41
+ end
42
+
43
+ @opts.parse!(args)
44
+
45
+ options
46
+ end
47
+
48
+ def self.run(args)
49
+ opts = parse_options(args)
50
+ Pssh.configure do |pssh|
51
+ opts.each do |k,v|
52
+ pssh.send :"#{k}=", v
53
+ end
54
+ end
55
+ Pssh::Client.start
56
+ end
57
+
58
+ end
59
+ end
60
+
@@ -0,0 +1,30 @@
1
+ module Pssh
2
+ class Client
3
+
4
+ def initialize
5
+ @pty = Pssh.pty = Pssh::Socket.new
6
+ @web = Pssh.web = Pssh::WebConsole.new
7
+ @app = Rack::Builder.new do
8
+ map "/assets/" do
9
+ run Rack::File.new "#{Pssh.base_path}/assets/"
10
+ end
11
+ map "/socket" do
12
+ run Pssh.pty
13
+ end
14
+ map "/" do
15
+ run Pssh.web
16
+ end
17
+ end
18
+ Thread.new do
19
+ @console = Console.new(pty: @pty, web: @web)
20
+ end
21
+ Thin::Logging.silent = true
22
+ Rack::Handler::Thin.run @app, Port: Pssh.port
23
+ end
24
+
25
+ def self.start
26
+ Pssh.client = @client = Client.new
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,81 @@
1
+ module Pssh
2
+ class Console
3
+
4
+ COMMANDS = %w(list block unblock sessions kill-session exit).sort
5
+ BANNER = <<-BANNER
6
+ ------------------------------------------------------------------
7
+ help\t\tDisplays this help menu.
8
+ info\t\tDisplays current tmux settings and configuration.
9
+ list[ sessions]\tShow the currently open sessions.
10
+ kill-session[s]\tKill either all sessions or a specific by id.
11
+ --all\t\tKills all the open sessions.
12
+ 'session id'\tKills only the session specified by the id.
13
+ exit\t\tCloses all active sessions and exits.
14
+ ------------------------------------------------------------------
15
+ Tip: Use tab-completion for commands and session ids.
16
+ BANNER
17
+
18
+ def initialize(opts = {})
19
+
20
+ @pty = opts[:pty]
21
+ @web = opts[:web]
22
+
23
+ begin
24
+ puts "[ pssh terminal ]"
25
+ puts "Service started on port #{Pssh.port}."
26
+ puts "Type 'help' for more information."
27
+
28
+ Readline.completion_append_character = " "
29
+ Readline.completion_proc = completion_proc
30
+
31
+ while command = Readline.readline(Pssh.prompt, true)
32
+ command.strip!
33
+ command.gsub!(/\s+/, ' ')
34
+ case command
35
+ when 'help'
36
+ puts BANNER.gsub(/^ {6}/,'')
37
+ when 'exit'
38
+ @pty.kill_all_sessions
39
+ Kernel.exit!
40
+ when 'info'
41
+ puts 'Current Configuration:'
42
+ if @pty.path
43
+ puts "Socket: #{@pty.path}"
44
+ puts "(Attach to this socket with `#{@pty.attach_cmd}`)"
45
+ else
46
+ puts 'Connections are made to a vanilla shell.'
47
+ end
48
+ when 'list sessions', 'list'
49
+ @pty.sessions.each do |k,v|
50
+ puts v[:user_string]
51
+ end
52
+ when /^kill-sessions?\s?(.*)$/
53
+ if $1 == '--all'
54
+ puts 'disconnecting all clients'
55
+ @pty.kill_all_sessions
56
+ else
57
+ puts "disconnecting #{$1}"
58
+ if @pty.sessions.keys.include?($1)
59
+ @pty.sessions[$1][:socket].close!
60
+ else
61
+ @pty.sessions.each do |k, sess|
62
+ if sess[:username] == $1
63
+ sess[:socket].close!
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ rescue Exception => e
71
+ puts e.inspect
72
+ puts e.backtrace
73
+ retry
74
+ end
75
+ end
76
+
77
+ def completion_proc
78
+ @completion_proc ||= proc { |s| (COMMANDS + Pssh.open_sessions.keys + Pssh.open_sessions.values.uniq).grep( /^#{Regexp.escape(s)}/ ) }
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,187 @@
1
+ module Pssh
2
+ class Socket < Rack::WebSocket::Application
3
+
4
+ attr_accessor :sessions
5
+ attr_accessor :path
6
+ attr_accessor :attach_cmd
7
+
8
+ def initialize(opts={})
9
+ super
10
+
11
+ # set up empty variables
12
+ @sessions = {}
13
+ @killed_sessions = []
14
+ @existing_socket = false
15
+
16
+ self.send Pssh.command.to_sym
17
+ end
18
+
19
+ def tmux
20
+ if ENV['TMUX']
21
+ @path = ENV['TMUX'].split(',').first
22
+ @existing_socket = true
23
+ @command = "tmux -S #{@path} attach"
24
+ else
25
+ @path = Pssh.default_socket_path
26
+ @command = "tmux -S #{@path} new"
27
+ end
28
+ @attach_cmd = "tmux -S #{@path} attach"
29
+ end
30
+
31
+ def screen
32
+ if ENV['STY']
33
+ @path = ENV['STY']
34
+ @existing_socket = true
35
+ @command = "screen -S #{@path} -X multiuser on && screen -x #{@path}"
36
+ else
37
+ @path = Pssh.default_socket_path
38
+ @command = "screen -m -S #{@path}"
39
+ end
40
+ @attach_cmd = "screen -x #{@path}"
41
+ end
42
+
43
+ def shell
44
+ @path = nil
45
+ @command = 'sh'
46
+ end
47
+
48
+ def params(env)
49
+ Hash[env['QUERY_STRING'].split('&').map { |e| e.split '=' }]
50
+ end
51
+
52
+ def on_open(env)
53
+ uuid = @uuid = params(env)['uuid']
54
+ if @killed_sessions.include? uuid
55
+ # this session has been killed from the console and shouldn't be
56
+ # restarted
57
+ close_websocket and return
58
+ end
59
+ if @sessions[uuid]
60
+ @sessions[uuid][:active] = true
61
+ elsif Pssh.open_sessions.keys.include?(uuid)
62
+ user_string = Pssh.open_sessions[uuid] ? "#{Pssh.open_sessions[uuid]} (#{uuid})" : uuid
63
+ print "\n#{user_string} attached.\n#{Pssh.prompt}"
64
+ @sessions[uuid] = {
65
+ data: [],
66
+ username: Pssh.open_sessions[uuid],
67
+ user_string: user_string,
68
+ active: true,
69
+ started: false
70
+ }
71
+ Pssh.open_sessions.delete uuid
72
+ else
73
+ @sessions[uuid] = {socket: self}
74
+ self.close! and return
75
+ end
76
+ @sessions[uuid][:socket] = self
77
+ end
78
+
79
+ def on_close(env)
80
+ uuid = params(env)['uuid']
81
+ @sessions[uuid][:active] = false
82
+ Thread.new do
83
+ sleep 10
84
+ if @sessions[uuid][:active] == false
85
+ @sessions[uuid][:read].close
86
+ @sessions[uuid][:write].close
87
+ @sessions.delete uuid
88
+ print "\n#{user_string} detached.\n#{Pssh.prompt}"
89
+ end
90
+ end
91
+ end
92
+
93
+ def close_websocket
94
+ @sessions[@uuid][:socket].send_data({ close: true }.to_json) if @sessions[@uuid][:socket]
95
+ @sessions[@uuid][:read].close if @sessions[@uuid][:read]
96
+ @sessions[@uuid][:write].close if @sessions[@uuid][:write]
97
+ @sessions[@uuid][:thread].exit if @sessions[@uuid][:thread]
98
+ @sessions.delete @uuid
99
+ super
100
+ end
101
+
102
+ def kill_all_sessions
103
+ @sessions.each do |k,v|
104
+ v[:socket].close!
105
+ end
106
+ end
107
+
108
+ def close!
109
+ self.close_websocket
110
+ @killed_sessions << @uuid
111
+ end
112
+
113
+ # Internal: Sends a message to the tmux or screen display notifying of a
114
+ # new user that has connected.
115
+ #
116
+ # Returns nothing.
117
+ def send_display_message(uuid)
118
+ if @existing_socket
119
+ case Pssh.command.to_sym
120
+ when :tmux
121
+ `tmux -S #{@path} display-message "#{@sessions[uuid][:user_string]} has connected"`
122
+ when :screen
123
+ `screen -S #{@path} -X wall "#{@sessions[uuid][:user_string]} has connected"`
124
+ end
125
+ end
126
+ end
127
+
128
+ def clear_environment
129
+ ENV['TMUX'] = nil
130
+ ENV['STY'] = nil
131
+ end
132
+
133
+ def on_message(env, message)
134
+ uuid = params(env)['uuid']
135
+ return unless @sessions[uuid]
136
+ case message[0]
137
+ when 's'
138
+ unless @sessions[uuid][:started]
139
+ @sessions[uuid][:started] = true
140
+ @sessions[uuid][:thread] = Thread.new do
141
+ begin
142
+ if @sessions[uuid]
143
+ size = message[1..-1].split ','
144
+ send_display_message(uuid)
145
+ clear_environment
146
+ @sessions[uuid][:read], @sessions[uuid][:write], @sessions[uuid][:pid] = PTY.spawn(@command)
147
+ @sessions[uuid][:write].winsize = [size[1].to_i, size[0].to_i]
148
+
149
+ while(@sessions[uuid] && @sessions[uuid][:active]) do
150
+ IO.select([@sessions[uuid][:read]])
151
+ begin
152
+ while (data = @sessions[uuid][:read].readpartial(2048)) do
153
+ data.encode!('UTF-16', 'UTF-8', :invalid => :replace, :replace => '')
154
+ data.encode!('UTF-8', 'UTF-16')
155
+ if data.valid_encoding?
156
+ @sessions[uuid][:socket].send_data({ data: data }.to_json)
157
+ end
158
+ end
159
+ rescue Exception => e
160
+ if @sessions[uuid]
161
+ if e.is_a?(Errno::EAGAIN)
162
+ retry
163
+ else
164
+ @sessions[uuid][:active] = false
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
170
+
171
+ rescue Exception => e
172
+ puts e.inspect
173
+ puts e.backtrace
174
+ puts '---'
175
+ retry
176
+ end
177
+ end
178
+ end
179
+ when 'd'
180
+ @sessions[uuid][:write].write_nonblock message[1..-1] if Pssh.io_mode['w']
181
+ when 'r'
182
+ size = message[1..-1].split ','
183
+ @sessions[uuid][:write].winsize= [size[1].to_i, size[0].to_i]
184
+ end
185
+ end
186
+ end
187
+ end