#!/usr/bin/env perl
# bird-bgpath - pretty-print `birdc show route all…` output
# (c) 2016 Mantas Mikulėnas <grawity@gmail.com>
# Released under the MIT License (dist/LICENSE.mit)
use v5.14;
use Data::Dumper;
use Net::DNS;

my %AS = ();

my %COMMUNITY = (
	64511 => {
		1 => "latency[0–2.7]",
		2 => "latency[2.7–7.3]",
		3 => "latency[7.3–20]",
		21 => "bw[0.1–1Mb]",
		31 => "enc[none]",
		32 => "enc[unsafe]",
		33 => "enc[nonPFS]",
		34 => "enc[PFS]",
		# https://lists.nox.tf/pipermail/dn42/2015-December/001259.html
		41 => "region[EU]",
		42 => "region[NAm-E]",
		43 => "region[NAm-C]",
		44 => "region[NAm-W]",
		45 => "region[CAm]",
		46 => "region[SAm-E]",
		47 => "region[SAm-W]",
		48 => "region[Af-N]",
		49 => "region[Af-S]",
		50 => "region[As-S]",
		51 => "region[As-SE]",
		52 => "region[As-E]",
		53 => "region[Pa]",
		_ => sub {
			my $n = shift;
			if (4 <= $n && $n <= 9) {
				sprintf("latency[%d-%d]", exp($n-1), exp($n));
			} elsif (20 <= $n && $n <= 29) {
				sprintf("bw[%d-%dMb]", 10**($n-22), 10**($n-21));
			} else {
				$n;
			}
		},
	},
	64666 => {
		2000 => "from[DE]",
		3000 => "from[AT]",
		4001 => "via[transit]",
		4002 => "via[downstream]",
		4003 => "via[local-peering]",
		4004 => "via[dn42-IX]",
		6000 => "export[none]",
		6001 => "export[no-PoP]",
		6002 => "export[no-peers]",
	},
	65535 => {
		666 => "blackhole",
		65281 => "no-export",
		65282 => "no-advertise",
		65283 => "no-export-subconfed",
	},
);

sub is_private_as {
	my ($as) = @_;
	return 1 if ($as >= 64512) && ($as <= 65535);
	return 1 if ($as >= 4200000000) && ($as <= 4294967294);
	return 0;
}

sub dns_asname_cymru {
	my ($as) = @_;
	my $r = `dig +short AS$as.asn.cymru.com txt`;
	$r =~ s/^"$as \| \S+ \| \S+ \| \S+ \| (.+)"/$1/ || return $as;
	$r =~ s/\n.*//s;
	#$r =~ s/,.*?$//;
	$r =~ s/^(\S{4,}).*, (\S+)$/$1, $2/;
	return $r;
}

sub dns_asname_dn42 {
	my ($as) = @_;
	my $r = `dig +short AS$as.asn.dn42 txt \@172.23.0.53`;
	$r =~ s/^"$as \| \S* \| \S* \| \S* \| (.+)"/$1/ || return $as;
	$r =~ s/\n.*//s;
	#$r =~ s/,.*?$//;
	$r =~ s/^(\S{4,}).*, (\S+)$/$1, $2/;
	$r =~ s!\s*https?://\S+!!;
	return $r;
}

sub asname {
	my ($as, $fmt) = @_;
	$fmt //= "%s";
	if (is_private_as($as)) {
		my $tail = ($as =~ s/^424242/./r);
		if ($AS{$as}) {
			return $AS{$as}." (".$tail.")";
		} else {
			return dns_asname_dn42($as)." (".$tail.")";
		}
	} else {
		if ($AS{$as}) {
			return $AS{$as}." (".$as.")";
		} else {
			return dns_asname_cymru($as)." (".$as.")";
		}
	}
}

