ddollar-net-ssh 2.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.
- data/CHANGELOG.rdoc +42 -0
- data/Manifest +101 -0
- data/README.rdoc +110 -0
- data/Rakefile +26 -0
- data/THANKS.rdoc +16 -0
- data/lib/net/ssh.rb +199 -0
- data/lib/net/ssh/authentication/agent.rb +175 -0
- data/lib/net/ssh/authentication/constants.rb +18 -0
- data/lib/net/ssh/authentication/key_manager.rb +169 -0
- data/lib/net/ssh/authentication/methods/abstract.rb +60 -0
- data/lib/net/ssh/authentication/methods/hostbased.rb +71 -0
- data/lib/net/ssh/authentication/methods/keyboard_interactive.rb +66 -0
- data/lib/net/ssh/authentication/methods/password.rb +39 -0
- data/lib/net/ssh/authentication/methods/publickey.rb +92 -0
- data/lib/net/ssh/authentication/pageant.rb +176 -0
- data/lib/net/ssh/authentication/session.rb +127 -0
- data/lib/net/ssh/buffer.rb +339 -0
- data/lib/net/ssh/buffered_io.rb +149 -0
- data/lib/net/ssh/config.rb +173 -0
- data/lib/net/ssh/connection/channel.rb +625 -0
- data/lib/net/ssh/connection/constants.rb +33 -0
- data/lib/net/ssh/connection/session.rb +569 -0
- data/lib/net/ssh/connection/term.rb +178 -0
- data/lib/net/ssh/errors.rb +85 -0
- data/lib/net/ssh/key_factory.rb +85 -0
- data/lib/net/ssh/known_hosts.rb +129 -0
- data/lib/net/ssh/loggable.rb +61 -0
- data/lib/net/ssh/packet.rb +102 -0
- data/lib/net/ssh/prompt.rb +93 -0
- data/lib/net/ssh/proxy/errors.rb +14 -0
- data/lib/net/ssh/proxy/http.rb +94 -0
- data/lib/net/ssh/proxy/socks4.rb +70 -0
- data/lib/net/ssh/proxy/socks5.rb +128 -0
- data/lib/net/ssh/service/forward.rb +267 -0
- data/lib/net/ssh/test.rb +89 -0
- data/lib/net/ssh/test/channel.rb +129 -0
- data/lib/net/ssh/test/extensions.rb +152 -0
- data/lib/net/ssh/test/kex.rb +44 -0
- data/lib/net/ssh/test/local_packet.rb +51 -0
- data/lib/net/ssh/test/packet.rb +81 -0
- data/lib/net/ssh/test/remote_packet.rb +38 -0
- data/lib/net/ssh/test/script.rb +157 -0
- data/lib/net/ssh/test/socket.rb +59 -0
- data/lib/net/ssh/transport/algorithms.rb +384 -0
- data/lib/net/ssh/transport/cipher_factory.rb +72 -0
- data/lib/net/ssh/transport/constants.rb +30 -0
- data/lib/net/ssh/transport/hmac.rb +31 -0
- data/lib/net/ssh/transport/hmac/abstract.rb +48 -0
- data/lib/net/ssh/transport/hmac/md5.rb +12 -0
- data/lib/net/ssh/transport/hmac/md5_96.rb +11 -0
- data/lib/net/ssh/transport/hmac/none.rb +15 -0
- data/lib/net/ssh/transport/hmac/sha1.rb +13 -0
- data/lib/net/ssh/transport/hmac/sha1_96.rb +11 -0
- data/lib/net/ssh/transport/identity_cipher.rb +40 -0
- data/lib/net/ssh/transport/kex.rb +13 -0
- data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +208 -0
- data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +77 -0
- data/lib/net/ssh/transport/openssl.rb +128 -0
- data/lib/net/ssh/transport/packet_stream.rb +230 -0
- data/lib/net/ssh/transport/server_version.rb +61 -0
- data/lib/net/ssh/transport/session.rb +262 -0
- data/lib/net/ssh/transport/state.rb +170 -0
- data/lib/net/ssh/verifiers/lenient.rb +30 -0
- data/lib/net/ssh/verifiers/null.rb +12 -0
- data/lib/net/ssh/verifiers/strict.rb +53 -0
- data/lib/net/ssh/version.rb +60 -0
- data/net-ssh.gemspec +56 -0
- data/setup.rb +1585 -0
- data/test/authentication/methods/common.rb +28 -0
- data/test/authentication/methods/test_abstract.rb +51 -0
- data/test/authentication/methods/test_hostbased.rb +108 -0
- data/test/authentication/methods/test_keyboard_interactive.rb +98 -0
- data/test/authentication/methods/test_password.rb +50 -0
- data/test/authentication/methods/test_publickey.rb +123 -0
- data/test/authentication/test_agent.rb +205 -0
- data/test/authentication/test_key_manager.rb +100 -0
- data/test/authentication/test_session.rb +93 -0
- data/test/common.rb +106 -0
- data/test/configs/exact_match +8 -0
- data/test/configs/wild_cards +14 -0
- data/test/connection/test_channel.rb +452 -0
- data/test/connection/test_session.rb +483 -0
- data/test/test_all.rb +6 -0
- data/test/test_buffer.rb +336 -0
- data/test/test_buffered_io.rb +63 -0
- data/test/test_config.rb +78 -0
- data/test/test_key_factory.rb +67 -0
- data/test/transport/hmac/test_md5.rb +34 -0
- data/test/transport/hmac/test_md5_96.rb +25 -0
- data/test/transport/hmac/test_none.rb +34 -0
- data/test/transport/hmac/test_sha1.rb +34 -0
- data/test/transport/hmac/test_sha1_96.rb +25 -0
- data/test/transport/kex/test_diffie_hellman_group1_sha1.rb +146 -0
- data/test/transport/kex/test_diffie_hellman_group_exchange_sha1.rb +92 -0
- data/test/transport/test_algorithms.rb +302 -0
- data/test/transport/test_cipher_factory.rb +163 -0
- data/test/transport/test_hmac.rb +34 -0
- data/test/transport/test_identity_cipher.rb +40 -0
- data/test/transport/test_packet_stream.rb +433 -0
- data/test/transport/test_server_version.rb +55 -0
- data/test/transport/test_session.rb +312 -0
- data/test/transport/test_state.rb +173 -0
- metadata +222 -0
@@ -0,0 +1,61 @@
|
|
1
|
+
module Net; module SSH
|
2
|
+
|
3
|
+
# A simple module to make logging easier to deal with. It assumes that the
|
4
|
+
# logger instance (if not nil) quacks like a Logger object (in Ruby's
|
5
|
+
# standard library). Although used primarily internally by Net::SSH, it
|
6
|
+
# can easily be used to add Net::SSH-like logging to your own programs.
|
7
|
+
#
|
8
|
+
# class MyClass
|
9
|
+
# include Net::SSH::Loggable
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# Net::SSH.start(...) do |ssh|
|
13
|
+
# obj = MyClass.new
|
14
|
+
# obj.logger = ssh.logger
|
15
|
+
# ...
|
16
|
+
# end
|
17
|
+
module Loggable
|
18
|
+
# The logger instance that will be used to log messages. If nil, nothing
|
19
|
+
# will be logged.
|
20
|
+
attr_accessor :logger
|
21
|
+
|
22
|
+
# Displays the result of yielding if the log level is Logger::DEBUG or
|
23
|
+
# greater.
|
24
|
+
def debug
|
25
|
+
logger.add(Logger::DEBUG, nil, facility) { yield } if logger
|
26
|
+
end
|
27
|
+
|
28
|
+
# Displays the result of yielding if the log level is Logger::INFO or
|
29
|
+
# greater.
|
30
|
+
def info
|
31
|
+
logger.add(Logger::INFO, nil, facility) { yield } if logger
|
32
|
+
end
|
33
|
+
|
34
|
+
# Displays the result of yielding if the log level is Logger::WARN or
|
35
|
+
# greater. (Called lwarn to avoid shadowing with Kernel#warn.)
|
36
|
+
def lwarn
|
37
|
+
logger.add(Logger::WARN, nil, facility) { yield } if logger
|
38
|
+
end
|
39
|
+
|
40
|
+
# Displays the result of yielding if the log level is Logger:ERROR or
|
41
|
+
# greater.
|
42
|
+
def error
|
43
|
+
logger.add(Logger::ERROR, nil, facility) { yield } if logger
|
44
|
+
end
|
45
|
+
|
46
|
+
# Displays the result of yielding if the log level is Logger::FATAL or
|
47
|
+
# greater.
|
48
|
+
def fatal
|
49
|
+
logger.add(Logger::FATAL, nil, facility) { yield } if logger
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# Sets the "facility" value, used for reporting where a log message
|
55
|
+
# originates. It defaults to the name of class with the object_id
|
56
|
+
# appended.
|
57
|
+
def facility
|
58
|
+
@facility ||= self.class.name.gsub(/::/, ".").gsub(/([a-z])([A-Z])/, "\\1_\\2").downcase + "[%x]" % object_id
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end; end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'net/ssh/buffer'
|
2
|
+
require 'net/ssh/transport/constants'
|
3
|
+
require 'net/ssh/authentication/constants'
|
4
|
+
require 'net/ssh/connection/constants'
|
5
|
+
|
6
|
+
module Net; module SSH
|
7
|
+
|
8
|
+
# A specialization of Buffer that knows the format of certain common
|
9
|
+
# packet types. It auto-parses those packet types, and allows them to
|
10
|
+
# be accessed via the #[] accessor.
|
11
|
+
#
|
12
|
+
# data = some_channel_request_packet
|
13
|
+
# packet = Net::SSH::Packet.new(data)
|
14
|
+
#
|
15
|
+
# p packet.type #-> 98 (CHANNEL_REQUEST)
|
16
|
+
# p packet[:request]
|
17
|
+
# p packet[:want_reply]
|
18
|
+
#
|
19
|
+
# This is used exclusively internally by Net::SSH, and unless you're doing
|
20
|
+
# protocol-level manipulation or are extending Net::SSH in some way, you'll
|
21
|
+
# never need to use this class directly.
|
22
|
+
class Packet < Buffer
|
23
|
+
@@types = {}
|
24
|
+
|
25
|
+
# Register a new packet type that should be recognized and auto-parsed by
|
26
|
+
# Net::SSH::Packet. Note that any packet type that is not preregistered
|
27
|
+
# will not be autoparsed.
|
28
|
+
#
|
29
|
+
# The +pairs+ parameter must be either empty, or an array of two-element
|
30
|
+
# tuples, where the first element of each tuple is the name of the field,
|
31
|
+
# and the second is the type.
|
32
|
+
#
|
33
|
+
# register DISCONNECT, [:reason_code, :long], [:description, :string], [:language, :string]
|
34
|
+
def self.register(type, *pairs)
|
35
|
+
@@types[type] = pairs
|
36
|
+
end
|
37
|
+
|
38
|
+
include Transport::Constants, Authentication::Constants, Connection::Constants
|
39
|
+
|
40
|
+
#--
|
41
|
+
# These are the recognized packet types. All other packet types will be
|
42
|
+
# accepted, but not auto-parsed, requiring the client to parse the
|
43
|
+
# fields using the methods provided by Net::SSH::Buffer.
|
44
|
+
#++
|
45
|
+
|
46
|
+
register DISCONNECT, [:reason_code, :long], [:description, :string], [:language, :string]
|
47
|
+
register IGNORE, [:data, :string]
|
48
|
+
register UNIMPLEMENTED, [:number, :long]
|
49
|
+
register DEBUG, [:always_display, :bool], [:message, :string], [:language, :string]
|
50
|
+
register SERVICE_ACCEPT, [:service_name, :string]
|
51
|
+
register USERAUTH_BANNER, [:message, :string], [:language, :string]
|
52
|
+
register USERAUTH_FAILURE, [:authentications, :string], [:partial_success, :bool]
|
53
|
+
register GLOBAL_REQUEST, [:request_type, :string], [:want_reply, :bool], [:request_data, :buffer]
|
54
|
+
register CHANNEL_OPEN, [:channel_type, :string], [:remote_id, :long], [:window_size, :long], [:packet_size, :long]
|
55
|
+
register CHANNEL_OPEN_CONFIRMATION, [:local_id, :long], [:remote_id, :long], [:window_size, :long], [:packet_size, :long]
|
56
|
+
register CHANNEL_OPEN_FAILURE, [:local_id, :long], [:reason_code, :long], [:description, :string], [:language, :string]
|
57
|
+
register CHANNEL_WINDOW_ADJUST, [:local_id, :long], [:extra_bytes, :long]
|
58
|
+
register CHANNEL_DATA, [:local_id, :long], [:data, :string]
|
59
|
+
register CHANNEL_EXTENDED_DATA, [:local_id, :long], [:data_type, :long], [:data, :string]
|
60
|
+
register CHANNEL_EOF, [:local_id, :long]
|
61
|
+
register CHANNEL_CLOSE, [:local_id, :long]
|
62
|
+
register CHANNEL_REQUEST, [:local_id, :long], [:request, :string], [:want_reply, :bool], [:request_data, :buffer]
|
63
|
+
register CHANNEL_SUCCESS, [:local_id, :long]
|
64
|
+
register CHANNEL_FAILURE, [:local_id, :long]
|
65
|
+
|
66
|
+
# The (integer) type of this packet.
|
67
|
+
attr_reader :type
|
68
|
+
|
69
|
+
# Create a new packet from the given payload. This will automatically
|
70
|
+
# parse the packet if it is one that has been previously registered with
|
71
|
+
# Packet.register; otherwise, the packet will need to be manually parsed
|
72
|
+
# using the methods provided in the Net::SSH::Buffer superclass.
|
73
|
+
def initialize(payload)
|
74
|
+
@named_elements = {}
|
75
|
+
super
|
76
|
+
@type = read_byte
|
77
|
+
instantiate!
|
78
|
+
end
|
79
|
+
|
80
|
+
# Access one of the auto-parsed fields by name. Raises an error if no
|
81
|
+
# element by the given name exists.
|
82
|
+
def [](name)
|
83
|
+
name = name.to_sym
|
84
|
+
raise ArgumentError, "no such element #{name}" unless @named_elements.key?(name)
|
85
|
+
@named_elements[name]
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
# Parse the packet's contents and assign the named elements, as described
|
91
|
+
# by the registered format for the packet.
|
92
|
+
def instantiate!
|
93
|
+
(@@types[type] || []).each do |name, datatype|
|
94
|
+
@named_elements[name.to_sym] = if datatype == :buffer
|
95
|
+
remainder_as_buffer
|
96
|
+
else
|
97
|
+
send("read_#{datatype}")
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end; end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Net; module SSH
|
2
|
+
|
3
|
+
# A basic prompt module that can be mixed into other objects. If HighLine is
|
4
|
+
# installed, it will be used to display prompts and read input from the
|
5
|
+
# user. Otherwise, the termios library will be used. If neither HighLine
|
6
|
+
# nor termios is installed, a simple prompt that echos text in the clear
|
7
|
+
# will be used.
|
8
|
+
|
9
|
+
module PromptMethods
|
10
|
+
|
11
|
+
# Defines the prompt method to use if the Highline library is installed.
|
12
|
+
module Highline
|
13
|
+
# Uses Highline#ask to present a prompt and accept input. If +echo+ is
|
14
|
+
# +false+, the characters entered by the user will not be echoed to the
|
15
|
+
# screen.
|
16
|
+
def prompt(prompt, echo=true)
|
17
|
+
@highline ||= ::HighLine.new
|
18
|
+
@highline.ask(prompt + " ") { |q| q.echo = echo }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Defines the prompt method to use if the Termios library is installed.
|
23
|
+
module Termios
|
24
|
+
# Displays the prompt to $stdout. If +echo+ is false, the Termios
|
25
|
+
# library will be used to disable keystroke echoing for the duration of
|
26
|
+
# this method.
|
27
|
+
def prompt(prompt, echo=true)
|
28
|
+
$stdout.print(prompt)
|
29
|
+
$stdout.flush
|
30
|
+
|
31
|
+
set_echo(false) unless echo
|
32
|
+
$stdin.gets.chomp
|
33
|
+
ensure
|
34
|
+
if !echo
|
35
|
+
set_echo(true)
|
36
|
+
$stdout.puts
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Enables or disables keystroke echoing using the Termios library.
|
43
|
+
def set_echo(enable)
|
44
|
+
term = ::Termios.getattr($stdin)
|
45
|
+
|
46
|
+
if enable
|
47
|
+
term.c_lflag |= (::Termios::ECHO | ::Termios::ICANON)
|
48
|
+
else
|
49
|
+
term.c_lflag &= ~::Termios::ECHO
|
50
|
+
end
|
51
|
+
|
52
|
+
::Termios.setattr($stdin, ::Termios::TCSANOW, term)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Defines the prompt method to use when neither Highline nor Termios are
|
57
|
+
# installed.
|
58
|
+
module Clear
|
59
|
+
# Displays the prompt to $stdout and pulls the response from $stdin.
|
60
|
+
# Text is always echoed in the clear, regardless of the +echo+ setting.
|
61
|
+
# The first time a prompt is given and +echo+ is false, a warning will
|
62
|
+
# be written to $stderr recommending that either Highline or Termios
|
63
|
+
# be installed.
|
64
|
+
def prompt(prompt, echo=true)
|
65
|
+
@seen_warning ||= false
|
66
|
+
if !echo && !@seen_warning
|
67
|
+
$stderr.puts "Text will be echoed in the clear. Please install the HighLine or Termios libraries to suppress echoed text."
|
68
|
+
@seen_warning = true
|
69
|
+
end
|
70
|
+
|
71
|
+
$stdout.print(prompt)
|
72
|
+
$stdout.flush
|
73
|
+
$stdin.gets.chomp
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Try to load Highline and Termios in turn, selecting the corresponding
|
79
|
+
# PromptMethods module to use. If neither are available, choose PromptMethods::Clear.
|
80
|
+
Prompt = begin
|
81
|
+
require 'highline'
|
82
|
+
HighLine.track_eof = false
|
83
|
+
PromptMethods::Highline
|
84
|
+
rescue LoadError
|
85
|
+
begin
|
86
|
+
require 'termios'
|
87
|
+
PromptMethods::Termios
|
88
|
+
rescue LoadError
|
89
|
+
PromptMethods::Clear
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
end; end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'net/ssh/errors'
|
2
|
+
|
3
|
+
module Net; module SSH; module Proxy
|
4
|
+
|
5
|
+
# A general exception class for all Proxy errors.
|
6
|
+
class Error < Net::SSH::Exception; end
|
7
|
+
|
8
|
+
# Used for reporting proxy connection errors.
|
9
|
+
class ConnectError < Error; end
|
10
|
+
|
11
|
+
# Used when the server doesn't recognize the user's credentials.
|
12
|
+
class UnauthorizedError < Error; end
|
13
|
+
|
14
|
+
end; end; end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'net/ssh/proxy/errors'
|
3
|
+
|
4
|
+
module Net; module SSH; module Proxy
|
5
|
+
|
6
|
+
# An implementation of an HTTP proxy. To use it, instantiate it, then
|
7
|
+
# pass the instantiated object via the :proxy key to Net::SSH.start:
|
8
|
+
#
|
9
|
+
# require 'net/ssh/proxy/http'
|
10
|
+
#
|
11
|
+
# proxy = Net::SSH::Proxy::HTTP.new('proxy.host', proxy_port)
|
12
|
+
# Net::SSH.start('host', 'user', :proxy => proxy) do |ssh|
|
13
|
+
# ...
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# If the proxy requires authentication, you can pass :user and :password
|
17
|
+
# to the proxy's constructor:
|
18
|
+
#
|
19
|
+
# proxy = Net::SSH::Proxy::HTTP.new('proxy.host', proxy_port,
|
20
|
+
# :user => "user", :password => "password")
|
21
|
+
#
|
22
|
+
# Note that HTTP digest authentication is not supported; Basic only at
|
23
|
+
# this point.
|
24
|
+
class HTTP
|
25
|
+
|
26
|
+
# The hostname or IP address of the HTTP proxy.
|
27
|
+
attr_reader :proxy_host
|
28
|
+
|
29
|
+
# The port number of the proxy.
|
30
|
+
attr_reader :proxy_port
|
31
|
+
|
32
|
+
# The map of additional options that were given to the object at
|
33
|
+
# initialization.
|
34
|
+
attr_reader :options
|
35
|
+
|
36
|
+
# Create a new socket factory that tunnels via the given host and
|
37
|
+
# port. The +options+ parameter is a hash of additional settings that
|
38
|
+
# can be used to tweak this proxy connection. Specifically, the following
|
39
|
+
# options are supported:
|
40
|
+
#
|
41
|
+
# * :user => the user name to use when authenticating to the proxy
|
42
|
+
# * :password => the password to use when authenticating
|
43
|
+
def initialize(proxy_host, proxy_port=80, options={})
|
44
|
+
@proxy_host = proxy_host
|
45
|
+
@proxy_port = proxy_port
|
46
|
+
@options = options
|
47
|
+
end
|
48
|
+
|
49
|
+
# Return a new socket connected to the given host and port via the
|
50
|
+
# proxy that was requested when the socket factory was instantiated.
|
51
|
+
def open(host, port)
|
52
|
+
socket = TCPSocket.new(proxy_host, proxy_port)
|
53
|
+
socket.write "CONNECT #{host}:#{port} HTTP/1.0\r\n"
|
54
|
+
|
55
|
+
if options[:user]
|
56
|
+
credentials = ["#{options[:user]}:#{options[:password]}"].pack("m*").gsub(/\s/, "")
|
57
|
+
socket.write "Proxy-Authorization: Basic #{credentials}\r\n"
|
58
|
+
end
|
59
|
+
|
60
|
+
socket.write "\r\n"
|
61
|
+
|
62
|
+
resp = parse_response(socket)
|
63
|
+
|
64
|
+
return socket if resp[:code] == 200
|
65
|
+
|
66
|
+
socket.close
|
67
|
+
raise ConnectError, resp.inspect
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def parse_response(socket)
|
73
|
+
version, code, reason = socket.gets.chomp.split(/ /, 3)
|
74
|
+
headers = {}
|
75
|
+
|
76
|
+
while (line = socket.gets.chomp) != ""
|
77
|
+
name, value = line.split(/:/, 2)
|
78
|
+
headers[name.strip] = value.strip
|
79
|
+
end
|
80
|
+
|
81
|
+
if headers["Content-Length"]
|
82
|
+
body = socket.read(headers["Content-Length"].to_i)
|
83
|
+
end
|
84
|
+
|
85
|
+
return { :version => version,
|
86
|
+
:code => code.to_i,
|
87
|
+
:reason => reason,
|
88
|
+
:headers => headers,
|
89
|
+
:body => body }
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
end; end; end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'resolv'
|
3
|
+
require 'ipaddr'
|
4
|
+
require 'net/ssh/proxy/errors'
|
5
|
+
|
6
|
+
module Net
|
7
|
+
module SSH
|
8
|
+
module Proxy
|
9
|
+
|
10
|
+
# An implementation of a SOCKS4 proxy. To use it, instantiate it, then
|
11
|
+
# pass the instantiated object via the :proxy key to Net::SSH.start:
|
12
|
+
#
|
13
|
+
# require 'net/ssh/proxy/socks4'
|
14
|
+
#
|
15
|
+
# proxy = Net::SSH::Proxy::SOCKS4.new('proxy.host', proxy_port, :user => 'user')
|
16
|
+
# Net::SSH.start('host', 'user', :proxy => proxy) do |ssh|
|
17
|
+
# ...
|
18
|
+
# end
|
19
|
+
class SOCKS4
|
20
|
+
|
21
|
+
# The SOCKS protocol version used by this class
|
22
|
+
VERSION = 4
|
23
|
+
|
24
|
+
# The packet type for connection requests
|
25
|
+
CONNECT = 1
|
26
|
+
|
27
|
+
# The status code for a successful connection
|
28
|
+
GRANTED = 90
|
29
|
+
|
30
|
+
# The proxy's host name or IP address, as given to the constructor.
|
31
|
+
attr_reader :proxy_host
|
32
|
+
|
33
|
+
# The proxy's port number.
|
34
|
+
attr_reader :proxy_port
|
35
|
+
|
36
|
+
# The additional options that were given to the proxy's constructor.
|
37
|
+
attr_reader :options
|
38
|
+
|
39
|
+
# Create a new proxy connection to the given proxy host and port.
|
40
|
+
# Optionally, a :user key may be given to identify the username
|
41
|
+
# with which to authenticate.
|
42
|
+
def initialize(proxy_host, proxy_port=1080, options={})
|
43
|
+
@proxy_host = proxy_host
|
44
|
+
@proxy_port = proxy_port
|
45
|
+
@options = options
|
46
|
+
end
|
47
|
+
|
48
|
+
# Return a new socket connected to the given host and port via the
|
49
|
+
# proxy that was requested when the socket factory was instantiated.
|
50
|
+
def open(host, port)
|
51
|
+
socket = TCPSocket.new(proxy_host, proxy_port)
|
52
|
+
ip_addr = IPAddr.new(Resolv.getaddress(host))
|
53
|
+
|
54
|
+
packet = [VERSION, CONNECT, port.to_i, ip_addr.to_i, options[:user]].pack("CCnNZ*")
|
55
|
+
socket.send packet, 0
|
56
|
+
|
57
|
+
version, status, port, ip = socket.recv(8).unpack("CCnN")
|
58
|
+
if status != GRANTED
|
59
|
+
socket.close
|
60
|
+
raise ConnectError, "error connecting to proxy (#{status})"
|
61
|
+
end
|
62
|
+
|
63
|
+
return socket
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'net/ssh/proxy/errors'
|
3
|
+
|
4
|
+
module Net
|
5
|
+
module SSH
|
6
|
+
module Proxy
|
7
|
+
|
8
|
+
# An implementation of a SOCKS5 proxy. To use it, instantiate it, then
|
9
|
+
# pass the instantiated object via the :proxy key to Net::SSH.start:
|
10
|
+
#
|
11
|
+
# require 'net/ssh/proxy/socks5'
|
12
|
+
#
|
13
|
+
# proxy = Net::SSH::Proxy::SOCKS5.new('proxy.host', proxy_port,
|
14
|
+
# :user => 'user', :password => "password")
|
15
|
+
# Net::SSH.start('host', 'user', :proxy => proxy) do |ssh|
|
16
|
+
# ...
|
17
|
+
# end
|
18
|
+
class SOCKS5
|
19
|
+
# The SOCKS protocol version used by this class
|
20
|
+
VERSION = 5
|
21
|
+
|
22
|
+
# The SOCKS authentication type for requests without authentication
|
23
|
+
METHOD_NO_AUTH = 0
|
24
|
+
|
25
|
+
# The SOCKS authentication type for requests via username/password
|
26
|
+
METHOD_PASSWD = 2
|
27
|
+
|
28
|
+
# The SOCKS authentication type for when there are no supported
|
29
|
+
# authentication methods.
|
30
|
+
METHOD_NONE = 0xFF
|
31
|
+
|
32
|
+
# The SOCKS packet type for requesting a proxy connection.
|
33
|
+
CMD_CONNECT = 1
|
34
|
+
|
35
|
+
# The SOCKS address type for connections via IP address.
|
36
|
+
ATYP_IPV4 = 1
|
37
|
+
|
38
|
+
# The SOCKS address type for connections via domain name.
|
39
|
+
ATYP_DOMAIN = 3
|
40
|
+
|
41
|
+
# The SOCKS response code for a successful operation.
|
42
|
+
SUCCESS = 0
|
43
|
+
|
44
|
+
# The proxy's host name or IP address
|
45
|
+
attr_reader :proxy_host
|
46
|
+
|
47
|
+
# The proxy's port number
|
48
|
+
attr_reader :proxy_port
|
49
|
+
|
50
|
+
# The map of options given at initialization
|
51
|
+
attr_reader :options
|
52
|
+
|
53
|
+
# Create a new proxy connection to the given proxy host and port.
|
54
|
+
# Optionally, :user and :password options may be given to
|
55
|
+
# identify the username and password with which to authenticate.
|
56
|
+
def initialize(proxy_host, proxy_port=1080, options={})
|
57
|
+
@proxy_host = proxy_host
|
58
|
+
@proxy_port = proxy_port
|
59
|
+
@options = options
|
60
|
+
end
|
61
|
+
|
62
|
+
# Return a new socket connected to the given host and port via the
|
63
|
+
# proxy that was requested when the socket factory was instantiated.
|
64
|
+
def open(host, port)
|
65
|
+
socket = TCPSocket.new(proxy_host, proxy_port)
|
66
|
+
|
67
|
+
methods = [METHOD_NO_AUTH]
|
68
|
+
methods << METHOD_PASSWD if options[:user]
|
69
|
+
|
70
|
+
packet = [VERSION, methods.size, *methods].pack("C*")
|
71
|
+
socket.send packet, 0
|
72
|
+
|
73
|
+
version, method = socket.recv(2).unpack("CC")
|
74
|
+
if version != VERSION
|
75
|
+
socket.close
|
76
|
+
raise Net::SSH::Proxy::Error, "invalid SOCKS version (#{version})"
|
77
|
+
end
|
78
|
+
|
79
|
+
if method == METHOD_NONE
|
80
|
+
socket.close
|
81
|
+
raise Net::SSH::Proxy::Error, "no supported authorization methods"
|
82
|
+
end
|
83
|
+
|
84
|
+
negotiate_password(socket) if method == METHOD_PASSWD
|
85
|
+
|
86
|
+
packet = [VERSION, CMD_CONNECT, 0].pack("C*")
|
87
|
+
|
88
|
+
if host =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/
|
89
|
+
packet << [ATYP_IPV4, $1.to_i, $2.to_i, $3.to_i, $4.to_i].pack("C*")
|
90
|
+
else
|
91
|
+
packet << [ATYP_DOMAIN, host.length, host].pack("CCA*")
|
92
|
+
end
|
93
|
+
|
94
|
+
packet << [port].pack("n")
|
95
|
+
socket.send packet, 0
|
96
|
+
|
97
|
+
version, reply, = socket.recv(4).unpack("C*")
|
98
|
+
len = socket.recv(1)[0]
|
99
|
+
socket.recv(len + 2)
|
100
|
+
|
101
|
+
unless reply == SUCCESS
|
102
|
+
socket.close
|
103
|
+
raise ConnectError, "#{reply}"
|
104
|
+
end
|
105
|
+
|
106
|
+
return socket
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
# Simple username/password negotiation with the SOCKS5 server.
|
112
|
+
def negotiate_password(socket)
|
113
|
+
packet = [0x01, options[:user].length, options[:user],
|
114
|
+
options[:password].length, options[:password]].pack("CCA*CA*")
|
115
|
+
socket.send packet, 0
|
116
|
+
|
117
|
+
version, status = socket.recv(2).unpack("CC")
|
118
|
+
|
119
|
+
if status != SUCCESS
|
120
|
+
socket.close
|
121
|
+
raise UnauthorizedError, "could not authorize user"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|