hostinfo.rb 3.33 KB
Newer Older
1
#!/usr/bin/env ruby
2

3
Thread.abort_on_exception = true
4

5
require 'net/https'
6

7 8 9 10 11 12 13 14 15 16 17
class Check
  # These are provided or computed
  attr_reader :hostname, :https_url, :ssh_port

  # These are looked up in parallel
  attr_reader :ip, :https_response, :ssh_port_open, :network_owner, :rev_name

  def initialize(hostname)
    @hostname = hostname
    @https_url  = "https://#{hostname}"
    @ssh_port = hostname.start_with?('altssh') ? 443 : 22
18 19
  end

20 21
  def execute
    parallel_lookup!
22

23 24 25 26 27 28 29 30
    [
      hostname,
      network_owner,
      rev_name,
      https_status,
      ssh_port_open,
      redirect
    ].join("\t")
31 32
  end

33 34
  def https_status
    return "Invalid" unless https_response
35

36
    https_response.code
37 38
  end

39 40 41 42
  def redirect
    return "-" unless https_response && https_response.key?('Location')

    https_response['Location'][0..39]
43 44
  end

45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
  private

  def parallel_lookup!
    # Checking whois and reverse IP lookup depends on the IP being resolved,
    # so set it off separately
    ip_thread = Thread.new { @ip = resolv(hostname) }

    threads = [
      Thread.new { @https_response = curl(https_url) },
      Thread.new { @ssh_port_open = check_ssh }
    ]

    # Once this join completes, it's safe to reference the IP
    ip_thread.join

    threads.concat([
      Thread.new { @network_owner = check_network_owner },
      Thread.new { @rev_name = check_rev_name }
    ])

    threads.map(&:join)
66 67
  end

68 69 70 71 72 73 74 75 76 77 78
  def popen(cmd)
    cmd.push(err: :close)
    result = IO.popen(cmd) { |io| io.read.strip }
    if $?.success?
      result
    else
      nil
    end
  end

  def check_ssh
79
    result = popen(%W[ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -oConnectTimeout=5 -p #{ssh_port} -q -T git@#{hostname}])
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

    if result =~ /Welcome to GitLab/
      "Yes"
    else
      "No"
    end
  end
  def check_network_owner
    return "DOES_NOT_RESOLVE" unless ip

    whois(ip) || "N/A"
  end

  def check_rev_name
    return "DOES_NOT_RESOLVE" unless ip

    resolv(ip, ptr: true) || ip
  end

  def resolv(hostname, ptr: false)
    cmd = ['dig', '+short']
    cmd << '-x' if ptr
    cmd << hostname

    result = popen(cmd)
    return nil unless result

    result.split("\n")[-1]
  end

  def whois(thing)
    result = popen(%W[whois #{thing}])
    return nil unless result

    orgname = result.lines.find { |l| l =~ /OrgName:/ }
115 116 117
    return nil unless orgname

    orgname.split(":", 2)[1].strip
118 119 120 121 122 123 124 125 126
  end

  def curl(url)
    result = popen(%W[curl --insecure --head --connect-timeout 5 --max-time 5 --silent #{url}])
    return nil unless result

    lines = result.lines.map(&:chomp)

    return nil unless lines.size > 1
127

128 129 130 131 132 133 134 135 136 137 138
    version, code, message = lines[0].split(" ")
  
    kls = Net::HTTPResponse::CODE_TO_OBJ[code] || Net::HTTPResponse
    out = kls.new(version, code, message)

    lines[1..-1].map do |line|
      key, value = line.split(": ")
      out.add_field(key, value)
    end

    out
139 140 141
  end
end

142 143 144 145 146 147
class Checks
  class << self
    def header
      ["HOST", "NETWORK", "REV", "HTTPS", "SSH", "REDIRECT"].join("\t")
    end
  end
148

149 150 151 152 153 154 155 156 157 158
  attr_reader :checks

  def initialize(hostnames)
    @checks = hostnames.map { |hostname| Check.new(hostname) }
  end

  def execute
    threads = checks.map { |check| Thread.new { check.execute } }
    threads.map(&:value)
  end
159
end
160 161 162 163

results = Checks.new(ARGV).execute
STDOUT.puts Checks.header
results.each { |result| STDOUT.puts result }