Skip to content

Commit 582325c

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 c9c4ab1 commit 582325c

File tree

4 files changed

+111
-1
lines changed

4 files changed

+111
-1
lines changed

lib/sshkit/backends/netssh.rb

+91-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,101 @@ 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+
@hosts_keys = nil
30+
end
31+
32+
def keys_for(hostlist)
33+
keys = hosts_keys || parse_file
34+
hostlist.split(',').each do |host|
35+
if key_list = keys[host]
36+
return key_list
37+
end
38+
end
39+
[]
40+
end
41+
42+
private
43+
44+
attr_reader :path
45+
attr_accessor :hosts_keys
46+
47+
def parse_file
48+
synchronize do
49+
return keys if hosts_keys
50+
51+
return self.hosts_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+
hostlist, key = parse_line(scanner)
59+
next unless key
60+
61+
hostlist.each do |host|
62+
(new_keys[host] ||= []) << key
63+
end
64+
end
65+
end
66+
return self.hosts_keys = new_keys
67+
end
68+
end
69+
70+
def parse_line(scanner)
71+
scanner.skip(/\s*/)
72+
return if scanner.match?(/$|#/)
73+
74+
hostlist = scanner.scan(/\S+/).split(',')
75+
scanner.skip(/\s*/)
76+
type = scanner.scan(/\S+/)
77+
78+
return unless Net::SSH::KnownHosts::SUPPORTED_TYPE.include?(type)
79+
80+
scanner.skip(/\s*/)
81+
blob = scanner.rest.unpack("m*").first
82+
return hostlist, Net::SSH::Buffer.new(blob).read_key
83+
end
84+
end
85+
86+
class KnownHosts
87+
include Mutex_m
88+
89+
def initialize
90+
super()
91+
@files = {}
92+
end
93+
94+
def search_for(host, options = {})
95+
::Net::SSH::KnownHosts.hostfiles(options).map do |path|
96+
known_hosts_file(path).keys_for(host)
97+
end.flatten
98+
end
99+
100+
def add(*args)
101+
::Net::SSH::KnownHosts.add(*args)
102+
synchronize { @files = {} }
103+
end
104+
105+
private
106+
107+
def known_hosts_file(path)
108+
@files[path] || synchronize { @files[path] ||= KnownHostsKeys.new(path) }
109+
end
110+
end
21111

22112
class Configuration
23113
attr_accessor :connection_timeout, :pty
24114
attr_writer :ssh_options
25115

26116
def ssh_options
27-
@ssh_options || {}
117+
@ssh_options ||= {known_hosts: KnownHosts.new}
28118
end
29119
end
30120

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

+18
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,23 @@ 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+
assert_equal(1, keys.count)
72+
assert_equal("ssh-rsa", keys[0].ssh_type)
73+
end
5674
end
5775
end
5876
end

0 commit comments

Comments
 (0)