Skip to content

Commit d2c1983

Browse files
committed
Provide an alternative implementation of Net::SSH::KnownHost in ssh_options
As discussed in capistrano#326 (comment) Net::SSH re-parse the known_hosts files every time it needs to lookup for a known key. This alternative implementation parse it once and for all, and cache the result.
1 parent 112d527 commit d2c1983

File tree

4 files changed

+107
-1
lines changed

4 files changed

+107
-1
lines changed

lib/sshkit/backends/netssh.rb

+86-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require 'strscan'
2+
require 'mutex_m'
13
require 'net/ssh'
24
require 'net/scp'
35

@@ -18,13 +20,96 @@ module SSHKit
1820
module Backend
1921

2022
class Netssh < Abstract
23+
class KnownHostsKeys
24+
include Mutex_m
25+
26+
def initialize(path)
27+
super()
28+
@path = File.expand_path(path)
29+
@keys = nil
30+
end
31+
32+
def keys_for(hostlist)
33+
hosts_keys = keys || parse_file
34+
hostlist.split(',').each do |host|
35+
if key_list = hosts_keys[host]
36+
return key_list
37+
end
38+
end
39+
[]
40+
end
41+
42+
private
43+
44+
attr_reader :path
45+
attr_accessor :keys
46+
47+
def parse_file
48+
synchronize do
49+
return keys if keys
50+
51+
return self.keys = {} unless File.readable?(path)
52+
53+
new_keys = {}
54+
File.open(path) do |file|
55+
scanner = StringScanner.new("")
56+
file.each_line do |line|
57+
scanner.string = line
58+
59+
scanner.skip(/\s*/)
60+
next if scanner.match?(/$|#/)
61+
62+
hostlist = scanner.scan(/\S+/).split(',')
63+
scanner.skip(/\s*/)
64+
type = scanner.scan(/\S+/)
65+
66+
next unless Net::SSH::KnownHosts::SUPPORTED_TYPE.include?(type)
67+
68+
scanner.skip(/\s*/)
69+
blob = scanner.rest.unpack("m*").first
70+
hostlist.each do |host|
71+
host_keys = new_keys[host] ||= []
72+
host_keys << Net::SSH::Buffer.new(blob).read_key
73+
end
74+
end
75+
end
76+
return self.keys = new_keys
77+
end
78+
end
79+
end
80+
81+
class KnownHosts
82+
include Mutex_m
83+
84+
def initialize
85+
super()
86+
@files = {}
87+
end
88+
89+
def search_for(host, options = {})
90+
::Net::SSH::KnownHosts.hostfiles(options).map do |path|
91+
known_hosts_file(path).keys_for(host)
92+
end.flatten
93+
end
94+
95+
def add(*args)
96+
::Net::SSH::KnownHosts.add(*args)
97+
synchronize { @files = {} }
98+
end
99+
100+
private
101+
102+
def known_hosts_file(path)
103+
@files[path] || synchronize { @files[path] ||= KnownHostsKeys.new(path) }
104+
end
105+
end
21106

22107
class Configuration
23108
attr_accessor :connection_timeout, :pty
24109
attr_writer :ssh_options
25110

26111
def ssh_options
27-
@ssh_options || {}
112+
@ssh_options ||= {known_hosts: KnownHosts.new}
28113
end
29114
end
30115

test/known_hosts/github

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==

test/known_hosts/github_hash

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
|1|eKp+6E0rZ3lONgsIziurXEnaIik=|rcQB/rlJMUquUyFta64KugPjX4o= ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==

test/unit/backends/test_netssh.rb

+19
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require 'helper'
2+
require 'tempfile'
23

34
module SSHKit
45
module Backend
@@ -53,6 +54,24 @@ def test_transfer_summarizer
5354
end
5455
end
5556

57+
def test_known_hosts_for_when_all_hosts_are_recognized
58+
perform_known_hosts_test("github")
59+
end
60+
61+
def test_known_hosts_for_when_an_host_hash_is_recognized
62+
perform_known_hosts_test("github_hash")
63+
end
64+
65+
private
66+
67+
def perform_known_hosts_test(hostfile)
68+
source = File.join(File.dirname(__FILE__), '../../known_hosts', hostfile)
69+
kh = Netssh::KnownHosts.new
70+
keys = kh.search_for('github.com', user_known_hosts_file: source, global_known_hosts_file: Tempfile.new('sshkit-test').path)
71+
p keys
72+
assert_equal(1, keys.count)
73+
assert_equal("ssh-rsa", keys[0].ssh_type)
74+
end
5675
end
5776
end
5877
end

0 commit comments

Comments
 (0)