em-wssh 0.5.0 → 0.6.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1db6dba2f0f3dff81eed7b595806938e0bd8f064
4
- data.tar.gz: 473cd9aaa813c0f21a5f178e03a90cf52acde42c
3
+ metadata.gz: 1ef81d191ad0809a51504e1f789f5e40f6c882be
4
+ data.tar.gz: da12d0d652fccf49292e6ee59917b607694d7af6
5
5
  SHA512:
6
- metadata.gz: c01b25a7cf340ddb7f9ad0aadc5feaee60afc1f6acbb1660122eb3724011d8e2062abf02db60144052b23be6113584cd848cd02338806108894058820d78542b
7
- data.tar.gz: 52d414591dfaf37c7c65c3a4610974f0ad491bb8c9d7b1d0bdfbfec3da832328b2189230638f0f72676476f5e4b2262c03908a2e4456806c19cfb359494fd20f
6
+ metadata.gz: 3ed9868c3ed61ce056cfdc261a391ce2f8b5607fb8ff8cc99efe94babf248e316698a988ef97f160e2cf7e1f0213503956e90d27f7f73702f3f181d90470e82f
7
+ data.tar.gz: d60cee8efb8911ce45bfb880e036dff03ba8e65dd334e3c6ccf007de95248e8cd161c473384a6b18aff20b4ce31cad1429581079373e5cd7156a5df1eeefa839
data/README.md CHANGED
@@ -11,7 +11,7 @@ Ruby version of ssh thru websocket proxying.
11
11
  Add this line to your application's Gemfile:
12
12
 
13
13
  ```ruby
14
- gem 'em-wssh' if Gem.win_platform?
14
+ gem 'em-wssh'
15
15
  ```
16
16
 
17
17
  And then execute:
@@ -50,7 +50,7 @@ Running client from terminal is not very useful. It should be called by ssh clie
50
50
  ssh -o ProxyCommand='wssh client wss://server.host.com/ssh/%h' sshd.local
51
51
  ```
52
52
 
53
- By default WSSH server has 60 seconds timeout. To prevent idle connection to drop,
53
+ By default nginx has 60 seconds timeout. To prevent idle connection to drop,
54
54
  one can use `ServerAliveInterval` parameter:
55
55
 
56
56
  ```sh
@@ -92,22 +92,26 @@ s.loop!
92
92
  ```ruby
93
93
  require 'em/wssh/client'
94
94
 
95
- s=EventMachine::Wssh::Client
96
- s.options[:uri]='wss://server.host.com/ssh/sshd.local'
97
- s.loop!
95
+ c=EventMachine::Wssh::Client
96
+ c.options[:uri]='wss://server.host.com/ssh/sshd.local'
97
+ c.loop!
98
98
  ```
99
99
 
100
100
  ```ruby
101
101
  require 'em/wssh/connect'
102
102
 
103
- s=EventMachine::Wssh::Connect
104
- s.options.merge! base: '.', all: true, uri: 'wss://server.host.com/ssh/sshd.local'
105
- s.loop!
103
+ p=EventMachine::Wssh::Connect
104
+ p.options.merge! base: '.', all: true, uri: 'wss://server.host.com/ssh/sshd.local'
105
+ p.loop!
106
106
  ```
107
107
 
108
+ Use `go!` method instead `loop!` to mimic cli behavour (command line parsing).
109
+
108
110
  Some options are not accesible to `wssh` command and can be used only programmaticaly.
109
111
 
110
- Eg, EventMachine::Wssh::Connect has option `onlisten` that allows listening to random port:
112
+ ### Ephemeral module
113
+
114
+ Eg, EventMachine::Wssh::Connect has option `onlisten` that allows listening to ephemeral port:
111
115
 
112
116
  ```ruby
113
117
  #!/usr/bin/env ruby
@@ -134,6 +138,35 @@ x=Net::SSH.start 'sshd.local', 'root',
134
138
  puts x.exec! 'hostname'
135
139
  ```
136
140
 
