em-wssh 0.3.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 +7 -0
- data/.gitignore +18 -0
- data/FarMenu.ini +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +97 -0
- data/Rakefile +1 -0
- data/bin/wssh +2 -0
- data/em-wssh.gemspec +26 -0
- data/hosts.yml +20 -0
- data/lib/em/wssh.rb +5 -0
- data/lib/em/wssh/all.rb +1 -0
- data/lib/em/wssh/client.rb +95 -0
- data/lib/em/wssh/connect.rb +163 -0
- data/lib/em/wssh/exe.rb +25 -0
- data/lib/em/wssh/help.rb +20 -0
- data/lib/em/wssh/server.rb +159 -0
- data/lib/em/wssh/service.rb +116 -0
- data/nginx/ssh +8 -0
- metadata +119 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5594f5ba9ee7af40c50e16a6495afe8567c1ae8e
|
4
|
+
data.tar.gz: 5d311ce91fcd6c547070ce9dfd805c44dbc8b204
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bf4bc293ae6fda9f53b3183566fb9a644146e9e70022cbd11273ea76274a8fbda896a367ff0219b3a6ac8e338ef49e23474b1703965ee427897776c4aa2f3fd1
|
7
|
+
data.tar.gz: cfd455480307b436d080bd803ac188dd90c615b7b270b04cfdfbe83bc07f6b8f8d501aeff8c45503fc7aa946bfe17a89d441f8ce19eb6227f7c31b6346c87acb
|
data/.gitignore
ADDED
data/FarMenu.ini
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
I: Install dependencies
|
2
|
+
bundle install
|
3
|
+
--:
|
4
|
+
B: Build gem
|
5
|
+
bundle exec rake build
|
6
|
+
L: Install gem locally
|
7
|
+
bundle exec rake install
|
8
|
+
U: Uninstall gem
|
9
|
+
gem uninstall -ax em-wssh
|
10
|
+
--:
|
11
|
+
S: Start server
|
12
|
+
bundle exec ruby bin/wssh server -new_console:cb
|
13
|
+
C: Start CONNECT proxy
|
14
|
+
bundle exec ruby bin/wssh connect ws://localhost:4567/test -new_console:cb
|
15
|
+
2: Start both servers
|
16
|
+
bundle exec ruby bin/wssh server -new_console:cb
|
17
|
+
bundle exec ruby bin/wssh connect ws://localhost:4567/test -new_console:cb
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Stas Ukolov
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
# em-wssh
|
2
|
+
|
3
|
+
[](http://badge.fury.io/rb/em-wssh)
|
4
|
+
|
5
|
+
Ruby version of ssh thru websocket proxying.
|
6
|
+
|
7
|
+
[Original version](https://github.com/ukoloff/wssh) uses Node.js
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'em-wssh' if Gem.win_platform?
|
15
|
+
```
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
```sh
|
20
|
+
$ bundle
|
21
|
+
```
|
22
|
+
|
23
|
+
Or install it yourself as:
|
24
|
+
|
25
|
+
```sh
|
26
|
+
$ gem install em-wssh
|
27
|
+
```
|
28
|
+
|
29
|
+
## Usage
|
30
|
+
|
31
|
+
...
|
32
|
+
|
33
|
+
## Data flow
|
34
|
+
|
35
|
+
Normal SSH session is very simple:
|
36
|
+
|
37
|
+
* SSH Client
|
38
|
+
* TCP Connection
|
39
|
+
* SSH Server, listening on TCP port 22
|
40
|
+
|
41
|
+
WSSH session is:
|
42
|
+
|
43
|
+
* SSH Client with -o ProxyCommand='ruby client.rb WSSH-URI'
|
44
|
+
* client.rb listening to its stdin
|
45
|
+
* Websocket (HTTP/HTTPS) connection to nginx
|
46
|
+
* nginx [configured](nginx/ssh) to redirect connection to WSSH server
|
47
|
+
* Another Websocket connection from nginx to WSSH server
|
48
|
+
* WSSH server, listening to dedicated TCP port (4567 by default)
|
49
|
+
* Normal TCP connection
|
50
|
+
* Normal SSH Server, listening on TCP port 22
|
51
|
+
|
52
|
+
And nginx stage can be omited in development/testing scenarios.
|
53
|
+
|
54
|
+
In some scenarios this path can be even longer:
|
55
|
+
|
56
|
+
* SSH Client, capable to connect via HTTP proxy (eg PuTTY/PLink)
|
57
|
+
* TCP connection to local proxy
|
58
|
+
* connect.rb listening to dedicated port (3122 by default)
|
59
|
+
* Websocket (HTTP/HTTPS) connection to nginx
|
60
|
+
* nginx [configured](nginx/ssh) to redirect connection to WSSH server
|
61
|
+
* Another Websocket connection from nginx to WSSH server
|
62
|
+
* WSSH server, listening to dedicated TCP port (4567 by default)
|
63
|
+
* Normal TCP connection
|
64
|
+
* Normal SSH Server, listening on TCP port 22
|
65
|
+
|
66
|
+
## Windows bugs
|
67
|
+
|
68
|
+
Windows installation of EventMachine has a few bugs:
|
69
|
+
|
70
|
+
1. Using STDIN [blocks](https://groups.google.com/forum/#!topic/eventmachine/5rDIOA2uOoA) all other connections
|
71
|
+
2. By default SSL/TLS is not available
|
72
|
+
3. No root certificates available ([Fixed](https://github.com/ukoloff/openssl-win-root))
|
73
|
+
|
74
|
+
So, this package is in fact almost unusable on MS Windows.
|
75
|
+
|
76
|
+
The only exception: if you connect to Non-TLS WSSH server
|
77
|
+
(ws: or http:, not wss: or https:), you **can** start connect.rb
|
78
|
+
and then use SSH client, capable to connect via HTTP proxy.
|
79
|
+
|
80
|
+
To connect to TLS WSSH server, you should use Node.js version.
|
81
|
+
|
82
|
+
## See also
|
83
|
+
|
84
|
+
* [Node.js version](https://github.com/ukoloff/wssh)
|
85
|
+
* [Python version](https://github.com/progrium/wssh)
|
86
|
+
|
87
|
+
## Credits
|
88
|
+
|
89
|
+
* [nginx](http://nginx.org/)
|
90
|
+
* [Ruby](https://www.ruby-lang.org/)
|
91
|
+
* [EventMachine](https://github.com/eventmachine/eventmachine)
|
92
|
+
* [EM-WebSocket](https://github.com/igrigorik/em-websocket)
|
93
|
+
* [faye-websocket](https://github.com/faye/faye-websocket-ruby)
|
94
|
+
* [Node.js](http://nodejs.org/)
|
95
|
+
* [OpenSSH](http://www.openssh.com/)
|
96
|
+
* [Net::SSH](https://github.com/net-ssh/net-ssh)
|
97
|
+
* [PuTTY](http://www.chiark.greenend.org.uk/~sgtatham/putty/)
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/wssh
ADDED
data/em-wssh.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'em/wssh'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "em-wssh"
|
8
|
+
spec.version = EventMachine::Wssh::VERSION
|
9
|
+
spec.authors = ["Stas Ukolov"]
|
10
|
+
spec.email = ["ukoloff@gmail.com"]
|
11
|
+
spec.description = 'Proxy SSH connection through Websocket (nginx)'
|
12
|
+
spec.summary = ''
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "em-websocket" # server side
|
22
|
+
spec.add_dependency "faye-websocket" # client side
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
25
|
+
spec.add_development_dependency "rake"
|
26
|
+
end
|
data/hosts.yml
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# Catch all regexp
|
2
|
+
//: localhost
|
3
|
+
|
4
|
+
# Simple rule
|
5
|
+
self: localhost
|
6
|
+
|
7
|
+
# Disabled
|
8
|
+
none: false
|
9
|
+
|
10
|
+
# Allowed
|
11
|
+
github.com: true
|
12
|
+
gitlab.com: true
|
13
|
+
bitbucket.org: true
|
14
|
+
|
15
|
+
# Sample regexp
|
16
|
+
/lab$/i: gitlab.com
|
17
|
+
|
18
|
+
# Allow intranet IPs
|
19
|
+
/10(?:\.\d+){3}/: true
|
20
|
+
/192\.168(?:\.\d+){2}/: true
|
data/lib/em/wssh.rb
ADDED
data/lib/em/wssh/all.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Dir.glob(File.expand_path '../*.rb', __FILE__){|f|require f}
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require_relative '../wssh'
|
2
|
+
|
3
|
+
module EventMachine::Wssh
|
4
|
+
module Client
|
5
|
+
Need=%w(faye/websocket)
|
6
|
+
|
7
|
+
def self.help
|
8
|
+
puts <<-EOT
|
9
|
+
WSSH client
|
10
|
+
|
11
|
+
Usage: ruby #{File.basename __FILE__} ws[s]://host[:port]/uri
|
12
|
+
EOT
|
13
|
+
exit 1
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.getopt
|
17
|
+
help if ARGV.length!=1
|
18
|
+
@uri=ARGV[0]
|
19
|
+
end
|
20
|
+
|
21
|
+
class Ws
|
22
|
+
def initialize uri
|
23
|
+
@buf=[]
|
24
|
+
|
25
|
+
@ws=Faye::WebSocket::Client.new uri
|
26
|
+
|
27
|
+
@ws.on :open do |event| onopen end
|
28
|
+
@ws.on :message do |event| onmessage event.data end
|
29
|
+
@ws.on :close do |event| onclose end
|
30
|
+
@ws.on :error do |error| onerror error end
|
31
|
+
end
|
32
|
+
|
33
|
+
def queue data
|
34
|
+
if @buf
|
35
|
+
@buf << data
|
36
|
+
else
|
37
|
+
@ws.send data
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def onopen
|
42
|
+
@buf.each{|data| @ws.send data}
|
43
|
+
@buf=nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def onmessage data
|
47
|
+
print data.pack 'C*'
|
48
|
+
end
|
49
|
+
|
50
|
+
def onclose
|
51
|
+
bye
|
52
|
+
end
|
53
|
+
|
54
|
+
def onerror error
|
55
|
+
bye
|
56
|
+
end
|
57
|
+
|
58
|
+
def bye
|
59
|
+
@ws.close if @ws
|
60
|
+
@ws=nil
|
61
|
+
@buf=nil
|
62
|
+
EM.stop_event_loop
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
module Stdio
|
67
|
+
def initialize ws
|
68
|
+
@ws = ws
|
69
|
+
end
|
70
|
+
|
71
|
+
def receive_data data
|
72
|
+
@ws.queue data.unpack 'C*'
|
73
|
+
end
|
74
|
+
|
75
|
+
def unbind
|
76
|
+
@ws.bye
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.listen!
|
81
|
+
EM.attach $stdin, Stdio, Ws.new(@uri)
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.loop!
|
85
|
+
self::Need.each{|f| require f}
|
86
|
+
EM.run{ listen! }
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.go!
|
90
|
+
getopt
|
91
|
+
STDOUT.sync=true
|
92
|
+
loop!
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
require_relative 'service'
|
2
|
+
|
3
|
+
module EventMachine::Wssh
|
4
|
+
module Connect
|
5
|
+
extend Service
|
6
|
+
|
7
|
+
Need=%w(faye/websocket)
|
8
|
+
|
9
|
+
@options={
|
10
|
+
host: 'localhost',
|
11
|
+
port: 3122,
|
12
|
+
daemon: false,
|
13
|
+
args: :uri,
|
14
|
+
log: 'log/connect.log',
|
15
|
+
pid: 'tmp/pids/connect.pid',
|
16
|
+
}
|
17
|
+
|
18
|
+
def self.help
|
19
|
+
puts <<-EOF
|
20
|
+
Simple HTTP CONNECT proxy to WSSH daemon
|
21
|
+
|
22
|
+
Usage: ruby #{File.basename __FILE__} [options...] ws[s]://host[:port]/uri
|
23
|
+
EOF
|
24
|
+
helptions
|
25
|
+
end
|
26
|
+
|
27
|
+
class Dst
|
28
|
+
attr_accessor :http
|
29
|
+
|
30
|
+
Connect=Module.nesting[1]
|
31
|
+
|
32
|
+
def self.count
|
33
|
+
@n||=0
|
34
|
+
@n+=1
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(http)
|
38
|
+
self.http=http
|
39
|
+
@count=self.class.count
|
40
|
+
end
|
41
|
+
|
42
|
+
def log *msg
|
43
|
+
Connect.log "<#{@count}>", *msg
|
44
|
+
end
|
45
|
+
|
46
|
+
def send data
|
47
|
+
@ws.send data.unpack 'C*' if data.length>0
|
48
|
+
end
|
49
|
+
|
50
|
+
def connect! host
|
51
|
+
log "Redirect to", uri="#{Connect.options[:uri]}/#{host}"
|
52
|
+
|
53
|
+
http.onbody
|
54
|
+
|
55
|
+
@ws = Faye::WebSocket::Client.new uri
|
56
|
+
|
57
|
+
@ws.on :open do |event| onopen end
|
58
|
+
@ws.on :message do |event| onmessage event.data end
|
59
|
+
@ws.on :close do |event| onclose end
|
60
|
+
@ws.on :error do |error| onerror error end
|
61
|
+
end
|
62
|
+
|
63
|
+
def onopen
|
64
|
+
log "Connected to WSSHD"
|
65
|
+
http.onopen
|
66
|
+
end
|
67
|
+
|
68
|
+
def onmessage data
|
69
|
+
http.send_data Array===data ? data.pack('C*') : data
|
70
|
+
end
|
71
|
+
|
72
|
+
def onerror error
|
73
|
+
log "Websocket error", error
|
74
|
+
bye
|
75
|
+
end
|
76
|
+
|
77
|
+
def onclose
|
78
|
+
log "Websocket closed"
|
79
|
+
bye
|
80
|
+
end
|
81
|
+
|
82
|
+
def bye
|
83
|
+
http.close_connection if http
|
84
|
+
@ws.close if @ws
|
85
|
+
instance_variables.each{|v| remove_instance_variable v if '@count'!=v.to_s}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
module Http
|
90
|
+
attr_accessor :dst
|
91
|
+
|
92
|
+
def log *msg
|
93
|
+
dst.log *msg
|
94
|
+
end
|
95
|
+
|
96
|
+
def wssh data
|
97
|
+
dst.send data
|
98
|
+
end
|
99
|
+
|
100
|
+
def post_init
|
101
|
+
self.dst = Dst.new self
|
102
|
+
|
103
|
+
port, ip = Socket.unpack_sockaddr_in get_peername
|
104
|
+
log "Client connected from", "#{ip}:#{port}"
|
105
|
+
end
|
106
|
+
|
107
|
+
def receive_data data
|
108
|
+
if @body
|
109
|
+
if Array===@body
|
110
|
+
@body << data
|
111
|
+
else
|
112
|
+
wssh data
|
113
|
+
end
|
114
|
+
return
|
115
|
+
end
|
116
|
+
|
117
|
+
if @hdrs
|
118
|
+
@hdrs << data
|
119
|
+
else
|
120
|
+
@hdrs = data
|
121
|
+
end
|
122
|
+
while m=/\r?\n/.match(@hdrs)
|
123
|
+
@hdrs=m.post_match
|
124
|
+
receive_line m.pre_match
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def receive_line line
|
129
|
+
@nhdr||=0
|
130
|
+
if 1==(@nhdr+=1)
|
131
|
+
m=/^connect\s+([-.\w]+):22(?:$|\s)/i.match line
|
132
|
+
return @wssh = m[1] if m
|
133
|
+
@hdrs=''
|
134
|
+
log "Bad request"
|
135
|
+
send_data "HTTP/1.0 500 Bad request\r\n\r\n"
|
136
|
+
dst.bye
|
137
|
+
else
|
138
|
+
dst.connect! @wssh if 0==line.length
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def onbody
|
143
|
+
@body=[@hdrs]
|
144
|
+
@hdrs=''
|
145
|
+
send_data "HTTP/1.0 200 Ok\r\n\r\n"
|
146
|
+
end
|
147
|
+
|
148
|
+
def onopen
|
149
|
+
@body.each{|data| wssh data}
|
150
|
+
@body=true
|
151
|
+
end
|
152
|
+
|
153
|
+
def unbind
|
154
|
+
log "Client disconnected"
|
155
|
+
dst.bye
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def self.listen!
|
160
|
+
EM.start_server options[:host], options[:port], Http
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
data/lib/em/wssh/exe.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require_relative '../wssh'
|
2
|
+
|
3
|
+
module EventMachine::Wssh
|
4
|
+
module Exe
|
5
|
+
def self.do!
|
6
|
+
cmd = ARGV.shift
|
7
|
+
help unless /\A\w+\Z/.match cmd
|
8
|
+
cmd=cmd.downcase
|
9
|
+
begin
|
10
|
+
require_relative cmd
|
11
|
+
rescue LoadError
|
12
|
+
help
|
13
|
+
end
|
14
|
+
m=Module.nesting[1].const_get cmd.sub(/^./){|s|s.upcase}
|
15
|
+
help unless Module===m and m.respond_to? :go!
|
16
|
+
m.go!
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.help
|
20
|
+
require_relative 'help'
|
21
|
+
Help.go!
|
22
|
+
exit
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/em/wssh/help.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require_relative 'all'
|
2
|
+
|
3
|
+
module EventMachine::Wssh
|
4
|
+
module Help
|
5
|
+
def self.go!
|
6
|
+
m=Module.nesting[1]
|
7
|
+
list=m.constants
|
8
|
+
.map{|n|m.const_get n}
|
9
|
+
.grep(Module)
|
10
|
+
.select{|m| m.respond_to? :go!}
|
11
|
+
.map{|m| m.name.split(/\W+/).last.downcase}
|
12
|
+
.sort
|
13
|
+
puts <<-EOT
|
14
|
+
WSSH v#{VERSION}
|
15
|
+
|
16
|
+
Available commands: #{list*', '}
|
17
|
+
EOT
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
require_relative 'service'
|
2
|
+
|
3
|
+
module EventMachine::Wssh
|
4
|
+
module Server
|
5
|
+
extend Service
|
6
|
+
|
7
|
+
Need=%w(yaml em-websocket)
|
8
|
+
|
9
|
+
@options={
|
10
|
+
host: 'localhost',
|
11
|
+
port: 4567,
|
12
|
+
daemon: false,
|
13
|
+
hosts: 'hosts.yml',
|
14
|
+
log: 'log/wsshd.log',
|
15
|
+
pid: 'tmp/pids/wsshd.pid',
|
16
|
+
}
|
17
|
+
|
18
|
+
def self.help
|
19
|
+
puts <<-EOF
|
20
|
+
Proxy ssh connection through websocket
|
21
|
+
|
22
|
+
Usage: ruby #{File.basename __FILE__} [options...]
|
23
|
+
EOF
|
24
|
+
helptions
|
25
|
+
end
|
26
|
+
|
27
|
+
module Ssh
|
28
|
+
attr_accessor :req
|
29
|
+
|
30
|
+
def initialize req
|
31
|
+
self.req=req
|
32
|
+
end
|
33
|
+
|
34
|
+
def log *msg
|
35
|
+
req.log *msg
|
36
|
+
end
|
37
|
+
|
38
|
+
def post_init
|
39
|
+
log "Connected to SSH server"
|
40
|
+
req.ssh=self
|
41
|
+
req.buf.each{|data| send_data data}
|
42
|
+
req.buf=nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def receive_data data
|
46
|
+
req.ws.send_binary data
|
47
|
+
end
|
48
|
+
|
49
|
+
def unbind
|
50
|
+
log 'SSH server closed connection'
|
51
|
+
req.bye
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class Req
|
56
|
+
attr_accessor :ws, :buf, :ssh
|
57
|
+
|
58
|
+
Server=Module.nesting[1]
|
59
|
+
|
60
|
+
def self.count
|
61
|
+
@n||=0
|
62
|
+
@n+=1
|
63
|
+
end
|
64
|
+
|
65
|
+
def initialize ws
|
66
|
+
self.ws=ws
|
67
|
+
self.buf=[]
|
68
|
+
|
69
|
+
@count=self.class.count
|
70
|
+
|
71
|
+
port, ip=Socket.unpack_sockaddr_in ws.get_peername
|
72
|
+
log "Connect from", ip
|
73
|
+
|
74
|
+
ws.onopen{|handshake| onopen handshake}
|
75
|
+
ws.onbinary{|msg| ondata msg}
|
76
|
+
ws.onclose{|code, body| onclose}
|
77
|
+
ws.onerror{|err| onerror err}
|
78
|
+
end
|
79
|
+
|
80
|
+
def log *msg
|
81
|
+
Server.log "<#{@count}>", *msg
|
82
|
+
end
|
83
|
+
|
84
|
+
def onopen handshake
|
85
|
+
xf=handshake.headers_downcased['x-forwarded-for']
|
86
|
+
log "Forwarded for", xf if xf
|
87
|
+
log "Request", handshake.path
|
88
|
+
unless host = resolve(handshake.path) rescue nil
|
89
|
+
log "Invalid host"
|
90
|
+
bye
|
91
|
+
return
|
92
|
+
end
|
93
|
+
log "Connecting to", host
|
94
|
+
EM.connect host, 22, Ssh, self
|
95
|
+
end
|
96
|
+
|
97
|
+
def ondata msg
|
98
|
+
if buf
|
99
|
+
buf << msg
|
100
|
+
else
|
101
|
+
ssh.send_data msg
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def onclose
|
106
|
+
log 'Client closed connection'
|
107
|
+
bye
|
108
|
+
end
|
109
|
+
|
110
|
+
def onerror err
|
111
|
+
log "Websocket error", err
|
112
|
+
bye
|
113
|
+
end
|
114
|
+
|
115
|
+
def resolve(path)
|
116
|
+
path = path.to_s
|
117
|
+
.split(/[^-.\w]+/)
|
118
|
+
.select{|s|s.length>0}
|
119
|
+
.select{|s|!s.match /^[-_.]|[-_.]$/}
|
120
|
+
.last
|
121
|
+
yml = YAML.load_file Server.path(:hosts)
|
122
|
+
|
123
|
+
if yml.key? path
|
124
|
+
host = yml[path]
|
125
|
+
raise 'X' unless host
|
126
|
+
host = path if true===host
|
127
|
+
host = host.to_s.strip
|
128
|
+
raise 'X' if 0==host.length
|
129
|
+
return host
|
130
|
+
end
|
131
|
+
|
132
|
+
host=nil
|
133
|
+
|
134
|
+
yml.each do |k, v|
|
135
|
+
next unless m=/^\/(.*)\/(i?)$/.match(k)
|
136
|
+
next unless Regexp.new(m[1], m[2]).match path
|
137
|
+
raise 'X' unless v
|
138
|
+
host = true===v ? path : v
|
139
|
+
host = host.to_s.strip
|
140
|
+
raise 'X' if 0==host.length
|
141
|
+
end
|
142
|
+
raise 'X' unless host
|
143
|
+
host
|
144
|
+
end
|
145
|
+
|
146
|
+
def bye
|
147
|
+
ssh.close_connection if ssh
|
148
|
+
ws.close if ws
|
149
|
+
instance_variables.each{|v| remove_instance_variable v if '@count'!=v.to_s}
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.listen!
|
154
|
+
EM::WebSocket.run host: options[:host], port: options[:port] do |ws|
|
155
|
+
Req.new ws
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require_relative '../wssh'
|
2
|
+
|
3
|
+
module EventMachine::Wssh
|
4
|
+
module Service
|
5
|
+
attr_reader :options
|
6
|
+
|
7
|
+
def log *msg
|
8
|
+
msg.unshift "[#{Time.now}]"
|
9
|
+
puts msg*' '
|
10
|
+
end
|
11
|
+
|
12
|
+
def helptions
|
13
|
+
puts <<-EOF
|
14
|
+
|
15
|
+
-a --all Listen to all interfaces
|
16
|
+
-b --base=dir Set home directory
|
17
|
+
-d --daemon Run daemonized
|
18
|
+
-h --help Show this help
|
19
|
+
-l --listen=port Listen to port
|
20
|
+
-v --version Show version
|
21
|
+
EOF
|
22
|
+
exit 1
|
23
|
+
end
|
24
|
+
|
25
|
+
def getopt
|
26
|
+
require 'getoptlong'
|
27
|
+
opts = GetoptLong.new(
|
28
|
+
['-l', '--listen', GetoptLong::REQUIRED_ARGUMENT],
|
29
|
+
['-b', '--base', GetoptLong::REQUIRED_ARGUMENT],
|
30
|
+
['-d', '--daemon', GetoptLong::NO_ARGUMENT],
|
31
|
+
['-a', '--all', GetoptLong::NO_ARGUMENT],
|
32
|
+
['-v', '--version', GetoptLong::NO_ARGUMENT],
|
33
|
+
)
|
34
|
+
begin
|
35
|
+
opts.each do |opt, arg|
|
36
|
+
case opt
|
37
|
+
when '-d'
|
38
|
+
options[:daemon]=true
|
39
|
+
when '-l'
|
40
|
+
options[:port]=arg
|
41
|
+
when '-b'
|
42
|
+
options[:base]=File.expand_path arg
|
43
|
+
when '-a'
|
44
|
+
options[:host]='0.0.0.0'
|
45
|
+
when '-v'
|
46
|
+
puts VERSION
|
47
|
+
exit 1
|
48
|
+
end
|
49
|
+
end
|
50
|
+
rescue
|
51
|
+
help
|
52
|
+
end
|
53
|
+
args=options[:args]
|
54
|
+
args=args.nil? ? [] : [args] unless Array===args
|
55
|
+
help if args.length!=ARGV.length
|
56
|
+
args.each{|arg| options[arg]=ARGV.shift}
|
57
|
+
end
|
58
|
+
|
59
|
+
def homebase
|
60
|
+
x = File.expand_path '..', __FILE__
|
61
|
+
x = File.dirname x until File.exists? File.join x, 'Gemfile'
|
62
|
+
x
|
63
|
+
end
|
64
|
+
|
65
|
+
def path(sym)
|
66
|
+
File.join options[:base]||=homebase, options[sym]
|
67
|
+
end
|
68
|
+
|
69
|
+
def mkdir(sym)
|
70
|
+
require 'fileutils'
|
71
|
+
FileUtils.mkdir_p File.dirname file=(path sym)
|
72
|
+
file
|
73
|
+
end
|
74
|
+
|
75
|
+
def daemonize!
|
76
|
+
throw 'Cannot daemonize on Windows!' if Gem.win_platform?
|
77
|
+
|
78
|
+
log "Going on in background..."
|
79
|
+
|
80
|
+
f = File.open mkdir(:log), 'a'
|
81
|
+
f.sync=true
|
82
|
+
|
83
|
+
STDIN.reopen '/dev/null'
|
84
|
+
STDOUT.reopen f
|
85
|
+
STDERR.reopen f
|
86
|
+
|
87
|
+
Process.daemon true, true
|
88
|
+
end
|
89
|
+
|
90
|
+
def daemonize?
|
91
|
+
daemonize! if options[:daemon]
|
92
|
+
end
|
93
|
+
|
94
|
+
def pid
|
95
|
+
File.write p=mkdir(:pid), $$
|
96
|
+
at_exit do
|
97
|
+
log "Exiting..."
|
98
|
+
File.unlink p
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def loop!
|
103
|
+
self::Need.each{|f| require f}
|
104
|
+
EM.run{ listen! }
|
105
|
+
end
|
106
|
+
|
107
|
+
def go!
|
108
|
+
getopt
|
109
|
+
daemonize?
|
110
|
+
log "Listening on #{options[:host]}:#{options[:port]}"
|
111
|
+
pid
|
112
|
+
loop!
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|
data/nginx/ssh
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
location /ssh {
|
2
|
+
proxy_pass http://localhost:4567;
|
3
|
+
proxy_http_version 1.1;
|
4
|
+
proxy_set_header Upgrade $http_upgrade;
|
5
|
+
proxy_set_header Connection "upgrade";
|
6
|
+
proxy_set_header X-Real-IP $remote_addr;
|
7
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
8
|
+
}
|
metadata
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: em-wssh
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Stas Ukolov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-03-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: em-websocket
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: faye-websocket
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.3'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.3'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: Proxy SSH connection through Websocket (nginx)
|
70
|
+
email:
|
71
|
+
- ukoloff@gmail.com
|
72
|
+
executables:
|
73
|
+
- wssh
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- .gitignore
|
78
|
+
- FarMenu.ini
|
79
|
+
- Gemfile
|
80
|
+
- LICENSE.txt
|
81
|
+
- README.md
|
82
|
+
- Rakefile
|
83
|
+
- bin/wssh
|
84
|
+
- em-wssh.gemspec
|
85
|
+
- hosts.yml
|
86
|
+
- lib/em/wssh.rb
|
87
|
+
- lib/em/wssh/all.rb
|
88
|
+
- lib/em/wssh/client.rb
|
89
|
+
- lib/em/wssh/connect.rb
|
90
|
+
- lib/em/wssh/exe.rb
|
91
|
+
- lib/em/wssh/help.rb
|
92
|
+
- lib/em/wssh/server.rb
|
93
|
+
- lib/em/wssh/service.rb
|
94
|
+
- nginx/ssh
|
95
|
+
homepage: ''
|
96
|
+
licenses:
|
97
|
+
- MIT
|
98
|
+
metadata: {}
|
99
|
+
post_install_message:
|
100
|
+
rdoc_options: []
|
101
|
+
require_paths:
|
102
|
+
- lib
|
103
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - '>='
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - '>='
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
requirements: []
|
114
|
+
rubyforge_project:
|
115
|
+
rubygems_version: 2.4.6
|
116
|
+
signing_key:
|
117
|
+
specification_version: 4
|
118
|
+
summary: ''
|
119
|
+
test_files: []
|