em-wssh 0.5.0 → 0.6.0

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