141
+ Unfortunately, EventMachine fails to run in thread inside Capistrano.
142
+ But Connect proxy still can run in separate process.
143
+ To do this, module Ephemeral was forged:
144
+
145
+ ```ruby
146
+ task :wssh do
147
+ require 'net/ssh/proxy/http'
148
+ require 'em/wssh/ephemeral'
149
+
150
+ port = EventMachine::Wssh::Ephemeral.allocate 'ws://localhost:4567/test'
151
+
152
+ proxy = Net::SSH::Proxy::HTTP.new 'localhost', port
153
+
154
+ roles(:all).each{|h| h.wssh_proxy proxy }
155
+ end
156
+
157
+ class SSHKit::Host
158
+ def wssh_proxy proxy
159
+ @ssh_options[:proxy]=proxy
160
+ end
161
+ end
162
+ ```
163
+ Use this task `cap stage wssh task(s)...` to tunnel all Capistrano ssh traffic through
164
+ WSSH server.
165
+
166
+ In addition, if WSSH URL is secure (wss: or https:) and Ephemeral module is running on Windows,
167
+ it automagically starts small Node.js process that wraps traffic into TLS.
168
+ This behavour can be disabled by setting Ephemeral.options[:tlswrap]=false
169
+
137
170
  ## Data flow
138
171
 
139
172
  Normal SSH session is very simple:
data/lib/em/wssh.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module EventMachine
2
2
  module Wssh
3
- VERSION = "0.5.0"
3
+ VERSION = "0.6.0"
4
4
  end
5
5
  end
@@ -12,7 +12,7 @@ module Client
12
12
  puts <<-EOT
13
13
  WSSH client
14
14
 
15
- Usage: #{Exe.biname} ws[s]://host[:port]/uri
15
+ #{Exe.usage} ws[s]://host[:port]/uri
16
16
  EOT
17
17
  exit 1
18
18
  end
@@ -22,7 +22,7 @@ module Connect
22
22
  puts <<-EOF
23
23
  Simple HTTP CONNECT proxy to WSSH daemon
24
24
 
25
- Usage: #{Exe.biname} [options...] ws[s]://host[:port]/uri
25
+ #{Exe.usage} [options...] ws[s]://host[:port]/uri
26
26
  EOF
27
27
  helptions
28
28
  end
@@ -66,6 +66,12 @@ Usage: #{Exe.biname} [options...] ws[s]://host[:port]/uri
66
66
  def onopen
67
67
  log "Connected to WSSHD"
68
68
  http.onopen
69
+ pinger
70
+ end
71
+
72
+ def pinger
73
+ return unless t=Connect.options[:ping]
74
+ @timer=EM::PeriodicTimer.new(t){@ws.ping if @ws}
69
75
  end
70
76
 
71
77
  def onmessage data
@@ -85,6 +91,7 @@ Usage: #{Exe.biname} [options...] ws[s]://host[:port]/uri
85
91
  def bye
86
92
  http.close_connection if http
87
93
  @ws.close if @ws
94
+ @timer.cancel if @timer
88
95
  instance_variables.each{|v| remove_instance_variable v if '@count'!=v.to_s}
89
96
  end
90
97
  end
