tnnl 0.0.1

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.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in tnnl.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 David Stamm
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.
@@ -0,0 +1,61 @@
1
+ # Tnnl
2
+
3
+ Tnnl is a command-line utility for wrangling SSH tunnels.
4
+
5
+ Unlike most SSH tunnel utilities I've come across, Tnnl will not clutter your
6
+ filesystem with a preferences YAML file. Instead it will try to make use of your
7
+ native SSH config (at `~/.ssh/config` for most folks).
8
+
9
+ __This project is still in an early/alpha state. You have been warned. :)__
10
+
11
+ ## Usage
12
+
13
+ ### Open an SSH tunnel
14
+
15
+ Open an SSH tunnel between local port 1234 and port 5678 on the remote host
16
+ `mysql.spatula.grommet`. If local port 1234 is unavailable, Tnnl will increment
17
+ by 1 until it finds an open port.
18
+
19
+ $ tnnl 1234:admin@mysql.spatula.grommet:3306
20
+
21
+ You can omit the local port number, and Tnnl will try to use the same port for
22
+ the local and remote hosts.
23
+
24
+ $ tnnl admin@mysql.spatula.grommet:3306
25
+
26
+ If you have defined a host alias in your SSH config, you can save yourself some
27
+ keystrokes by referencing that alias.
28
+
29
+ $ tnnl db:3306
30
+
31
+ ### Find and close open tunnels
32
+
33
+ Use `tnnl list` to list all open SSH tunnels that were created by Tnnl.
34
+
35
+ $ tnnl list
36
+ 1. localhost:3307 ==> mysql.spatula.grommet:3306
37
+ 2. localhost:3000 ==> 123.45.67.89:3000
38
+ 3. localhost:666 ==> chunkybacon.gov:666
39
+
40
+ You can use the index numbers referenced in `tnnl list` to close 1 or more
41
+ tunnels.
42
+
43
+ $ tnnl close 2
44
+ $ tnnl close 1 3
45
+
46
+ Or close all tunnels created by Tnnl.
47
+
48
+ $ tnnl close all
49
+
50
+ ## Known Issues
51
+
52
+ - The list feature relies on renaming processes via $0, which does not work
53
+ properly on Ruby 1.9.3-p0 on OS X. This appears to be an issue with this
54
+ particular build of Ruby on this platform
55
+ (https://groups.google.com/forum/#!topic/urug/zfmEGqjX47M). 1.9.3-p0 users on OS
56
+ X are encouraged to upgrade to a newer build.
57
+ - Tnnl uses Net::SSH under the hood, and Net::SSH currently supports only a
58
+ subset of OpenSSH configuration options. The `StrictHostKeyChecking` preference
59
+ is not supported, so Tnnl errs on the safe side and prompts you to update
60
+ ~/.ssh/known_hosts when a modified host key is detected. Feel free to open an
61
+ issue and/or submit a pull request if this is ruining your day.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
4
+
5
+ require 'tnnl'
6
+
7
+ Tnnl::CLI.run!(ARGV)
@@ -0,0 +1,6 @@
1
+ require 'net/ssh'
2
+
3
+ require 'tnnl/ssh'
4
+ require 'tnnl/cli'
5
+ require 'tnnl/process'
6
+ require 'tnnl/version'
@@ -0,0 +1,145 @@
1
+ module Tnnl
2
+ module CLI
3
+ class << self
4
+
5
+ # This is the entry-point for all command-line operations. It parses
6
+ # arguments and delegates to the appropriate methods in Tnnl::CLI and
7
+ # Tnnl::SSH.
8
+ def run!(args)
9
+ command = args.shift || 'help'
10
+
11
+ if command == 'help'
12
+ help
13
+ elsif command == 'list'
14
+ list
15
+ elsif command == 'close'
16
+ close(args)
17
+ elsif command.include? ':'
18
+ open(command)
19
+ else
20
+ help
21
+ end
22
+ end
23
+
24
+ def help
25
+ # Shamelessly stolen from https://github.com/holman/boom
26
+ puts %{
27
+ tnnl (v#{Tnnl::VERSION}) - a utility for managing SSH tunnels
28
+
29
+ tnnl [local-port]:[host]:[remote-port]
30
+ open an SSH tunnel between a port on localhost and port on a remote machine
31
+
32
+ tnnl list
33
+ print a numbered list of open SSH tunnels
34
+
35
+ tnnl close [num] [num...]
36
+ close 1 or more tunnels using the number(s) provided by list
37
+
38
+ tnnl close all
39
+ close all SSH tunnels opened by this program
40
+
41
+ For more detailed documentation, please see:
42
+ https://github.com/whylom/tnnl
43
+ }.gsub(/^ {10}/, '') # strip the first 10 spaces of every line
44
+ end
45
+
46
+ # Display a numbered list of SSH tunnels opened by this utility.
47
+ def list
48
+ processes = Tnnl::Process.list
49
+ abort 'There are no SSH tunnels open at this time.' if processes.empty?
50
+
51
+ processes.each_with_index do |process,i|
52
+ puts " #{i+1}. #{process}"
53
+ end
54
+ end
55
+
56
+ def close(args)
57
+ if args.first == 'all' && args.size == 1
58
+ Tnnl::Process.kill_all
59
+ elsif args.any?
60
+ Tnnl::Process.kill_several(*args.map(&:to_i))
61
+ else
62
+ abort "Usage:\n tnnl close all\n tnnl close [index] [index...]"
63
+ end
64
+ end
65
+
66
+ # Open an SSH tunnel. After parsing the user's command-input input, find
67
+ # the closest available local port. Rescue from common SSH errors, and
68
+ # provide appropriate feedback to the user.
69
+ def open(connection)
70
+ host, user, local_port, remote_port = parse_connection(connection)
71
+ local_port = Tnnl::SSH.find_open_port(local_port)
72
+
73
+ puts "Opening SSH tunnel... "
74
+ Tnnl::SSH.open(host, user, local_port, remote_port)
75
+ puts "localhost:#{local_port} ==> #{user}@#{host}:#{remote_port}"
76
+
77
+ rescue Tnnl::SSH::HostNotFound
78
+ puts 'ERROR: could not resolve host'
79
+ rescue Tnnl::SSH::TimeoutError
80
+ puts 'ERROR: connection timed out'
81
+ rescue Tnnl::SSH::AuthenticationFailed
82
+ puts 'ERROR: authentication failed'
83
+ rescue Tnnl::SSH::HostKeyMismatch => e
84
+ handle_host_key_mismatch(e)
85
+ end
86
+
87
+ # Parses input from command-line and returns an array of SSH connection
88
+ # parameters suitable for passing to Tnnl::SSH.open. Raises ArgumentError
89
+ # if any of the required parameters are missing.
90
+ def parse_connection(connection)
91
+ parts = connection.split(':')
92
+
93
+ if parts.size == 3
94
+ # 1234:host:5678
95
+ local_port, host, remote_port = parts
96
+ elsif parts.size == 2
97
+ # host:5678
98
+ host, remote_port = parts
99
+ local_port = remote_port
100
+ end
101
+
102
+ if host.include? '@'
103
+ # user@host:5678 -> parse manually
104
+ user, host = host.split('@')
105
+ else
106
+ # host:5678 -> look up user & actual hostname in local SSH config
107
+ config = Net::SSH.configuration_for(host)
108
+
109
+ # TODO: Make this a custom exception handled by Tnnl::CLI.run!
110
+ abort "Could not find host '#{host}' in your SSH config." if config.empty?
111
+
112
+ user = config[:user]
113
+ host = config[:host_name]
114
+ end
115
+
116
+ parts = [host, user, local_port.to_i, remote_port.to_i]
117
+ raise ArgumentError if parts.any? { |a| a.nil? }
118
+
119
+ parts
120
+ end
121
+
122
+ # Prompt user to save the new host key in their known hosts file.
123
+ def handle_host_key_mismatch(error)
124
+ valid_responses = %w(y n)
125
+ response = nil
126
+
127
+ while !valid_responses.include?(response)
128
+ puts "\nWARNING! The remote host key has changed."
129
+ puts "Someone could be eavesdropping on your SSH connection."
130
+ puts "It is also possible that the RSA host key has just been changed."
131
+
132
+ puts "\nThe fingerprint for the RSA key sent by the remote host is:"
133
+ puts error.fingerprint
134
+
135
+ print "\nAccept this new key in the known hosts file? (y/n): "
136
+ response = gets.strip.downcase
137
+ end
138
+
139
+ # Record this host and key in the known hosts file if the user wishes.
140
+ error.remember_host! if response == 'y'
141
+ end
142
+
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,51 @@
1
+ module Tnnl
2
+ class Process
3
+ attr_accessor :name, :pid
4
+
5
+ class << self
6
+ # Returns an array of instances of Tnnl::Process representing each of the
7
+ # currently open SSH tunnels created by Tnnl on this machine.
8
+ def list
9
+ # Grep the process list to get processes with "tnnl" somewhere in the
10
+ # name. Format the output from ps for easier parsing.
11
+ list = `ps -eo pid,comm | grep tnnl`.split(/\n/)
12
+
13
+ # Transform each string into an instance of Tnnl::Process.
14
+ processes = list.map do |line|
15
+ pid, name = line.strip.split(' ')
16
+ self.new(pid, name)
17
+ end
18
+
19
+ # Remove any processes we might have found that don't conform to our
20
+ # naming convention.
21
+ processes.select { |p| p.name =~ /^tnnl\[.*\]$/ }
22
+ end
23
+
24
+ def kill_several(*to_kill)
25
+ list.each_with_index do |process, i|
26
+ process.kill if to_kill.include?(i+1)
27
+ end
28
+ end
29
+
30
+ def kill_all
31
+ list.each(&:kill)
32
+ end
33
+ end
34
+
35
+
36
+ def initialize(pid, name)
37
+ @pid = pid.to_i
38
+ @name = name
39
+ end
40
+
41
+ def kill
42
+ ::Process.kill('INT', pid)
43
+ end
44
+
45
+ def to_s
46
+ metadata = name.scan(/\[(.*)\]/).last.first
47
+ local_port, host, remote_port = metadata.split(':')
48
+ "localhost:#{local_port} ==> #{host}:#{remote_port}"
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,83 @@
1
+ module Tnnl
2
+ module SSH
3
+
4
+ class HostNotFound < SocketError ; end
5
+ class TimeoutError < Timeout::Error ; end
6
+ class AuthenticationFailed < Net::SSH::AuthenticationFailed ; end
7
+
8
+ # Wraps & forwards method calls to an instance of Net::SSH::HostKeyMismatch
9
+ class HostKeyMismatch < Net::SSH::Exception
10
+ def initialize(error)
11
+ @error = error
12
+ end
13
+
14
+ def method_missing(method, *args)
15
+ @error.send(method, *args)
16
+ end
17
+ end
18
+
19
+
20
+
21
+ TIMEOUT = 15
22
+
23
+ class << self
24
+
25
+ # Returns an available port on localhost, starting with the provided port
26
+ # and incrementing by 1 until a port is found.
27
+ def find_open_port(port)
28
+ while true
29
+ begin
30
+ # Attempt to bind to the requested port.
31
+ socket = Socket.new(Socket::Constants::AF_INET, Socket::Constants::SOCK_STREAM, 0)
32
+ sockaddr = Socket.pack_sockaddr_in(port, '127.0.0.1')
33
+ socket.bind(sockaddr)
34
+
35
+ # If an exception wasn't raised, the current port is available.
36
+ # Close the socket now (so we can use the port for realsies)
37
+ # and exit the loop by returning the port.
38
+ socket.close
39
+ return port
40
+ rescue Errno::EADDRINUSE
41
+ # If the current port is in use, increment by 1 and try again.
42
+ port += 1
43
+ end
44
+ end
45
+ end
46
+
47
+ # Opens an SSH tunnel from the specified local port, to the requested
48
+ # remote host/port.
49
+ def open(host, user, local_port, remote_port)
50
+ # Impose an artificial timeout on establishing a connection to the
51
+ # remote host.
52
+ Timeout.timeout(TIMEOUT) do
53
+ Net::SSH.start(host, user) do |ssh|
54
+ # Open an SSH tunnel.
55
+ ssh.forward.local(local_port, '127.0.0.1', remote_port)
56
+
57
+ # All of the exceptions we're ready to handle will have already
58
+ # been caught prior to this. Now we can safely fork a new process
59
+ # to keep the tunnel open.
60
+ fork do
61
+ # Rename the forked process so it can be easily located later.
62
+ $0 = "tnnl[#{local_port}:#{user}@#{host}:#{remote_port}]"
63
+
64
+ run = true
65
+ trap('INT') { run = false }
66
+ ssh.loop(0.1) { run }
67
+ end
68
+ end
69
+ end
70
+ rescue SocketError
71
+ raise Tnnl::SSH::HostNotFound
72
+ rescue Timeout::Error
73
+ raise Tnnl::SSH::TimeoutError
74
+ rescue Net::SSH::AuthenticationFailed
75
+ raise Tnnl::SSH::AuthenticationFailed
76
+ rescue Net::SSH::HostKeyMismatch => e
77
+ raise Tnnl::SSH::HostKeyMismatch.new(e)
78
+ end
79
+
80
+ end
81
+
82
+ end
83
+ end
@@ -0,0 +1,3 @@
1
+ module Tnnl
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/tnnl/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ['David Stamm']
6
+ gem.email = ['whylom@gmail.com']
7
+ gem.description = %q{A command-line utility for wrangling SSH tunnels.}
8
+ gem.summary = %q{Tnnl is a command-line utility for wrangling SSH tunnels.}
9
+ gem.homepage = 'https://github.com/whylom/tnnl'
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = 'tnnl'
15
+ gem.require_paths = ['lib']
16
+ gem.version = Tnnl::VERSION
17
+
18
+ gem.add_dependency 'net-ssh', '~> 2.3.0'
19
+ end
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tnnl
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - David Stamm
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-04-24 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: net-ssh
16
+ requirement: &70332409473040 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 2.3.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70332409473040
25
+ description: A command-line utility for wrangling SSH tunnels.
26
+ email:
27
+ - whylom@gmail.com
28
+ executables:
29
+ - tnnl
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - .gitignore
34
+ - Gemfile
35
+ - LICENSE
36
+ - README.md
37
+ - Rakefile
38
+ - bin/tnnl
39
+ - lib/tnnl.rb
40
+ - lib/tnnl/cli.rb
41
+ - lib/tnnl/process.rb
42
+ - lib/tnnl/ssh.rb
43
+ - lib/tnnl/version.rb
44
+ - tnnl.gemspec
45
+ homepage: https://github.com/whylom/tnnl
46
+ licenses: []
47
+ post_install_message:
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ! '>='
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubyforge_project:
65
+ rubygems_version: 1.8.11
66
+ signing_key:
67
+ specification_version: 3
68
+ summary: Tnnl is a command-line utility for wrangling SSH tunnels.
69
+ test_files: []