# $Id: Hosts.pm,v 1.10 2008/10/21 15:41:02 turnstep Exp $

package Net::SSH::Perl::Util::Hosts;
use strict;
use warnings;

use Net::SSH::Perl::Constants qw( :hosts );
use Crypt::Misc qw( encode_b64 decode_b64 );
use Crypt::Mac::HMAC qw( hmac );
use Socket;

use Carp qw( croak );

use constant SALT_LEN => 20;

sub _check_host_in_hostfile {
    my($host, $port, $hostfile, $key) = @_;
    my $key_class = ref($key);

    if (defined $port && $port != 22) {
        $host = "[$host]:$port";
    }

    # ssh returns HOST_NEW if the host file can't be opened
    open my $fh, '<', $hostfile or return HOST_NEW;
    local($_, $/);
    $/ = "\n";
    my $status = HOST_NEW;
    HOST: while (<$fh>) {
        chomp;
        my ($hosts, $keyblob) = split /\s+/, $_, 2;
        next unless $keyblob;
        my $fkey;
        ## Trap errors for any potentially unsupported key types
        eval {
            $fkey = $key_class->extract_public($keyblob);
        };
        next if $@;

        my $checkhost = $host;

        ## Check for hashed entries
        if (index($hosts, '|') == 0) {
            if ($hosts !~ /^\|1\|(.+?)\|/) {
                warn qq{Cannot parse line $. of $hostfile\n};
                next;
            }
            my $salt = $1;

            my $rawsalt = decode_b64($salt);
            my $hash = encode_b64(hmac('SHA1',$rawsalt,$host));
            $checkhost = "|1|$salt|$hash";
        }

        for my $h (split /,/, $hosts) {
            if ($h eq $checkhost && $key->ssh_name eq $fkey->ssh_name) {
                $status = $key->equal($fkey) ? HOST_OK : HOST_CHANGED;
                last HOST
            }
        }
    }
    close $fh;
    $status;
}

sub _all_keys_for_host {
    my($host, $port, $hostfile) = @_;
    my $ip;
    if ($host =~ /[a-zA-Z]+/) {
        $ip = inet_ntoa(inet_aton($host));
    }
    if (defined $port && $port != 22) {
        $host = "[$host]:$port";
        $ip = "[$ip]:$port";
    }

    open my $fh, '<', $hostfile or return 0;
    local($_, $/);
    $/ = "\n";
    my @keys;
    while (<$fh>) {
        chomp;
        my ($hosts, $keyblob) = split /\s+/, $_, 2;
        my @hosts_to_check = ($host);
        push @hosts_to_check, $ip if $ip;

        foreach my $checkhost (@hosts_to_check) {
            ## Check for hashed entries
            if (index($hosts, '|') == 0) {
                if ($hosts !~ /^\|1\|(.+?)\|/) {
                    warn qq{Cannot parse line $. of $hostfile\n};
                    next
                }
                my $salt = $1;
    
                my $rawsalt = decode_b64($salt);
                my $hash = encode_b64(hmac('SHA1',$rawsalt,$host));
                $checkhost = "|1|$salt|$hash";
            }
            for my $h (split /,/, $hosts) {
                if ($h eq $checkhost) {
                    my $fkey;
                    eval { $fkey = Net::SSH::Perl::Key->extract_public($keyblob) };
                    push @keys, $fkey if $fkey;
                }
            }
        }
    }
    close $fh;
    return wantarray ? @keys : \@keys
}

sub _add_host_to_hostfile {
    my($host, $port, $hostfile, $key, $hash_flag) = @_;
    unless (-e $hostfile) {
        require File::Basename;
        my $dir = File::Basename::dirname($hostfile);
        unless (-d $dir) {
            require File::Path;
            File::Path::mkpath([ $dir ])
                or die "Can't create directory $dir: $!";
        }
    }

    my $ip;
    if ($host =~ /[a-zA-Z]+/) {
        $ip = inet_ntoa(inet_aton($host));
        $ip = "[$ip]:$port" if $ip && defined $port && $port != 22;
    }
    $host = "[$host]:$port" if defined $port && $port != 22;

    my $data;
    open my $fh, '>>', $hostfile or croak "Can't write to $hostfile: $!";
    if ($hash_flag) {
        use Crypt::PRNG qw( random_bytes );
        my @entries = ($host);
        push @entries, $ip if $ip;
        foreach my $entry (@entries) {
            my $rawsalt = random_bytes(SALT_LEN);
            my $salt = encode_b64($rawsalt);
            my $hash = encode_b64(hmac('SHA1', $rawsalt, $entry));
            $data .= join(' ', "|1|$salt|$hash", $key->dump_public, "\n");
        }
    }
    else {
        $host = "$host,$ip" if $ip;
        $data = join(' ', $host, $key->dump_public, "\n");
    }
    print $fh $data;
    close $fh or croak "Can't close $hostfile: $!";
}

sub _remove_host_from_hostfile {
    my($host, $port, $hostfile, $key) = @_;
    return unless -e $hostfile;

    my $ip;
    if ($host =~ /[a-zA-Z]+/) {
        $ip = inet_ntoa(inet_aton($host));
        $ip = "[$ip]:$port" if $ip && defined $port && $port != 22;
    }
    $host = "[$host]:$port" if defined $port && $port != 22;

    open my $fh, '<', $hostfile or croak "Can't open $hostfile: $!";
    open my $fhw, '>', "$hostfile.new" or croak "Can't open $hostfile.new for writing: $!";

    LINE: while (<$fh>) {
        chomp;
        my ($hosts, $keyblob) = split /\s+/, $_, 2;
        my $fkey;
        ## Trap errors for any potentially unsupported key types
        eval {
            $fkey = Net::SSH::Perl::Key->extract_public($keyblob);
        };
        # keep it if we don't know what it is
        if ($@) {
            print $fhw $_,"\n";
            next LINE;
        }

        my @hosts_to_check = ($host);
        push @hosts_to_check, $ip if $ip;

        foreach my $checkhost (@hosts_to_check) {
            ## Check for hashed entries
            if (index($hosts, '|') == 0) {
                if ($hosts !~ /^\|1\|(.+?)\|/) {
                    warn qq{Cannot parse line $. of $hostfile\n};
                    next;
                }
                my $salt = $1;
    
                my $rawsalt = decode_b64($salt);
                my $hash = encode_b64(hmac('SHA1',$rawsalt,$checkhost));
                $checkhost = "|1|$salt|$hash";
            }

            for my $h (split /,/, $hosts) {
                if ($h eq $checkhost && $key->equal($fkey)) {
                    next LINE;
                }
            }
        }
        print $fhw $_,"\n";
    }
    close $fhw or croak "Can't close $hostfile.new: $!";
    close $fh or croak "Can't close $hostfile: $!";
    rename "$hostfile.new", $hostfile;
}

1;