@@ -0,0 +1,119 @@
1
+ require_relative 'service'
2
+
3
+ module EventMachine::Wssh
4
+ class Ephemeral
5
+ extend Service
6
+
7
+ @options={
8
+ tlswrap: true,
9
+ base: '.',
10
+ pid: 'tmp/pids/ephemeral.pid',
11
+ log: 'log/ephemeral.log',
12
+ tls: 'log/tls.log',
13
+ }
14
+
15
+ %i(log options mkdir).each do |meth|
16
+ define_method meth do |*args|
17
+ self.class.send meth, *args
18
+ end
19
+ end
20
+
21
+ def initialize
22
+ require 'socket'
23
+ @sock=Addrinfo.tcp('127.0.0.1', 0).listen 1
24
+ end
25
+
26
+ def myport
27
+ Socket.unpack_sockaddr_in(@sock.getsockname).first
28
+ end
29
+
30
+ def rport
31
+ sock=@sock.accept.first
32
+ at_exit{sock}
33
+ sock.gets.to_i
34
+ end
35
+
36
+ def tlswrap uri
37
+ return uri unless options[:tlswrap] and Gem.win_platform?
38
+ require 'uri'
39
+ z = URI uri
40
+ return uri unless %w(wss https).include? z.scheme
41
+ log "Running TLS Wrapper..."
42
+ spawn 'node', '.', myport.to_s, z.host,
43
+ chdir: __dir__,
44
+ %i(out err)=>File.open(mkdir(:tls), 'a')
45
+ z.scheme='ws'
46
+ z.host='localhost'
47
+ z.port=rport
48
+ z.to_s
49
+ end
50
+
51
+ def allocate uri
52
+ uri = tlswrap uri
53
+
54
+ log "Running WSSH proxy..."
55
+ spawn *%w(bundle exec wssh ephemeral), myport.to_s, uri,
56
+ %i(out err)=>File.open(mkdir(:log), 'a')
57
+
58
+ rport
59
+ end
60
+
61
+ def self.allocate uri
62
+ new.allocate uri
63
+ end
64
+
65
+ def self.help
66
+ require_relative 'exe'
67
+ puts <<-EOF
68
+ Run HTTP proxy on ephemeral port
69
+
70
+ #{Exe.usage} <port> <uri>
71
+
72
+ For internal use.
73
+ EOF
74
+ exit
75
+ end
76
+
77
+ def self.go!
78
+ help if 2!=ARGV.length
79
+ require_relative 'connect'
80
+
81
+ pid
82
+
83
+ Connect.options.merge!(
84
+ port: 0,
85
+ uri: ARGV[1],
86
+ ping: 50,
87
+ onlisten: Proc.new{|port| onlisten port},
88
+ )
89
+ Connect.loop!
90
+ end
91
+
92
+ module Parent
93
+ def initialize port
94
+ @port=port
95
+ @c=EM::Wssh::Connect
96
+ log "Listening to port", port
97
+ end
98
+
99
+ def log *args
100
+ @c.log *args
101
+ end
102
+
103
+ def post_init
104
+ log "Connected to parent"
105
+ send_data "#{@port}\n"
106
+ end
107
+
108
+ def unbind
109
+ log "Parent disconnected"
110
+ EM.stop_event_loop
111
+ end
112
+ end
113
+
114
+ def self.onlisten port
115
+ EM.connect 'localhost', ARGV[0], Parent, port
116
+ end
117
+
118
+ end
119
+ end
data/lib/em/wssh/exe.rb CHANGED
@@ -22,6 +22,7 @@ module Exe
22
22
  .map{|n|m.const_get n}
23
23
  .grep(Module)
24
24
  .select{|m| m.respond_to? :go!}
25
+ .select{|m| m.const_defined? :Title}
25
26
  .map{|m| [m.name.split(/\W+/).last.downcase, m]}
26
27
  .sort_by{|x| x[0]}
27
28
  ]
@@ -39,8 +40,8 @@ module Exe
39
40
  exit
40
41
  end
41
42
 
42
- def self.biname
43
- "wssh "+File.basename(caller_locations.first.path).sub(/[.][^.]*\Z/, '')
43
+ def self.usage
44
+ "Usage: wssh "+File.basename(caller_locations.first.path).sub(/[.][^.]*\Z/, '')
44
45
  end
45
46
  end
46
47
  end