sub tagname {
	my ($tag) = @_;
	if ($tag =~ /^\((\d+),(\d+)\)$/ || $tag =~ /^\(r[ot], (\d+), (\d+)\)$/) {
		my $as = int $1;
		my $cm = int $2;
		my $name;
		if ($COMMUNITY{$as}{$cm}){ $name = $COMMUNITY{$as}{$cm}; }
		elsif ($COMMUNITY{$as}{_}) { $name = $COMMUNITY{$as}{_}->($cm); }
		if ($AS{$as} || $name) {
			return "(".($AS{$as} // $as).",".($name || $cm).")";
		}
	}
	elsif ($tag =~ /^\((\d+), (\d+), (\d+)\)$/) {
		my $as = int $1;
		my $cm1 = int $2;
		my $cm2 = int $3;
		my $name1 = $COMMUNITY{$as}{$cm1}{_name};
		my $name2 = $COMMUNITY{$as}{$cm1}{$cm2};
		if ($AS{$as} || $name1 || $name2) {
			return "(".($AS{$as} // $as).",".($name1 || $cm1).",".($name2 || $cm2).")";
		}
	}
	return $tag;
}

sub filter {
	my $rt;
	my $via;
	my $dev;
	my $asn;
	my $rest;
	my $proto;
	while (<STDIN>) {
		if (m{^(\S*)\s+(unicast) \[(\S+) (\S+)( from \S+)?\] ([I*]+ )?(\([0-9/]+\) .*)}) {
			# unicast [ospf_lan4 16:55:32.740] I (150/75) [10.147.2.37]
			# unicast [henet 2020-04-04] * (100) [AS2847i]
			# unicast [int_sky 2020-04-05 from 2a06:e881:108:1::2] (100/17) [AS2847i]
			#print " · \e[38;5;210m$1\e[m\n" if $1;
			my $prefix = $1;
			my $proto = $3;
			my $date = $4;
			my $status = $6;
			my $rest = $7;
			my $primary = ($status =~ /\*/);
			print "\e[4m$prefix\e[m\n";
			if ($primary) {
				print "\t\e[2mProtocol:\e[m $proto \e[2m[since $date]\e[m \e[1mprimary\e[m $status$rest\e[m\n";
			} else {
				print "\t\e[2mProtocol:\e[m $proto \e[2m[since $date]\e[m $status$rest\e[m\n";
			}
		}
		elsif (/^\s+(via \S+ on \S+)$/) { 
			print "\t\e[94mNexthop:\e[m $1\n";
		}
		elsif (s/^(\S+\/\d+)?\s+(via|dev) /$2 /) {
			$rt = $1 if $1;
			if (/^via (\S+) on (\S+) (.+) \[AS(\d+)(\w*?)\??\]$/) {
				$via = $1; $dev = $2; $rest = $3; $asn = $4;
				print "\t",
					"via \e[38;5;43m".$via."\e[m ",
					"on \e[38;5;45m".$dev."\e[m ",
					"at \e[38;5;46m".$asn."\e[m ",
					$rest."\n";
			}
			elsif (/^via (\S+) on (\S+) (\[.+\] .+)$/) {
				$via = $1; $dev = $2; $rest = $3;
				print "\t",
					"via \e[38;5;43m".$via."\e[m ",
					"on \e[38;5;45m".$dev."\e[m ",
					$rest."\n";
			}
			elsif (/^dev (\S+) (\[(\S+) \S+\] .+)$/) {
				$dev = $1; $rest = $2; $proto = $3;
				print "\t",
					"local ",
					"on \e[38;5;45m".$dev."\e[m ",
					$rest."\n";
			}
			else {
				print "\t\e[33m>> ", $_, "\e[m";
			}
		}
		elsif (/(\s+Type:) (.+)$/) {
			# Type: BGP univ
			print if $2 !~ /^(BGP|OSPF) univ$/;
		}
		elsif (/(\s+BGP\.as_path:) (.+)$/) {
			my $prefix = $1;
			my @aspath = split(/ /, $2);
			@aspath = map {asname($_)} @aspath;
			@aspath = map {"\e[1m$_\e[m"} @aspath;
			print $prefix.join(" \e[1;33m›\e[m ", "", @aspath)."\n";
		}
		elsif (/(\s+BGP\.next_hop:) (.+)$/) {
			print $1." \e[38;5;43m".$2."\e[m\n" if $2 ne $via;
		}
		elsif (/^(\s+BGP\.(?:|ext_|large_)community:) (.+)$/) {
			my $prefix = $1;
			my @tags = ($2 =~ /\(.+?\)/g);
			@tags = map {tagname($_)} @tags;
			if (eval {require Sort::Naturally}) {
				@tags = Sort::Naturally::nsort(@tags);
			} else {
				@tags = sort @tags;
			}
			#print $prefix." ".join(" ", @tags)."\n";
		}
		elsif (/^(\s+BGP\.local_pref:) (.+)$/) {
			print if $2 ne "100";
		}
		elsif (/^(\s+BGP\.origin:) (.+)$/) {
			print if $2 ne "IGP";
		}
		else {
			print;
		}
	}
}

my $dir = $ENV{XDG_CONFIG_HOME} // $ENV{HOME}."/.config";
my $path_asnames = "$dir/nullroute.eu.org/dn42-asnames.txt";

if (open(my $f, "<", $path_asnames)) {
	while (<$f>) {
		if (/^(\d+)\s+(.+)$/) {
			$AS{int $1} = $2;
		}
	}
	close($f);
}

filter
