em-wssh 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](https://badge.fury.io/rb/em-wssh.svg)](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: []
|