data/lib/em/wssh/help.rb CHANGED
@@ -1,46 +1,46 @@
1
- require_relative 'exe'
2
-
3
- module EventMachine::Wssh
4
- module Help
5
-
6
- Title='Show this help'
7
-
8
- def self.go!
9
- if mod = Exe.command?(ARGV.shift)
10
- command mod
11
- else
12
- top
13
- end
14
- end
15
-
16
- def self.top
17
- require_relative 'all'
18
-
19
- puts <<-EOT
20
- WSSH suite v#{VERSION}
21
-
22
- Usage: wssh command [parameters...]
23
-
24
- Available commands:
25
-
26
- EOT
27
- Exe.commands.each{|cmd, mod| puts " wssh #{cmd}\t#{mod::Title rescue nil}"}
28
- end
29
-
30
- def self.command mod
31
- if mod.respond_to? :help
32
- mod.help
33
- else
34
- puts mod::Title
35
- end
36
- end
37
-
38
- def self.help
39
- puts <<-EOT
40
- Shows help for WSSH suite or individual commands
41
-
42
- Usage: #{Exe.biname} [command]
43
- EOT
44
- end
45
- end
46
- end
1
+ require_relative 'exe'
2
+
3
+ module EventMachine::Wssh
4
+ module Help
5
+
6
+ Title='Show this help'
7
+
8
+ def self.go!
9
+ if mod = Exe.command?(ARGV.shift)
10
+ command mod
11
+ else
12
+ top
13
+ end
14
+ end
15
+
16
+ def self.top
17
+ require_relative 'all'
18
+
19
+ puts <<-EOT
20
+ WSSH suite v#{VERSION}
21
+
22
+ Usage: wssh command [parameters...]
23
+
24
+ Available commands:
25
+
26
+ EOT
27
+ Exe.commands.each{|cmd, mod| puts " wssh #{cmd}\t#{mod::Title}"}
28
+ end
29
+
30
+ def self.command mod
31
+ if mod.respond_to? :help
32
+ mod.help
33
+ else
34
+ puts mod::Title
35
+ end
36
+ end
37
+
38
+ def self.help
39
+ puts <<-EOT
40
+ Shows help for WSSH suite or individual commands
41
+
42
+ #{Exe.usage} [command]
43
+ EOT
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,212 @@
1
+ //
2
+ // Simple stunnel for Websocket
3
+ //
4
+ net = require('net')
5
+ ssl = require('tls')
6
+
7
+ if(4!=process.argv.length) help()
8
+
9
+ var Host = process.argv[3]
10
+
11
+ net.createServer(Req)
12
+ .listen(0, On)
13
+
14
+ var
15
+ myport,
16
+ Count=0,
17
+ _log=log
18
+
19
+ function help()
20
+ {
21
+ path = require('path')
22
+ log('Usage:', path.basename(__filename), 'port host')
23
+ process.exit()
24
+ }
25
+
26
+ function On()
27
+ {
28
+ myport = this.address().port;
29
+ log("Listening", this.address())
30
+ net.connect(process.argv[2], 'localhost')
31
+ .on('connect', pConnect)
32
+ .on('end', pEnd)
33
+ .on('error', pError)
34
+ }
35
+
36
+ function pConnect()
37
+ {
38
+ log('Connected to parent')
39
+ this.write(''+myport+'\n')
40
+ }
41
+
42
+ function pEnd()
43
+ {
44
+ log('Parent closed')
45
+ process.exit()
46
+ }
47
+
48
+ function pError(err)
49
+ {
50
+ log('Parent error', err)
51
+ process.exit()
52
+ }
53
+
54
+ function Req(conn)
55
+ {
56
+ var
57
+ No=++Count, hs=[], prolog, body, tls
58
+
59
+ function log()
60
+ {
61
+ _log.apply(this, ['<'+No+'>'].concat([].slice.call(arguments)))
62
+ }
63
+
64
+ log("Client connected from", conn.remoteAddress+':'+conn.remotePort)
65
+
66
+ conn
67
+ .on('readable', cRead)
68
+ .on('end', cClose)
69
+ .on('error', cError)
70
+
71
+ function cRead()
72
+ {
73
+ var x
74
+ while(null!=(x=this.read()))
75
+ body ? queue(x) : headers(x)
76
+ }
77
+
78
+ function cClose()
79
+ {
80
+ log('Client disconnected')
81
+ bye()
82
+ }
83
+
84
+ function cError(e)
85
+ {
86
+ log('Client error', e)
87
+ bye()
88
+ }
89
+
90
+ function bye()
91
+ {
92
+ conn.end()
93
+ if(tls) tls.end()
94
+ }
95
+
96
+ function queue(data)
97
+ {
98
+ if(Array.isArray(body))
99
+ body.push(data)
100
+ else
101
+ send(data)
102
+ }
103
+
104
+ function headers(data)
105
+ {
106
+ prolog = prolog ? Buffer.concat([prolog, data]) : data
107
+ while(splitHdrs()){}
108
+ }
109
+
110
+ function splitHdrs()
111
+ {
112
+ for(var cr, L=prolog.length, i=0; i<L; i++)
113
+ if(10==prolog[i])
114
+ {
115
+ L = prolog
116
+ prolog = prolog.slice(i+1)
117
+ header(L.slice(0, i-(cr ? 1 : 0)).toString())
118
+ return true
119
+ }
120
+ else
121
+ cr = 13==prolog[i]
122
+ }
123
+
124
+ function header(line)
125
+ {
126
+ if(!line.length)
127
+ tlsConnect()
128
+ else
129
+ hs.push(line)
130
+ }
131
+
132
+ function tlsConnect()
133
+ {
134
+ body=[prolog]
135
+ prolog=new Buffer(0)
136
+ patchHeaders()
137
+ tls = ssl.connect({
138
+ servername: Host,
139
+ host: Host,
140
+ port: 443
141
+ })
142
+ tls
143
+ .on('secureConnect', tConnect)
144
+ .on('readable', tData)
145
+ .on('end', tEnd)
146
+ .on('error', tError)
147
+ }
148
+
149
+ function patchHeaders()
150
+ {
151
+ if(!hs.length) return
152
+ var verb = hs.shift()
153
+ hs=['Host', 'Origin']
154
+ .map(hostHeader)
155
+ .concat(hs.filter(filterHeader))
156
+ hs.unshift(verb)
157
+ }
158
+
159
+ function send(data)
160
+ {
161
+ if(data.length)
162
+ tls.write(data)
163
+ }
164
+
165
+ function tConnect()
166
+ {
167
+ log('TLS connected', 'Auth='+this.authorized,'CN='+this.getPeerCertificate().subject.CN)
168
+ send(hs.join('\r\n')+'\r\n\r\n')
169
+ body.forEach(send)
170
+ body = true
171
+ }
172
+
173
+ function tData()
174
+ {
175
+ var x
176
+ while(null!=(x=this.read()))
177
+ if(x.length) conn.write(x)
178
+ }
179
+
180
+ function tError(err)
181
+ {
182
+ log('TLS error', err)
183
+ bye()
184
+ }
185
+
186
+ function tEnd()
187
+ {
188
+ log('TLS closed')
189
+ bye()
190
+ }
191
+ }
192
+
193
+ function hostHeader(s)
194
+ {
195
+ return s+': '+Host
196
+ }
197
+
198
+ function filterHeader(s)
199
+ {
200
+ return !/^(?:host|origin):/i.test(s)
201
+ }
202
+
203
+ function log()
204
+ {
205
+ var
206
+ d = new Date()
207
+ console.info.apply(
208
+ console,
209
+ ['['+d.toISOString().replace('T', ' ').replace(/Z$/, '')
210
+ +d.toTimeString().split(/\s+/)[1].replace(/^\w+/, ' ')+']']
211
+ .concat([].slice.call(arguments)))
212
+ }
@@ -22,7 +22,7 @@ module Server
22
22
  puts <<-EOF
