tnnl 0.0.1

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