pssh 0.2.2

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