tbm 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +7 -8
- data/Rakefile +12 -5
- data/lib/TBM/cli.rb +2 -1
- data/lib/TBM/config_parser.rb +85 -71
- data/lib/TBM/meta.rb +2 -2
- data/spec/config_spec.rb +19 -2
- metadata +5 -4
data/README.md
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
# Tunnel Boring Machine
|
2
|
+
[![Build Status](https://travis-ci.org/geoffreywiseman/tunnel-boring-machine.png?branch=master)](https://travis-ci.org/geoffreywiseman/tunnel-boring-machine) [![Dependency Status](https://gemnasium.com/geoffreywiseman/tunnel-boring-machine.png)](https://gemnasium.com/geoffreywiseman/tunnel-boring-machine) [![Code Climate](https://codeclimate.com/github/geoffreywiseman/tunnel-boring-machine.png)](https://codeclimate.com/github/geoffreywiseman/tunnel-boring-machine)
|
2
3
|
|
3
4
|
Tunnel Boring Machine is a ruby application to manage SSH tunnels, which you can use to achieve something a little like a VPN, wherein SSH access to a server can give you access to the network beyond that server.
|
4
5
|
|
5
6
|
I use SSH tunnels on a regular basis to access resources at client sites that are not exposed directly to the internet as a whole. Managing those tunnels as a series of bash scripts or aliases became cumbersome. I wanted / needed something better, and the tunnel boring machine has evolved from that need.
|
6
7
|
|
7
8
|
## Current Status ##
|
8
|
-
It's pretty early days. I'm using this myself,
|
9
|
+
It's pretty early days. I'm using this myself, and I've shared it so other people can use it, but its advantages over a shell script are still fairly thin. There are some enhancements that I have planned that might make the case much stronger, but we'll see.
|
9
10
|
|
10
11
|
## Installing ##
|
11
12
|
It is bundled as a ruby gem, so if you have Ruby and RubyGems installed, simply run:
|
@@ -17,12 +18,12 @@ If you prefer, you can certainly download it and build it yourself, or simply in
|
|
17
18
|
## Invocation ##
|
18
19
|
For the time being, TBM is a simple command you invoke to open the tunnels you need, then you cancel with `^C` to close the tunnels that you had opened. Something like this:
|
19
20
|
|
20
|
-
$ tbm dev-
|
21
|
+
$ tbm dev-nginx
|
21
22
|
|
22
23
|
Eventually, I expect that TBM will become a little more interactive, allowing you to open additional tunnels without closing the ones you already opened, close a tunnel without closing all of them, and so forth. Whether it does this as an interactive program, a shell command that interacts with a running process is all TBD.
|
23
24
|
|
24
25
|
## Configuration ##
|
25
|
-
You configure the tunnel boring machine by creating a configuration file in YAML form at `~/.
|
26
|
+
You configure the tunnel boring machine by creating a configuration file in YAML form at `~/.tbm`. At the moment, you can't have multiple configuration files, change the location of the configuration file or anything of that nature.
|
26
27
|
|
27
28
|
An example configuration file follows:
|
28
29
|
|
@@ -35,14 +36,12 @@ An example configuration file follows:
|
|
35
36
|
as400: [ 449, 8470, 8471, 8476 ]
|
36
37
|
alias: [ ju, ussi ]
|
37
38
|
qa:
|
38
|
-
|
39
|
-
staging:
|
40
|
-
alias: [ stage, st ]
|
41
|
-
tunnel: 8080:80
|
39
|
+
tunnel: 8080
|
40
|
+
staging (stage, st): 8080:80
|
42
41
|
5250: 8023:as400:23
|
43
42
|
webfacing: as400:10905
|
44
43
|
|
45
|
-
|
44
|
+
I have [documented the configuration file format](http://geoffreywiseman.github.com/tunnel-boring-machine/config.html) on the website.
|
46
45
|
|
47
46
|
## License ##
|
48
47
|
I've put it under the UNLICENSE. Basically, I don't care if you use it, bundle it inside commercial software, or otherwise make use of it, and I don't offer any kind of warranty or support guarantees, nor do I guarantee that any of the projects dependencies are suited for whatever purpose you have in mind. That's all up to you. That said, if you want to talk about it, see the next section.
|
data/Rakefile
CHANGED
@@ -21,19 +21,26 @@ RSpec::Core::RakeTask.new(:coverage) do |t|
|
|
21
21
|
end
|
22
22
|
|
23
23
|
spec = Gem::Specification.new do |spec|
|
24
|
+
# Basics
|
24
25
|
spec.name = 'tbm'
|
25
26
|
spec.version = TBM::VERSION
|
26
27
|
spec.date = TBM::RELEASE_DATE
|
27
28
|
spec.summary = 'Manages SSH Tunnels by creating an SSH connection and forwarding ports based on named targets defined in configuration.'
|
28
29
|
spec.description = 'The "Tunnel Boring Machine" is meant to bore ssh tunnels through the internet to your desired destination simply and repeatedly, as often as you need them. This is a tool for someone who needs SSH tunnels frequently.'
|
29
|
-
spec.
|
30
|
-
spec.email = 'geoffrey.wiseman@codiform.com'
|
31
|
-
spec.homepage = 'http://github.com/geoffreywiseman/tunnel-boring-machine'
|
32
|
-
spec.executables << 'tbm'
|
30
|
+
spec.add_dependency( 'net-ssh', '>= 2.6.2' )
|
33
31
|
|
32
|
+
# Files
|
33
|
+
spec.executables << 'tbm'
|
34
34
|
spec.files = Dir['{lib,spec}/**/*.rb', 'bin/*', 'Rakefile', 'README.md', 'UNLICENSE']
|
35
35
|
|
36
|
-
|
36
|
+
# Documentation
|
37
|
+
spec.has_rdoc = true
|
38
|
+
|
39
|
+
# About
|
40
|
+
spec.author = 'Geoffrey Wiseman'
|
41
|
+
spec.email = 'geoffrey.wiseman@codiform.com'
|
42
|
+
spec.homepage = 'http://github.com/geoffreywiseman/tunnel-boring-machine'
|
43
|
+
spec.license = 'UNLICENSE'
|
37
44
|
end
|
38
45
|
|
39
46
|
Gem::PackageTask.new( spec ) do |pkg|
|
data/lib/TBM/cli.rb
CHANGED
data/lib/TBM/config_parser.rb
CHANGED
@@ -7,8 +7,31 @@ module TBM
|
|
7
7
|
|
8
8
|
# The configuration file used for parsing config.
|
9
9
|
CONFIG_FILE = File.expand_path( '~/.tbm' )
|
10
|
+
|
11
|
+
# Pattern for Gateway Server with Optional Username
|
10
12
|
GATEWAY_PATTERN = /^([^@]+)(@([^@]+))?$/
|
11
13
|
|
14
|
+
# Pattern for a tunnel with a remote host followed by a port (example.com:3333)
|
15
|
+
HOSTPORT_PATTERN = /^([a-zA-Z0-9\.\-]+):(\d{1,5})$/
|
16
|
+
|
17
|
+
# Pattern for a tunnel with a remote host and local and remote ports (1234:example.com:4321)
|
18
|
+
PORTHOSTPORT_PATTERN = /^(\d{1,5}):([a-zA-Z0-9\.\-]+):(\d{1,5})$/
|
19
|
+
|
20
|
+
# Pattern for a tunnel with a local port and a gateway port (1234:4321)
|
21
|
+
PORTPORT_PATTERN = /^(\d{1,5}):(\d{1,5})$/
|
22
|
+
|
23
|
+
# Pattern for a tunnel with a single port to be used client/server to forward to the gateway (1234)
|
24
|
+
PORT_PATTERN = /^\d{1,5}$/
|
25
|
+
|
26
|
+
# Pattern for a target with aliases defined in the target name
|
27
|
+
TARGET_NAME_PATTERN = /^
|
28
|
+
([A-Za-z0-9\.\-\[\]\#]+) # name
|
29
|
+
(\s*\(\s* # optional parenthesized aliases
|
30
|
+
([A-Za-z0-9\.\-\[\]\#]+\s* # first alias
|
31
|
+
(,\s*[A-Za-z0-9\.\-\[\]\#]+\s*)*) # repeating alias pattern with commas
|
32
|
+
\))?\s*$/x # end of parenthesized aliases
|
33
|
+
|
34
|
+
|
12
35
|
# Parses the tunnel boring machine configuration to get a list of targets which can
|
13
36
|
# be invoked to bore tunnels.
|
14
37
|
#
|
@@ -69,16 +92,33 @@ module TBM
|
|
69
92
|
|
70
93
|
def self.parse_targets( gateway_host, gateway_username, targets, config )
|
71
94
|
if Hash === targets
|
72
|
-
targets.each_key do |
|
73
|
-
|
74
|
-
|
75
|
-
|
95
|
+
targets.each_key do |target_name_string|
|
96
|
+
names = parse_target_names( target_name_string.to_s )
|
97
|
+
if names.empty?
|
98
|
+
config.errors << "Cannot parse target name: #{target_name_string}"
|
99
|
+
else
|
100
|
+
target = Target.new( names.shift, gateway_host, gateway_username )
|
101
|
+
config.targets << target
|
102
|
+
names.each { |aka| target.add_alias aka }
|
103
|
+
configure_target( target, targets[target_name_string], config )
|
104
|
+
end
|
76
105
|
end
|
77
106
|
else
|
78
107
|
config.errors << "Cannot parse targets, expected Hash, received: #{targets.class}"
|
79
108
|
end
|
80
109
|
end
|
81
110
|
|
111
|
+
def self.parse_target_names( target_name_string )
|
112
|
+
names = []
|
113
|
+
if TARGET_NAME_PATTERN =~ target_name_string
|
114
|
+
names << $1
|
115
|
+
unless $3.nil?
|
116
|
+
names.concat $3.split(',').map { |x| x.strip }
|
117
|
+
end
|
118
|
+
end
|
119
|
+
return names
|
120
|
+
end
|
121
|
+
|
82
122
|
def self.configure_target( target, target_config, config )
|
83
123
|
case target_config
|
84
124
|
when Fixnum, String
|
@@ -99,54 +139,45 @@ module TBM
|
|
99
139
|
def self.tunnel( target, tunnel_config, config )
|
100
140
|
case tunnel_config
|
101
141
|
when Fixnum
|
102
|
-
|
103
|
-
target.add_tunnel( Tunnel.new( tunnel_config ) )
|
104
|
-
else
|
105
|
-
config.errors << "Invalid port number: #{tunnel_config}"
|
106
|
-
end
|
142
|
+
config.errors.concat validate_and_add( tunnel_config, tunnel_config, target )
|
107
143
|
when String
|
108
144
|
case tunnel_config
|
109
|
-
when
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
port = $1.to_i
|
118
|
-
remote_port = $2.to_i
|
119
|
-
if !valid_port?( port )
|
120
|
-
config.errors << "Invalid local port number #{port} from #{tunnel_config}"
|
121
|
-
elsif !valid_port?( remote_port )
|
122
|
-
config.errors << "Invalid remote port number #{remote_port} from #{tunnel_config}"
|
123
|
-
else
|
124
|
-
target.add_tunnel( Tunnel.new( port, :remote_port => remote_port ) )
|
125
|
-
end
|
126
|
-
when /^(\d{1,5}):([a-zA-Z0-9\.\-]+):(\d{1,5})$/
|
127
|
-
port = $1.to_i
|
128
|
-
remote_host = $2
|
129
|
-
remote_port = $3.to_i
|
130
|
-
if !valid_port?( port )
|
131
|
-
config.errors << "Invalid local port number #{port} from #{tunnel_config}"
|
132
|
-
elsif !valid_port?( remote_port )
|
133
|
-
config.errors << "Invalid remote port number #{remote_port} from #{tunnel_config}"
|
134
|
-
else
|
135
|
-
target.add_tunnel( Tunnel.new( port, :remote_host => remote_host, :remote_port => remote_port ) )
|
136
|
-
end
|
137
|
-
when /^([a-zA-Z0-9\.\-]+):(\d{1,5})$/
|
138
|
-
port = $2.to_i
|
139
|
-
remote_host = $1
|
140
|
-
if !valid_port?( port )
|
141
|
-
config.errors << "Invalid port number #{port} from #{tunnel_config}"
|
142
|
-
else
|
143
|
-
target.add_tunnel( Tunnel.new( port, :remote_host => remote_host ) )
|
144
|
-
end
|
145
|
+
when PORT_PATTERN
|
146
|
+
config.errors.concat validate_and_add( tunnel_config.to_i, tunnel_config, target )
|
147
|
+
when PORTPORT_PATTERN
|
148
|
+
config.errors.concat validate_and_add( $1.to_i, tunnel_config, target, :remote_port => $2.to_i )
|
149
|
+
when PORTHOSTPORT_PATTERN
|
150
|
+
config.errors.concat validate_and_add( $1.to_i, tunnel_config, target, :remote_host => $2, :remote_port => $3.to_i )
|
151
|
+
when HOSTPORT_PATTERN
|
152
|
+
config.errors.concat validate_and_add( $2.to_i, tunnel_config, target, :remote_host => $1 )
|
145
153
|
else
|
146
|
-
config.errors
|
154
|
+
config.errors.concat "Cannot parse tunnel: #{tunnel_config} (#{tunnel_config.class})"
|
147
155
|
end
|
148
156
|
else
|
149
|
-
config.errors
|
157
|
+
config.errors.concat "Cannot parse tunnel: #{tunnel_config} (#{tunnel_config.class})"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def self.validate_and_add( port, tunnel_config, target, options={} )
|
162
|
+
errors = []
|
163
|
+
if options.has_key?( :remote_port ) then
|
164
|
+
validate_port( port, 'local', tunnel_config, errors )
|
165
|
+
validate_port( options[:remote_port], 'remote', tunnel_config, errors )
|
166
|
+
else
|
167
|
+
validate_port( port, nil, tunnel_config, errors )
|
168
|
+
end
|
169
|
+
target.add_tunnel( Tunnel.new( port, options ) ) if errors.empty?
|
170
|
+
return errors
|
171
|
+
end
|
172
|
+
|
173
|
+
def self.validate_port( port, port_qualifier, tunnel_config, errors )
|
174
|
+
if valid_port?( port )
|
175
|
+
return true
|
176
|
+
else
|
177
|
+
qualified_port = ( port_qualifier.nil? ? "port" : "#{port_qualifier} port" )
|
178
|
+
port_source = tunnel_config.to_s.match PORT_PATTERN ? '' : " from ${tunnel_config}"
|
179
|
+
errors << "Invalid #{qualified_port} number #{port}#{port_source}"
|
180
|
+
return false
|
150
181
|
end
|
151
182
|
end
|
152
183
|
|
@@ -184,35 +215,18 @@ module TBM
|
|
184
215
|
def self.remote_tunnel( target, remote_host, tunnel_config, config )
|
185
216
|
case tunnel_config
|
186
217
|
when Fixnum
|
187
|
-
|
188
|
-
target.add_tunnel( Tunnel.new( tunnel_config, :remote_host => remote_host ) )
|
189
|
-
else
|
190
|
-
config.errors << "Invalid port number: #{tunnel_config}"
|
191
|
-
end
|
218
|
+
config.errors.concat validate_and_add( tunnel_config, tunnel_config, target, :remote_host => remote_host )
|
192
219
|
when String
|
193
220
|
case tunnel_config
|
194
|
-
when
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
else
|
199
|
-
config.errors << "Invalid port number: #{tunnel_config}"
|
200
|
-
end
|
201
|
-
when /^(\d{1,5}):(\d{1,5})$/
|
202
|
-
port = $1.to_i
|
203
|
-
remote_port = $2.to_i
|
204
|
-
if !valid_port?( port )
|
205
|
-
config.errors << "Invalid local port number #{port} from #{tunnel_config}"
|
206
|
-
elsif !valid_port?( remote_port )
|
207
|
-
config.errors << "Invalid remote port number #{remote_port} from #{tunnel_config}"
|
208
|
-
else
|
209
|
-
target.add_tunnel( Tunnel.new( port, :remote_port => remote_port, :remote_host => remote_host ) )
|
210
|
-
end
|
221
|
+
when PORT_PATTERN
|
222
|
+
config.errors.concat validate_and_add( tunnel_config.to_i, tunnel_config, target, :remote_host => remote_host )
|
223
|
+
when PORTPORT_PATTERN
|
224
|
+
config.errors.concat validate_and_add( $1.to_i, tunnel_config, target, :remote_host => remote_host, :remote_port => $2.to_i )
|
211
225
|
else
|
212
|
-
config.errors
|
226
|
+
config.errors.concat "Cannot parse tunnel: #{tunnel_config} (#{tunnel_config.class})"
|
213
227
|
end
|
214
228
|
else
|
215
|
-
config.errors
|
229
|
+
config.errors.concat "Cannot parse tunnel: #{tunnel_config} (#{tunnel_config.class})"
|
216
230
|
end
|
217
231
|
end
|
218
232
|
|
data/lib/TBM/meta.rb
CHANGED
data/spec/config_spec.rb
CHANGED
@@ -85,6 +85,23 @@ describe ConfigParser do
|
|
85
85
|
specify { subject.errors.should include_match(/No target config/) }
|
86
86
|
end
|
87
87
|
|
88
|
+
context "with parenthesized alias" do
|
89
|
+
let(:targets) { { 'target-name (aka)' => 8080 } }
|
90
|
+
it "should accept alias as name" do
|
91
|
+
subject.should be_valid
|
92
|
+
subject.get_target( 'aka' ).should_not be_nil
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context "with parenthesized aliases" do
|
97
|
+
let(:targets) { { 'target-name (pseudo,nym)' => 8080 } }
|
98
|
+
it "should accept alias as name" do
|
99
|
+
subject.should be_valid
|
100
|
+
subject.get_target( 'pseudo' ).should_not be_nil
|
101
|
+
subject.get_target( 'nym' ).should_not be_nil
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
88
105
|
context "with target config of 8080" do
|
89
106
|
let(:target) { 8080 }
|
90
107
|
it "should forward port 8080" do
|
@@ -95,7 +112,7 @@ describe ConfigParser do
|
|
95
112
|
|
96
113
|
context "with target config of 0" do
|
97
114
|
let(:target) { 0 }
|
98
|
-
specify { subject.errors.should include( "Invalid port number
|
115
|
+
specify { subject.errors.should include( "Invalid port number 0" ) }
|
99
116
|
end
|
100
117
|
|
101
118
|
context "with target config of '8443'" do
|
@@ -108,7 +125,7 @@ describe ConfigParser do
|
|
108
125
|
|
109
126
|
context "with target config of '77777'" do
|
110
127
|
let(:target) { "77777" }
|
111
|
-
specify { subject.errors.should include( "Invalid port number
|
128
|
+
specify { subject.errors.should include( "Invalid port number 77777" ) }
|
112
129
|
end
|
113
130
|
|
114
131
|
context "with target config of '8080:80'" do
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tbm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-03-07 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: net-ssh
|
@@ -53,7 +53,8 @@ files:
|
|
53
53
|
- README.md
|
54
54
|
- UNLICENSE
|
55
55
|
homepage: http://github.com/geoffreywiseman/tunnel-boring-machine
|
56
|
-
licenses:
|
56
|
+
licenses:
|
57
|
+
- UNLICENSE
|
57
58
|
post_install_message:
|
58
59
|
rdoc_options: []
|
59
60
|
require_paths:
|
@@ -66,7 +67,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
66
67
|
version: '0'
|
67
68
|
segments:
|
68
69
|
- 0
|
69
|
-
hash:
|
70
|
+
hash: 450154189210157473
|
70
71
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
71
72
|
none: false
|
72
73
|
requirements:
|