23
23
  Proxy ssh connection through websocket
24
24
 
25
- Usage: #{Exe.biname} [options...]
25
+ #{Exe.usage} [options...]
26
26
  EOF
27
27
  helptions
28
28
  end
@@ -94,6 +94,12 @@ EOF
94
94
  end
95
95
  log "Connecting to", host
96
96
  EM.connect host, 22, Ssh, self
97
+ pinger
98
+ end
99
+
100
+ def pinger
101
+ return unless t=Server.options[:ping]
102
+ @timer=EM::PeriodicTimer.new(t){ws.ping if ws}
97
103
  end
98
104
 
99
105
  def ondata msg
@@ -148,6 +154,7 @@ EOF
148
154
  def bye
149
155
  ssh.close_connection if ssh
150
156
  ws.close if ws
157
+ @timer.cancel if @timer
151
158
  instance_variables.each{|v| remove_instance_variable v if '@count'!=v.to_s}
152
159
  end
153
160
  end
@@ -17,6 +17,7 @@ module Service
17
17
  -d --daemon Run daemonized
18
18
  -h --help Show this help
19
19
  -l --listen=port Listen to port
20
+ -p --ping[=sec] Periodic ping
20
21
  -v --version Show version
21
22
  EOF
22
23
  exit 1
@@ -30,6 +31,7 @@ EOF
30
31
  ['-d', '--daemon', GetoptLong::NO_ARGUMENT],
31
32
  ['-a', '--all', GetoptLong::NO_ARGUMENT],
32
33
  ['-v', '--version', GetoptLong::NO_ARGUMENT],
34
+ ['-p', '--ping', GetoptLong::OPTIONAL_ARGUMENT],
33
35
  )
34
36
  begin
35
37
  opts.each do |opt, arg|
@@ -42,6 +44,9 @@ EOF
42
44
  options[:base]=File.expand_path arg
43
45
  when '-a'
44
46
  options[:host]='0.0.0.0'
47
+ when '-p'
48
+ arg=arg.to_i
49
+ options[:ping]= arg<1 ? 50 : arg
45
50
  when '-v'
46
51
  puts VERSION
47
52
  exit 1
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: em-wssh
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stas Ukolov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-03-06 00:00:00.000000000 Z
11
+ date: 2015-03-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: em-websocket
@@ -87,8 +87,10 @@ files:
87
87
  - lib/em/wssh/all.rb
88
88
  - lib/em/wssh/client.rb
89
89
  - lib/em/wssh/connect.rb
90
+ - lib/em/wssh/ephemeral.rb
90
91
  - lib/em/wssh/exe.rb
91
92
  - lib/em/wssh/help.rb
93
+ - lib/em/wssh/index.js
92
94
  - lib/em/wssh/server.rb
93
95
  - lib/em/wssh/service.rb
94
96
  - lib/em/wssh/version.rb