File: known_hosts.rb

package info (click to toggle)
ruby-sshkit 1.21.2-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 700 kB
  • sloc: ruby: 3,522; makefile: 2
file content (145 lines) | stat: -rw-r--r-- 3,594 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
require 'base64'

module SSHKit

  module Backend

    class Netssh < Abstract

      class KnownHostsKeys
        include Mutex_m

        def initialize(path)
          super()
          @path = File.expand_path(path)
          @hosts_keys = nil
        end

        def keys_for(hostlist)
          keys, hashes = hosts_keys, hosts_hashes
          parse_file unless keys && hashes
          keys, hashes = hosts_keys, hosts_hashes

          host_names = hostlist.split(',')

          keys_found = host_names.map { |h| keys[h] || [] }.compact.inject(:&)
          return keys_found unless keys_found.empty?

          host_names.each do |host|
            hashes.each do |(hmac, salt), hash_keys|
              if OpenSSL::HMAC.digest(sha1, salt, host) == hmac
                return hash_keys
              end
            end
          end

          []
        end

        private

        attr_reader :path
        attr_accessor :hosts_keys, :hosts_hashes

        def sha1
          @sha1 ||= OpenSSL::Digest.new('sha1')
        end

        def parse_file
          synchronize do
            return if hosts_keys && hosts_hashes

            unless File.readable?(path)
              self.hosts_keys = {}
              self.hosts_hashes = []
              return
            end

            new_keys = {}
            new_hashes = []
            File.open(path) do |file|
              scanner = StringScanner.new("")
              file.each_line do |line|
                scanner.string = line
                parse_line(scanner, new_keys, new_hashes)
              end
            end
            self.hosts_keys = new_keys
            self.hosts_hashes = new_hashes
          end
        end

        def parse_line(scanner, hosts_keys, hosts_hashes)
          return if empty_line?(scanner)

          hostlist = parse_hostlist(scanner)
          return unless supported_type?(scanner)
          key = parse_key(scanner)

          if hostlist.size == 1 && hostlist.first =~ /\A\|1(\|.+){2}\z/
            hosts_hashes << [parse_host_hash(hostlist.first), key]
          else
            hostlist.each do |host|
              (hosts_keys[host] ||= []) << key
            end
          end
        end

        def parse_host_hash(line)
          _, _, salt, hmac = line.split('|')
          [Base64.decode64(hmac), Base64.decode64(salt)]
        end

        def empty_line?(scanner)
          scanner.skip(/\s*/)
          scanner.match?(/$|#/)
        end

        def parse_hostlist(scanner)
          scanner.skip(/\s*/)
          scanner.scan(/\S+/).split(',')
        end

        def supported_type?(scanner)
          scanner.skip(/\s*/)
          Net::SSH::KnownHosts::SUPPORTED_TYPE.include?(scanner.scan(/\S+/))
        end

        def parse_key(scanner)
          scanner.skip(/\s*/)
          Net::SSH::Buffer.new(scanner.rest.unpack("m*").first).read_key
        end
      end

      class KnownHosts
        include Mutex_m

        def initialize
          super()
          @files = {}
        end

        def search_for(host, options = {})
          keys = ::Net::SSH::KnownHosts.hostfiles(options).map do |path|
            known_hosts_file(path).keys_for(host)
          end.flatten
          ::Net::SSH::HostKeys.new(keys, host, self, options)
        end

        def add(*args)
          ::Net::SSH::KnownHosts.add(*args)
          synchronize { @files = {} }
        end

        private

        def known_hosts_file(path)
          @files[path] || synchronize { @files[path] ||= KnownHostsKeys.new(path) }
        end
      end

    end

  end

end