From 9be511dc94afa097ab278a2561ba659a200b9fa1 Mon Sep 17 00:00:00 2001
From: Alexandre Dulaunoy
Date: Thu, 8 Mar 2012 23:25:21 +0100
Subject: [PATCH] Initial passive DNS server commit at CanSecWest
---
pdns-server/bin/pdns-dnscap2feeder.sh | 6 +
pdns-server/bin/pdns-feeder.pl | 147 +
pdns-server/bin/pdns-ranking.py | 85 +
pdns-server/bin/query.pl | 114 +
pdns-server/doc/RR-Type.txt | 79 +
pdns-server/doc/datastore-format.txt | 97 +
pdns-server/lib/install.sh | 3 +
pdns-server/web/PDNSq.pm | 171 +
pdns-server/web/pdns-web.pl | 214 +
pdns-server/web/pdns-web.sh | 5 +
pdns-server/web/public/static/moo-pdns.js | 17 +
.../static/mootools-core-1.3.1-full-compat.js | 5928 +++++++
.../web/public/static/mootools-core-1.3.1.js | 5928 +++++++
.../public/static/mootools-more-1.3.1.1.js | 13463 ++++++++++++++++
pdns-server/web/public/static/pdns.css | 16 +
15 files changed, 26273 insertions(+)
create mode 100644 pdns-server/bin/pdns-dnscap2feeder.sh
create mode 100644 pdns-server/bin/pdns-feeder.pl
create mode 100644 pdns-server/bin/pdns-ranking.py
create mode 100644 pdns-server/bin/query.pl
create mode 100644 pdns-server/doc/RR-Type.txt
create mode 100644 pdns-server/doc/datastore-format.txt
create mode 100644 pdns-server/lib/install.sh
create mode 100644 pdns-server/web/PDNSq.pm
create mode 100755 pdns-server/web/pdns-web.pl
create mode 100644 pdns-server/web/pdns-web.sh
create mode 100644 pdns-server/web/public/static/moo-pdns.js
create mode 100644 pdns-server/web/public/static/mootools-core-1.3.1-full-compat.js
create mode 100644 pdns-server/web/public/static/mootools-core-1.3.1.js
create mode 100644 pdns-server/web/public/static/mootools-more-1.3.1.1.js
create mode 100644 pdns-server/web/public/static/pdns.css
diff --git a/pdns-server/bin/pdns-dnscap2feeder.sh b/pdns-server/bin/pdns-dnscap2feeder.sh
new file mode 100644
index 0000000..9fb6ddb
--- /dev/null
+++ b/pdns-server/bin/pdns-dnscap2feeder.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+#
+# A simple feeder using dnscap input
+#
+
+./dnscap -T -s r -ieth1 -g 2>&1 >/dev/null | perl pdns-feeder.pl
diff --git a/pdns-server/bin/pdns-feeder.pl b/pdns-server/bin/pdns-feeder.pl
new file mode 100644
index 0000000..b7b5b0d
--- /dev/null
+++ b/pdns-server/bin/pdns-feeder.pl
@@ -0,0 +1,147 @@
+#
+# Minimal and Scalable Passive DNS - Redis feeder
+#
+# Read dnscap output to feed a Redis database
+#
+#
+# Copyright (C) 2010-2012 Alexandre Dulaunoy
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+use strict;
+use warnings;
+
+use Date::Manip;
+use Redis;
+use Scalar::Util;
+use IO::Handle;
+
+$| = 1;
+
+my $block = 0;
+my $date;
+my %counter = (
+ "processed" => 0,
+ "redisrst" => 0
+);
+my @RRType = ( "A", "AAAA", "CNAME", "NS" );
+my %RRTypevalue = (
+ "A" => 1,
+ "AAAA" => 28,
+ "CNAME" => 5,
+ "NS" => 2,
+ "MX" => 15,
+ "SRV" => 33
+);
+
+my $parsedate = new Date::Manip::Date;
+
+my $r = Redis->new( server => '127.0.0.1:6379', encoding => undef );
+
+my $stdin = new IO::Handle;
+$stdin->fdopen( fileno(STDIN), "r" );
+
+while ( defined( $_ = $stdin->getline ) ) {
+
+ if ( $counter{'redisrst'} == 1 ) {
+ $r->quit;
+ undef($r);
+ print "Reset\n";
+ $r = Redis->new( server => '127.0.0.1:6379', encoding => undef );
+ $counter{'redisrst'} = 0;
+ }
+ if (m/^\[/) {
+ $block = 1;
+ {
+ my @s = split;
+ $date = $s[1];
+ }
+ }
+
+ if ( !(m/^\[/) ) {
+ $_ =~ s/\cI//;
+
+ # discarding - [1.2.3.4].53 [149.13.33.69].5234
+ if (m/^\[/) { next; }
+
+ # split - 21 ns10.ovh.net,IN,AAAA,172800,2001:41d0:1:1981::1 \
+ if (m/^\d* /) {
+ $_ =~ s/^\d* //;
+ }
+
+ # discarding - dns QUERY,NOERROR,26278,qr|rd|ra \
+ if (m/^dns /) { next; }
+
+ # decode line
+ ProcessRequest( $date, $_ );
+ $counter{'processed'}++;
+ }
+ if ( ( $counter{'processed'} % 1000 ) == 0 ) {
+ $counter{'redisrst'} = 1;
+ print "Processed:"
+ . $counter{'processed'}
+ . " Redist RST:"
+ . $counter{'redisrst'} . "\n";
+ }
+
+}
+
+sub ProcessRequest {
+ my $epoch = shift;
+ my $line = shift;
+ chomp($line);
+
+ #my $err = $parsedate->parse($date);
+ #my $epoch = $parsedate->printf('%s');
+
+ my @l = split( /,/, $line );
+ if ( $l[2] ~~ @RRType ) {
+ my @rdatas = split( / /, $l[4] );
+
+ #$l[4] =~ s/\s\\.*$//g;
+ #$l[4] =~ s/(.*)\s/$1/g;
+ print $l[0] . " " . $l[2] . " " . "$rdatas[0]\n";
+ my @rdatap = split( / /, $l[4] );
+ RedisUpdate( $l[0], $l[2], $rdatap[0], $epoch );
+ undef(@rdatap);
+ }
+}
+
+sub RedisUpdate {
+ my $name = lc(shift);
+ my $recordtype = shift;
+ my $rdata = lc(shift);
+ my $timestamp = shift;
+
+ my $recordtypevalue = $RRTypevalue{$recordtype};
+ my $pdns_r = "r:" . $name . ":" . $recordtypevalue;
+ my $pdns_v = "v:" . $rdata;
+ my $pdns_s = "s:" . $name . ":" . $rdata;
+ my $pdns_l = "l:" . $name . ":" . $rdata;
+ my $pdns_o = "o:" . $name . ":" . $rdata;
+
+ my $ret = $r->sadd( $pdns_r, $rdata );
+ $ret = $r->sadd( $pdns_v, $name );
+
+ # set first seen value
+ if ( !( $r->exists($pdns_s) ) ) {
+ $ret = $r->set( $pdns_s, $timestamp );
+ }
+
+ # set last seen value
+ $ret = $r->set( $pdns_l, $timestamp );
+
+ # increment the occurence value
+ $ret = $r->incr($pdns_o);
+}
diff --git a/pdns-server/bin/pdns-ranking.py b/pdns-server/bin/pdns-ranking.py
new file mode 100644
index 0000000..5cf13bd
--- /dev/null
+++ b/pdns-server/bin/pdns-ranking.py
@@ -0,0 +1,85 @@
+#
+# Minimal and Scalable Passive DNS - ranking records
+#
+# Copyright (C) 2012 Alexandre Dulaunoy
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# Sample input (pdns-ranking.py) read line with the Passive DNS format:
+# s:truecsi.org:ns3.afraid.org. = 1327737190
+#
+# and output the following:
+# truecsi.org
+# [(1.0003765060241001, 'truecsi.org')]
+# truecsi.org,1.00037650602
+#
+# and update the DB 6 database
+# redis> select 6
+# OK
+# redis> KEYS "*"
+# 1) "truecsi.org"
+# redis> GET "truecsi.org"
+# "1.00037650602"
+# redis>
+
+
+import sys
+import re
+import redis
+
+path = "../lib/DomainClassifier/DomainClassifier/"
+sys.path.append(path)
+
+import domainclassifier
+
+
+def domexist(ldomain = None):
+ print ldomain
+ r = redis.StrictRedis(host='localhost', port=6379, db=6)
+
+ if ldomain is not None:
+ if r.exists(ldomain):
+ return True
+ else:
+ return False
+ else:
+ return False
+
+def domstore(ldomain = None, rank = 1.0):
+ r = redis.StrictRedis(host='localhost', port=6379, db=6)
+ return r.set(ldomain,rank)
+
+for line in sys.stdin:
+ domain = line.split(':')[1].lower()
+ if not (re.search("(spamhaus.org|arpa)$", domain)):
+ if not domexist(ldomain=domain):
+ c = domainclassifier.Extract(domain)
+ c.domain()
+ c.validdomain()
+ r = c.rankdomain()
+ if r:
+ r.sort(reverse=True)
+ ranking = r[0][0]
+ print r
+ if ranking is not None:
+ print domain+","+str(ranking)
+ domstore(ldomain=domain, rank=ranking)
+ else:
+ print domain+","+str(1)
+ domstore(ldomain=domain)
+ else:
+ print domain+","+str(1)
+ domstore(ldomain=domain)
+ else:
+ print domain+" already cached"
diff --git a/pdns-server/bin/query.pl b/pdns-server/bin/query.pl
new file mode 100644
index 0000000..661c49f
--- /dev/null
+++ b/pdns-server/bin/query.pl
@@ -0,0 +1,114 @@
+#
+# Minimal and Scalable Passive DNS - Query tool
+#
+# Copyright (C) 2010-2012 Alexandre Dulaunoy
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+
+use POSIX qw(strftime);
+use Redis;
+use Date::Manip;
+
+# will lookup the following RR types
+my %RRTypevalue = (
+ "A" => 1,
+ "AAAA" => 28,
+ "CNAME" => 5,
+ "NS" => 2
+
+);
+
+my $r = Redis->new( server => '127.0.0.1:6379', encoding => undef );
+
+#sample feeder input
+#1298823157.764221 "SET" "l:www.l.google.com:72.14.204.103" "1298819557"
+#1298823157.764353 "INCR" "o:www.l.google.com:72.14.204.103"
+#1298823157.765310 "SADD" "r:www.l.google.com:1" "72.14.204.104"
+#1298823157.765439 "SADD" "v:72.14.204.104" "www.l.google.com"
+#1298823157.765567 "EXISTS" "s:www.l.google.com:72.14.204.104"
+#1298823157.765696 "SET" "l:www.l.google.com:72.14.204.104" "1298819557"
+#1298823157.765823 "INCR" "o:www.l.google.com:72.14.204.104"
+#1298823157.766776 "SADD" "r:www.l.google.com:1" "72.14.204.147"
+#1298823157.766901 "SADD" "v:72.14.204.147" "www.l.google.com"
+#1298823157.767086 "EXISTS" "s:www.l.google.com:72.14.204.147"
+#1298823157.767207 "SET" "l:www.l.google.com:72.14.204.147" "1298819557"
+#1298823157.767333 "INCR" "o:www.l.google.com:72.14.204.147"
+
+while ( my ( $typename, $typevalue ) = each(%RRTypevalue) ) {
+
+ my @x = Lookup( $ARGV[0], $typevalue );
+
+ if ( defined(@x) ) {
+ foreach $p (@x) {
+ print $p. "("
+ . $typename . ")"
+ . " first seen: "
+ . NiceDate( FirstSeen( $ARGV[0], $p ) )
+ . " last seen: "
+ . NiceDate( LastSeen( $ARGV[0], $p ) )
+ . " Hits: "
+ . OccTuple( $ARGV[0], $p ) . "\n";
+ print " behind: " . join( ",", LookupIP($p) ) . "\n";
+ }
+ }
+
+}
+
+sub NiceDate {
+ my $epochvalue = shift;
+
+ return strftime( "%Y%m%d-%H:%M:%S", localtime($epochvalue) );
+
+}
+
+sub Lookup {
+ my $name = shift;
+ my $type = shift;
+
+ # default A record
+ if ( !( defined($type) ) ) { $type = 1; }
+ my $key = "r:$name:$type";
+ return $r->smembers($key);
+}
+
+sub LookupIP {
+ my $name = shift;
+ my $key = "v:$name";
+ return $r->smembers($key);
+}
+
+sub FirstSeen {
+ my $name = shift;
+ my $rdata = shift;
+
+ my $key = "s:$name:$rdata";
+ return $r->get($key);
+}
+
+sub LastSeen {
+ my $name = shift;
+ my $rdata = shift;
+
+ my $key = "l:$name:$rdata";
+ return $r->get($key);
+}
+
+sub OccTuple {
+ my $name = shift;
+ my $rdata = shift;
+
+ my $key = "o:$name:$rdata";
+ return $r->get($key);
+}
diff --git a/pdns-server/doc/RR-Type.txt b/pdns-server/doc/RR-Type.txt
new file mode 100644
index 0000000..f04600a
--- /dev/null
+++ b/pdns-server/doc/RR-Type.txt
@@ -0,0 +1,79 @@
+A 1 a host address [RFC1035]
+NS 2 an authoritative name server [RFC1035]
+MD 3 a mail destination (Obsolete - use MX) [RFC1035]
+MF 4 a mail forwarder (Obsolete - use MX) [RFC1035]
+CNAME 5 the canonical name for an alias [RFC1035]
+SOA 6 marks the start of a zone of authority [RFC1035]
+MB 7 a mailbox domain name (EXPERIMENTAL) [RFC1035]
+MG 8 a mail group member (EXPERIMENTAL) [RFC1035]
+MR 9 a mail rename domain name (EXPERIMENTAL) [RFC1035]
+NULL 10 a null RR (EXPERIMENTAL) [RFC1035]
+WKS 11 a well known service description [RFC1035]
+PTR 12 a domain name pointer [RFC1035]
+HINFO 13 host information [RFC1035]
+MINFO 14 mailbox or mail list information [RFC1035]
+MX 15 mail exchange [RFC1035]
+TXT 16 text strings [RFC1035]
+RP 17 for Responsible Person [RFC1183]
+AFSDB 18 for AFS Data Base location [RFC1183][RFC5864]
+X25 19 for X.25 PSDN address [RFC1183]
+ISDN 20 for ISDN address [RFC1183]
+RT 21 for Route Through [RFC1183]
+NSAP 22 for NSAP address, NSAP style A record [RFC1706]
+NSAP-PTR 23 for domain name pointer, NSAP style [RFC1348]
+SIG 24 for security signature [RFC4034][RFC3755][RFC2535]
+KEY 25 for security key [RFC4034][RFC3755][RFC2535]
+PX 26 X.400 mail mapping information [RFC2163]
+GPOS 27 Geographical Position [RFC1712]
+AAAA 28 IP6 Address [RFC3596]
+LOC 29 Location Information [RFC1876]
+NXT 30 Next Domain - OBSOLETE [RFC3755][RFC2535]
+EID 31 Endpoint Identifier [Patton]
+NIMLOC 32 Nimrod Locator [Patton]
+SRV 33 Server Selection [RFC2782]
+ATMA 34 ATM Address [ATMDOC]
+NAPTR 35 Naming Authority Pointer [RFC2915][RFC2168][RFC3403]
+KX 36 Key Exchanger [RFC2230]
+CERT 37 CERT [RFC4398]
+A6 38 A6 (Experimental) [RFC3226][RFC2874]
+DNAME 39 DNAME [RFC2672]
+SINK 40 SINK [Eastlake]
+OPT 41 OPT [RFC2671]
+APL 42 APL [RFC3123]
+DS 43 Delegation Signer [RFC4034][RFC3658]
+SSHFP 44 SSH Key Fingerprint [RFC4255]
+IPSECKEY 45 IPSECKEY [RFC4025]
+RRSIG 46 RRSIG [RFC4034][RFC3755]
+NSEC 47 NSEC [RFC4034][RFC3755]
+DNSKEY 48 DNSKEY [RFC4034][RFC3755]
+DHCID 49 DHCID [RFC4701]
+NSEC3 50 NSEC3 [RFC5155]
+NSEC3PARAM 51 NSEC3PARAM [RFC5155]
+Unassigned 52-54
+HIP 55 Host Identity Protocol [RFC5205]
+NINFO 56 NINFO [Reid]
+RKEY 57 RKEY [Reid]
+TALINK 58 Trust Anchor LINK [Wijngaards]
+Unassigned 59-98
+SPF 99 [RFC4408]
+UINFO 100 [IANA-Reserved]
+UID 101 [IANA-Reserved]
+GID 102 [IANA-Reserved]
+UNSPEC 103 [IANA-Reserved]
+Unassigned 104-248
+TKEY 249 Transaction Key [RFC2930]
+TSIG 250 Transaction Signature [RFC2845]
+IXFR 251 incremental transfer [RFC1995]
+AXFR 252 transfer of an entire zone [RFC1035][RFC5936]
+MAILB 253 mailbox-related RRs (MB, MG or MR) [RFC1035]
+MAILA 254 mail agent RRs (Obsolete - see MX) [RFC1035]
+* 255 A request for all records [RFC1035]
+URI 256 URI [Faltstrom]
+Unassigned 257-32767
+TA 32768 DNSSEC Trust Authorities [Weiler] 2005-12-13
+DLV 32769 DNSSEC Lookaside Validation [RFC4431]
+Unassigned 32770-65279
+Private use 65280-65534
+Reserved 65535
+
+
diff --git a/pdns-server/doc/datastore-format.txt b/pdns-server/doc/datastore-format.txt
new file mode 100644
index 0000000..98d1f54
--- /dev/null
+++ b/pdns-server/doc/datastore-format.txt
@@ -0,0 +1,97 @@
+This is a description of the various data-structure used
+in the Passive DNS toolkit.
+
+= Redis Database number =
+
+ - (0) default passive dns
+ - (6) ranking of hostname
+
+= format for redis db 0 =
+
+KEY, VALUE
+
+s:
+l:
+o:
+
+KEY, {SET}
+
+r:: {}
+v: {}
+
+# "SET" "l:www.l.google.com:72.14.204.103" "1298819557"
+#1298823157.764353 "INCR" "o:www.l.google.com:72.14.204.103"
+#1298823157.765310 "SADD" "r:www.l.google.com:1" "72.14.204.104"
+#1298823157.765439 "SADD" "v:72.14.204.104" "www.l.google.com"
+#1298823157.765567 "EXISTS" "s:www.l.google.com:72.14.204.104"
+#1298823157.765696 "SET" "l:www.l.google.com:72.14.204.104" "1298819557"
+#1298823157.765823 "INCR" "o:www.l.google.com:72.14.204.104"
+#1298823157.766776 "SADD" "r:www.l.google.com:1" "72.14.204.147"
+#1298823157.766901 "SADD" "v:72.14.204.147" "www.l.google.com"
+#1298823157.767086 "EXISTS" "s:www.l.google.com:72.14.204.147"
+#1298823157.767207 "SET" "l:www.l.google.com:72.14.204.147" "1298819557"
+#1298823157.767333 "INCR" "o:www.l.google.com:72.14.204.147"
+
+
+== sample db 0 ==
+
+Here is the classical update process for a feeder:
+
+Set or update the last-seen.
+
+#1298823157.764221 "SET" "l:www.l.google.com:72.14.204.103" "1298819557"
+
+Update the occurence of the answer seen.
+
+#1298823157.764353 "INCR" "o:www.l.google.com:72.14.204.103"
+
+Add to a set "record:rr-type" the seen answer.
+
+#1298823157.765310 "SADD" "r:www.l.google.com:1" "72.14.204.104"
+
+Add the reverse for fast lookup.
+
+#1298823157.765439 "SADD" "v:72.14.204.104" "www.l.google.com"
+
+If there is no existing first-seen for the tuple,
+
+#1298823157.765567 "EXISTS" "s:www.l.google.com:72.14.204.104"
+
+A first-seen is set:
+
+#1298823157.765696 "SET" "l:www.l.google.com:72.14.204.104" "1298819557"
+
+and so on...
+
+#1298823157.765823 "INCR" "o:www.l.google.com:72.14.204.104"
+#1298823157.766776 "SADD" "r:www.l.google.com:1" "72.14.204.147"
+#1298823157.766901 "SADD" "v:72.14.204.147" "www.l.google.com"
+#1298823157.767086 "EXISTS" "s:www.l.google.com:72.14.204.147"
+#1298823157.767207 "SET" "l:www.l.google.com:72.14.204.147" "1298819557"
+#1298823157.767333 "INCR" "o:www.l.google.com:72.14.204.147"
+
+= format for redis db 6 =
+
+This db index hold a ranking value per hostname.
+
+KEY, VALUE
+
+hostname, float double precision
+
+This can be seen as a cache when an expiration can be set
+by key to limit the lifetime of a ranking. You can create
+a persistent ranking for setting up some "golden" domains.
+
+== sample db 6 ==
+
+1) "truecsi.org"
+redis> GET "truecsi.org"
+"1.00037650602"
+redis>
+
+= contact =
+
+Update, extension, or new data-structure more than welcome.
+
+https://www.github.com/adulau/
+
diff --git a/pdns-server/lib/install.sh b/pdns-server/lib/install.sh
new file mode 100644
index 0000000..e0829da
--- /dev/null
+++ b/pdns-server/lib/install.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+git clone git://github.com/adulau/DomainClassifier.git
diff --git a/pdns-server/web/PDNSq.pm b/pdns-server/web/PDNSq.pm
new file mode 100644
index 0000000..9aad7ba
--- /dev/null
+++ b/pdns-server/web/PDNSq.pm
@@ -0,0 +1,171 @@
+#
+# Minimal and Scalable Passive DNS - Query tool
+#
+# Copyright (C) 2010-2012 Alexandre Dulaunoy
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+package PDNS::Query;
+
+use base 'Exporter';
+
+use POSIX qw(strftime);
+use Redis;
+use Date::Manip;
+use Time::Format;
+
+my %RRTypevalue = (
+ "A" => 1,
+ "NS" => 2,
+ "AAAA" => 28,
+ "CNAME" => 5
+);
+
+my $r = Redis->new( server => '127.0.0.1:6379', encoding => undef );
+
+my $burl = "/l/";
+
+sub Query {
+
+ my $query = shift;
+ my $ret = "";
+ $ret .= "[+-] ";
+ $ret .= "";
+
+ while ( my ( $typename, $typevalue ) = each(%RRTypevalue) ) {
+
+ my @x = Lookup( $query, $typevalue );
+ if ( defined(@x) ) {
+ foreach $p (@x) {
+ if ( $typename =~ m/CNAME/ ) {
+ $ret .= MakeLink($p) . "(" . $typename . ")";
+ }
+ else {
+ $ret .= $p . "(" . $typename . ")";
+ }
+ $ret .=
+ " first seen: "
+ . NiceDate( FirstSeen( $query, $p ) )
+ . " last seen: "
+ . NiceDate( LastSeen( $query, $p ) )
+ . " Hits: "
+ . OccTuple( $query, $p ) . "\n";
+
+ #$ret .= " behind: " . join( "" , LookupIP($p) ) . "\n";
+ $ret .= " ";
+ foreach $n ( LookupIP($p) ) {
+ $ret .= MakeLink($n);
+ }
+ $ret .= " ";
+ }
+ }
+
+ }
+ $ret .= "
";
+ return $ret;
+}
+
+sub QueryJSON {
+ my $query = shift;
+ my $ret = <$value ";
+ return $ret;
+}
+
+sub NiceDate {
+ my $epochvalue = shift;
+
+ return strftime( "%Y%m%d-%H:%M:%S", localtime($epochvalue) );
+
+}
+
+sub NiceDateISO {
+ my $epochvalue = shift;
+ my $format = "yyyy-mm{on}-ddThh:mm:ss+00:00";
+ return time_format( $format, $epochvalue );
+}
+
+sub Lookup {
+ my $name = shift;
+ my $type = shift;
+
+ # default A record
+ if ( !( defined($type) ) ) { $type = 1; }
+ my $key = "r:$name:$type";
+ return $r->smembers($key);
+}
+
+sub LookupIP {
+ my $name = shift;
+ my $key = "v:$name";
+ return $r->smembers($key);
+}
+
+sub FirstSeen {
+ my $name = shift;
+ my $rdata = shift;
+
+ my $key = "s:$name:$rdata";
+ return $r->get($key);
+}
+
+sub LastSeen {
+ my $name = shift;
+ my $rdata = shift;
+
+ my $key = "l:$name:$rdata";
+ return $r->get($key);
+}
+
+sub OccTuple {
+ my $name = shift;
+ my $rdata = shift;
+
+ my $key = "o:$name:$rdata";
+ return -1;
+
+ #return $r->get($key);
+}
+
+1;
diff --git a/pdns-server/web/pdns-web.pl b/pdns-server/web/pdns-web.pl
new file mode 100755
index 0000000..bb09711
--- /dev/null
+++ b/pdns-server/web/pdns-web.pl
@@ -0,0 +1,214 @@
+#!/bin/env perl
+#
+# Minimal and Scalable Passive DNS - Web interface
+#
+# Copyright (C) 2010-2012 Alexandre Dulaunoy
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+
+use Mojolicious::Lite;
+use Mojolicious::Static;
+use PDNSq;
+
+# Documentation browser under "/perldoc" (this plugin requires Perl 5.10)
+plugin 'pod_renderer';
+
+get '/l/(.query)' => sub {
+ my $self = shift;
+ my $q = $self->param('query');
+ my $ret = PDNS::Query::Query($q);
+ my $json = PDNS::Query::QueryJSON($q);
+ $self->render(template => 'index', q => $ret, query => $q, jq => $json);
+};
+
+post '/l' => sub {
+ my $self = shift;
+ my $q = $self->param('q');
+ my $ret = PDNS::Query::Query($q);
+ my $json = PDNS::Query::QueryJSON($q);
+ $self->render(template => 'index', q => $ret, query => $q, jq => $json);
+};
+
+get '/js/(.query)' => sub {
+ my $self = shift;
+ my $q = $self->param('query');
+ $self->render_static('static/'.$q);
+};
+
+get '/__history__.html' => sub {
+ my $self = shift;
+ $self->render_static('static/__history__.html');
+};
+
+get '/' => sub {
+ my $self = shift;
+ $self->render(template => 'main');
+};
+
+
+app->secret('You have to change it');
+app->start;
+__DATA__
+
+@@ main.html.ep
+% layout 'default';
+% title 'Passive DNS';
+
Passive DNS interface
+
+
+@@ index.html.ep
+% layout 'default';
+% title 'PDNS Lookup';
+Passive DNS lookup result for <%= $query %>
+(go to visual timeline)
+<%== $q %>
+Visual timeline
+
+
+
+
+This page uses Javascript to show you a Timeline. Please enable Javascript in your browser to see the full page. Thank you.
+
+
+
+@@ layouts/default.html.ep
+
+ <%= title %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <%= content =%>
+
+
diff --git a/pdns-server/web/pdns-web.sh b/pdns-server/web/pdns-web.sh
new file mode 100644
index 0000000..cfc9867
--- /dev/null
+++ b/pdns-server/web/pdns-web.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+#
+# Will listen on port 3000
+perl pdns-web.pl daemon --reload
+
diff --git a/pdns-server/web/public/static/moo-pdns.js b/pdns-server/web/public/static/moo-pdns.js
new file mode 100644
index 0000000..0caa5a4
--- /dev/null
+++ b/pdns-server/web/public/static/moo-pdns.js
@@ -0,0 +1,17 @@
+window.addEvent('domready', function() {
+
+ var status = {
+ 'true': 'open',
+ 'false': 'close'
+ };
+
+ // -- vertical
+
+ var myVerticalSlide = new Fx.Slide('vertical_slide');
+
+ document.id('v_toggle').addEvent('click', function(event){
+ event.stop();
+ myVerticalSlide.toggle();
+ });
+
+});
diff --git a/pdns-server/web/public/static/mootools-core-1.3.1-full-compat.js b/pdns-server/web/public/static/mootools-core-1.3.1-full-compat.js
new file mode 100644
index 0000000..348f3b7
--- /dev/null
+++ b/pdns-server/web/public/static/mootools-core-1.3.1-full-compat.js
@@ -0,0 +1,5928 @@
+/*
+---
+MooTools: the javascript framework
+
+web build:
+ - http://mootools.net/core/7c56cfef9dddcf170a5d68e3fb61cfd7
+
+packager build:
+ - packager build Core/Core Core/Array Core/String Core/Number Core/Function Core/Object Core/Event Core/Browser Core/Class Core/Class.Extras Core/Slick.Parser Core/Slick.Finder Core/Element Core/Element.Style Core/Element.Event Core/Element.Dimensions Core/Fx Core/Fx.CSS Core/Fx.Tween Core/Fx.Morph Core/Fx.Transitions Core/Request Core/Request.HTML Core/Request.JSON Core/Cookie Core/JSON Core/DOMReady Core/Swiff
+
+/*
+---
+
+name: Core
+
+description: The heart of MooTools.
+
+license: MIT-style license.
+
+copyright: Copyright (c) 2006-2010 [Valerio Proietti](http://mad4milk.net/).
+
+authors: The MooTools production team (http://mootools.net/developers/)
+
+inspiration:
+ - Class implementation inspired by [Base.js](http://dean.edwards.name/weblog/2006/03/base/) Copyright (c) 2006 Dean Edwards, [GNU Lesser General Public License](http://opensource.org/licenses/lgpl-license.php)
+ - Some functionality inspired by [Prototype.js](http://prototypejs.org) Copyright (c) 2005-2007 Sam Stephenson, [MIT License](http://opensource.org/licenses/mit-license.php)
+
+provides: [Core, MooTools, Type, typeOf, instanceOf, Native]
+
+...
+*/
+
+(function(){
+
+this.MooTools = {
+ version: '1.3.1',
+ build: 'af48c8d589f43f32212f9bb8ff68a127e6a3ba6c'
+};
+
+// typeOf, instanceOf
+
+var typeOf = this.typeOf = function(item){
+ if (item == null) return 'null';
+ if (item.$family) return item.$family();
+
+ if (item.nodeName){
+ if (item.nodeType == 1) return 'element';
+ if (item.nodeType == 3) return (/\S/).test(item.nodeValue) ? 'textnode' : 'whitespace';
+ } else if (typeof item.length == 'number'){
+ if (item.callee) return 'arguments';
+ if ('item' in item) return 'collection';
+ }
+
+ return typeof item;
+};
+
+var instanceOf = this.instanceOf = function(item, object){
+ if (item == null) return false;
+ var constructor = item.$constructor || item.constructor;
+ while (constructor){
+ if (constructor === object) return true;
+ constructor = constructor.parent;
+ }
+ return item instanceof object;
+};
+
+// Function overloading
+
+var Function = this.Function;
+
+var enumerables = true;
+for (var i in {toString: 1}) enumerables = null;
+if (enumerables) enumerables = ['hasOwnProperty', 'valueOf', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString', 'toString', 'constructor'];
+
+Function.prototype.overloadSetter = function(usePlural){
+ var self = this;
+ return function(a, b){
+ if (a == null) return this;
+ if (usePlural || typeof a != 'string'){
+ for (var k in a) self.call(this, k, a[k]);
+ if (enumerables) for (var i = enumerables.length; i--;){
+ k = enumerables[i];
+ if (a.hasOwnProperty(k)) self.call(this, k, a[k]);
+ }
+ } else {
+ self.call(this, a, b);
+ }
+ return this;
+ };
+};
+
+Function.prototype.overloadGetter = function(usePlural){
+ var self = this;
+ return function(a){
+ var args, result;
+ if (usePlural || typeof a != 'string') args = a;
+ else if (arguments.length > 1) args = arguments;
+ if (args){
+ result = {};
+ for (var i = 0; i < args.length; i++) result[args[i]] = self.call(this, args[i]);
+ } else {
+ result = self.call(this, a);
+ }
+ return result;
+ };
+};
+
+Function.prototype.extend = function(key, value){
+ this[key] = value;
+}.overloadSetter();
+
+Function.prototype.implement = function(key, value){
+ this.prototype[key] = value;
+}.overloadSetter();
+
+// From
+
+var slice = Array.prototype.slice;
+
+Function.from = function(item){
+ return (typeOf(item) == 'function') ? item : function(){
+ return item;
+ };
+};
+
+Array.from = function(item){
+ if (item == null) return [];
+ return (Type.isEnumerable(item) && typeof item != 'string') ? (typeOf(item) == 'array') ? item : slice.call(item) : [item];
+};
+
+Number.from = function(item){
+ var number = parseFloat(item);
+ return isFinite(number) ? number : null;
+};
+
+String.from = function(item){
+ return item + '';
+};
+
+// hide, protect
+
+Function.implement({
+
+ hide: function(){
+ this.$hidden = true;
+ return this;
+ },
+
+ protect: function(){
+ this.$protected = true;
+ return this;
+ }
+
+});
+
+// Type
+
+var Type = this.Type = function(name, object){
+ if (name){
+ var lower = name.toLowerCase();
+ var typeCheck = function(item){
+ return (typeOf(item) == lower);
+ };
+
+ Type['is' + name] = typeCheck;
+ if (object != null){
+ object.prototype.$family = (function(){
+ return lower;
+ }).hide();
+ //<1.2compat>
+ object.type = typeCheck;
+ //1.2compat>
+ }
+ }
+
+ if (object == null) return null;
+
+ object.extend(this);
+ object.$constructor = Type;
+ object.prototype.$constructor = object;
+
+ return object;
+};
+
+var toString = Object.prototype.toString;
+
+Type.isEnumerable = function(item){
+ return (item != null && typeof item.length == 'number' && toString.call(item) != '[object Function]' );
+};
+
+var hooks = {};
+
+var hooksOf = function(object){
+ var type = typeOf(object.prototype);
+ return hooks[type] || (hooks[type] = []);
+};
+
+var implement = function(name, method){
+ if (method && method.$hidden) return;
+
+ var hooks = hooksOf(this);
+
+ for (var i = 0; i < hooks.length; i++){
+ var hook = hooks[i];
+ if (typeOf(hook) == 'type') implement.call(hook, name, method);
+ else hook.call(this, name, method);
+ }
+
+ var previous = this.prototype[name];
+ if (previous == null || !previous.$protected) this.prototype[name] = method;
+
+ if (this[name] == null && typeOf(method) == 'function') extend.call(this, name, function(item){
+ return method.apply(item, slice.call(arguments, 1));
+ });
+};
+
+var extend = function(name, method){
+ if (method && method.$hidden) return;
+ var previous = this[name];
+ if (previous == null || !previous.$protected) this[name] = method;
+};
+
+Type.implement({
+
+ implement: implement.overloadSetter(),
+
+ extend: extend.overloadSetter(),
+
+ alias: function(name, existing){
+ implement.call(this, name, this.prototype[existing]);
+ }.overloadSetter(),
+
+ mirror: function(hook){
+ hooksOf(this).push(hook);
+ return this;
+ }
+
+});
+
+new Type('Type', Type);
+
+// Default Types
+
+var force = function(name, object, methods){
+ var isType = (object != Object),
+ prototype = object.prototype;
+
+ if (isType) object = new Type(name, object);
+
+ for (var i = 0, l = methods.length; i < l; i++){
+ var key = methods[i],
+ generic = object[key],
+ proto = prototype[key];
+
+ if (generic) generic.protect();
+
+ if (isType && proto){
+ delete prototype[key];
+ prototype[key] = proto.protect();
+ }
+ }
+
+ if (isType) object.implement(prototype);
+
+ return force;
+};
+
+force('String', String, [
+ 'charAt', 'charCodeAt', 'concat', 'indexOf', 'lastIndexOf', 'match', 'quote', 'replace', 'search',
+ 'slice', 'split', 'substr', 'substring', 'toLowerCase', 'toUpperCase'
+])('Array', Array, [
+ 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift', 'concat', 'join', 'slice',
+ 'indexOf', 'lastIndexOf', 'filter', 'forEach', 'every', 'map', 'some', 'reduce', 'reduceRight'
+])('Number', Number, [
+ 'toExponential', 'toFixed', 'toLocaleString', 'toPrecision'
+])('Function', Function, [
+ 'apply', 'call', 'bind'
+])('RegExp', RegExp, [
+ 'exec', 'test'
+])('Object', Object, [
+ 'create', 'defineProperty', 'defineProperties', 'keys',
+ 'getPrototypeOf', 'getOwnPropertyDescriptor', 'getOwnPropertyNames',
+ 'preventExtensions', 'isExtensible', 'seal', 'isSealed', 'freeze', 'isFrozen'
+])('Date', Date, ['now']);
+
+Object.extend = extend.overloadSetter();
+
+Date.extend('now', function(){
+ return +(new Date);
+});
+
+new Type('Boolean', Boolean);
+
+// fixes NaN returning as Number
+
+Number.prototype.$family = function(){
+ return isFinite(this) ? 'number' : 'null';
+}.hide();
+
+// Number.random
+
+Number.extend('random', function(min, max){
+ return Math.floor(Math.random() * (max - min + 1) + min);
+});
+
+// forEach, each
+
+var hasOwnProperty = Object.prototype.hasOwnProperty;
+Object.extend('forEach', function(object, fn, bind){
+ for (var key in object){
+ if (hasOwnProperty.call(object, key)) fn.call(bind, object[key], key, object);
+ }
+});
+
+Object.each = Object.forEach;
+
+Array.implement({
+
+ forEach: function(fn, bind){
+ for (var i = 0, l = this.length; i < l; i++){
+ if (i in this) fn.call(bind, this[i], i, this);
+ }
+ },
+
+ each: function(fn, bind){
+ Array.forEach(this, fn, bind);
+ return this;
+ }
+
+});
+
+// Array & Object cloning, Object merging and appending
+
+var cloneOf = function(item){
+ switch (typeOf(item)){
+ case 'array': return item.clone();
+ case 'object': return Object.clone(item);
+ default: return item;
+ }
+};
+
+Array.implement('clone', function(){
+ var i = this.length, clone = new Array(i);
+ while (i--) clone[i] = cloneOf(this[i]);
+ return clone;
+});
+
+var mergeOne = function(source, key, current){
+ switch (typeOf(current)){
+ case 'object':
+ if (typeOf(source[key]) == 'object') Object.merge(source[key], current);
+ else source[key] = Object.clone(current);
+ break;
+ case 'array': source[key] = current.clone(); break;
+ default: source[key] = current;
+ }
+ return source;
+};
+
+Object.extend({
+
+ merge: function(source, k, v){
+ if (typeOf(k) == 'string') return mergeOne(source, k, v);
+ for (var i = 1, l = arguments.length; i < l; i++){
+ var object = arguments[i];
+ for (var key in object) mergeOne(source, key, object[key]);
+ }
+ return source;
+ },
+
+ clone: function(object){
+ var clone = {};
+ for (var key in object) clone[key] = cloneOf(object[key]);
+ return clone;
+ },
+
+ append: function(original){
+ for (var i = 1, l = arguments.length; i < l; i++){
+ var extended = arguments[i] || {};
+ for (var key in extended) original[key] = extended[key];
+ }
+ return original;
+ }
+
+});
+
+// Object-less types
+
+['Object', 'WhiteSpace', 'TextNode', 'Collection', 'Arguments'].each(function(name){
+ new Type(name);
+});
+
+// Unique ID
+
+var UID = Date.now();
+
+String.extend('uniqueID', function(){
+ return (UID++).toString(36);
+});
+
+//<1.2compat>
+
+var Hash = this.Hash = new Type('Hash', function(object){
+ if (typeOf(object) == 'hash') object = Object.clone(object.getClean());
+ for (var key in object) this[key] = object[key];
+ return this;
+});
+
+Hash.implement({
+
+ forEach: function(fn, bind){
+ Object.forEach(this, fn, bind);
+ },
+
+ getClean: function(){
+ var clean = {};
+ for (var key in this){
+ if (this.hasOwnProperty(key)) clean[key] = this[key];
+ }
+ return clean;
+ },
+
+ getLength: function(){
+ var length = 0;
+ for (var key in this){
+ if (this.hasOwnProperty(key)) length++;
+ }
+ return length;
+ }
+
+});
+
+Hash.alias('each', 'forEach');
+
+Object.type = Type.isObject;
+
+var Native = this.Native = function(properties){
+ return new Type(properties.name, properties.initialize);
+};
+
+Native.type = Type.type;
+
+Native.implement = function(objects, methods){
+ for (var i = 0; i < objects.length; i++) objects[i].implement(methods);
+ return Native;
+};
+
+var arrayType = Array.type;
+Array.type = function(item){
+ return instanceOf(item, Array) || arrayType(item);
+};
+
+this.$A = function(item){
+ return Array.from(item).slice();
+};
+
+this.$arguments = function(i){
+ return function(){
+ return arguments[i];
+ };
+};
+
+this.$chk = function(obj){
+ return !!(obj || obj === 0);
+};
+
+this.$clear = function(timer){
+ clearTimeout(timer);
+ clearInterval(timer);
+ return null;
+};
+
+this.$defined = function(obj){
+ return (obj != null);
+};
+
+this.$each = function(iterable, fn, bind){
+ var type = typeOf(iterable);
+ ((type == 'arguments' || type == 'collection' || type == 'array' || type == 'elements') ? Array : Object).each(iterable, fn, bind);
+};
+
+this.$empty = function(){};
+
+this.$extend = function(original, extended){
+ return Object.append(original, extended);
+};
+
+this.$H = function(object){
+ return new Hash(object);
+};
+
+this.$merge = function(){
+ var args = Array.slice(arguments);
+ args.unshift({});
+ return Object.merge.apply(null, args);
+};
+
+this.$lambda = Function.from;
+this.$mixin = Object.merge;
+this.$random = Number.random;
+this.$splat = Array.from;
+this.$time = Date.now;
+
+this.$type = function(object){
+ var type = typeOf(object);
+ if (type == 'elements') return 'array';
+ return (type == 'null') ? false : type;
+};
+
+this.$unlink = function(object){
+ switch (typeOf(object)){
+ case 'object': return Object.clone(object);
+ case 'array': return Array.clone(object);
+ case 'hash': return new Hash(object);
+ default: return object;
+ }
+};
+
+//1.2compat>
+
+}).call(this);
+
+
+/*
+---
+
+name: Array
+
+description: Contains Array Prototypes like each, contains, and erase.
+
+license: MIT-style license.
+
+requires: Type
+
+provides: Array
+
+...
+*/
+
+Array.implement({
+
+ invoke: function(methodName){
+ var args = Array.slice(arguments, 1);
+ return this.map(function(item){
+ return item[methodName].apply(item, args);
+ });
+ },
+
+ every: function(fn, bind){
+ for (var i = 0, l = this.length; i < l; i++){
+ if ((i in this) && !fn.call(bind, this[i], i, this)) return false;
+ }
+ return true;
+ },
+
+ filter: function(fn, bind){
+ var results = [];
+ for (var i = 0, l = this.length; i < l; i++){
+ if ((i in this) && fn.call(bind, this[i], i, this)) results.push(this[i]);
+ }
+ return results;
+ },
+
+ clean: function(){
+ return this.filter(function(item){
+ return item != null;
+ });
+ },
+
+ indexOf: function(item, from){
+ var len = this.length;
+ for (var i = (from < 0) ? Math.max(0, len + from) : from || 0; i < len; i++){
+ if (this[i] === item) return i;
+ }
+ return -1;
+ },
+
+ map: function(fn, bind){
+ var results = [];
+ for (var i = 0, l = this.length; i < l; i++){
+ if (i in this) results[i] = fn.call(bind, this[i], i, this);
+ }
+ return results;
+ },
+
+ some: function(fn, bind){
+ for (var i = 0, l = this.length; i < l; i++){
+ if ((i in this) && fn.call(bind, this[i], i, this)) return true;
+ }
+ return false;
+ },
+
+ associate: function(keys){
+ var obj = {}, length = Math.min(this.length, keys.length);
+ for (var i = 0; i < length; i++) obj[keys[i]] = this[i];
+ return obj;
+ },
+
+ link: function(object){
+ var result = {};
+ for (var i = 0, l = this.length; i < l; i++){
+ for (var key in object){
+ if (object[key](this[i])){
+ result[key] = this[i];
+ delete object[key];
+ break;
+ }
+ }
+ }
+ return result;
+ },
+
+ contains: function(item, from){
+ return this.indexOf(item, from) != -1;
+ },
+
+ append: function(array){
+ this.push.apply(this, array);
+ return this;
+ },
+
+ getLast: function(){
+ return (this.length) ? this[this.length - 1] : null;
+ },
+
+ getRandom: function(){
+ return (this.length) ? this[Number.random(0, this.length - 1)] : null;
+ },
+
+ include: function(item){
+ if (!this.contains(item)) this.push(item);
+ return this;
+ },
+
+ combine: function(array){
+ for (var i = 0, l = array.length; i < l; i++) this.include(array[i]);
+ return this;
+ },
+
+ erase: function(item){
+ for (var i = this.length; i--;){
+ if (this[i] === item) this.splice(i, 1);
+ }
+ return this;
+ },
+
+ empty: function(){
+ this.length = 0;
+ return this;
+ },
+
+ flatten: function(){
+ var array = [];
+ for (var i = 0, l = this.length; i < l; i++){
+ var type = typeOf(this[i]);
+ if (type == 'null') continue;
+ array = array.concat((type == 'array' || type == 'collection' || type == 'arguments' || instanceOf(this[i], Array)) ? Array.flatten(this[i]) : this[i]);
+ }
+ return array;
+ },
+
+ pick: function(){
+ for (var i = 0, l = this.length; i < l; i++){
+ if (this[i] != null) return this[i];
+ }
+ return null;
+ },
+
+ hexToRgb: function(array){
+ if (this.length != 3) return null;
+ var rgb = this.map(function(value){
+ if (value.length == 1) value += value;
+ return value.toInt(16);
+ });
+ return (array) ? rgb : 'rgb(' + rgb + ')';
+ },
+
+ rgbToHex: function(array){
+ if (this.length < 3) return null;
+ if (this.length == 4 && this[3] == 0 && !array) return 'transparent';
+ var hex = [];
+ for (var i = 0; i < 3; i++){
+ var bit = (this[i] - 0).toString(16);
+ hex.push((bit.length == 1) ? '0' + bit : bit);
+ }
+ return (array) ? hex : '#' + hex.join('');
+ }
+
+});
+
+//<1.2compat>
+
+Array.alias('extend', 'append');
+
+var $pick = function(){
+ return Array.from(arguments).pick();
+};
+
+//1.2compat>
+
+
+/*
+---
+
+name: String
+
+description: Contains String Prototypes like camelCase, capitalize, test, and toInt.
+
+license: MIT-style license.
+
+requires: Type
+
+provides: String
+
+...
+*/
+
+String.implement({
+
+ test: function(regex, params){
+ return ((typeOf(regex) == 'regexp') ? regex : new RegExp('' + regex, params)).test(this);
+ },
+
+ contains: function(string, separator){
+ return (separator) ? (separator + this + separator).indexOf(separator + string + separator) > -1 : this.indexOf(string) > -1;
+ },
+
+ trim: function(){
+ return this.replace(/^\s+|\s+$/g, '');
+ },
+
+ clean: function(){
+ return this.replace(/\s+/g, ' ').trim();
+ },
+
+ camelCase: function(){
+ return this.replace(/-\D/g, function(match){
+ return match.charAt(1).toUpperCase();
+ });
+ },
+
+ hyphenate: function(){
+ return this.replace(/[A-Z]/g, function(match){
+ return ('-' + match.charAt(0).toLowerCase());
+ });
+ },
+
+ capitalize: function(){
+ return this.replace(/\b[a-z]/g, function(match){
+ return match.toUpperCase();
+ });
+ },
+
+ escapeRegExp: function(){
+ return this.replace(/([-.*+?^${}()|[\]\/\\])/g, '\\$1');
+ },
+
+ toInt: function(base){
+ return parseInt(this, base || 10);
+ },
+
+ toFloat: function(){
+ return parseFloat(this);
+ },
+
+ hexToRgb: function(array){
+ var hex = this.match(/^#?(\w{1,2})(\w{1,2})(\w{1,2})$/);
+ return (hex) ? hex.slice(1).hexToRgb(array) : null;
+ },
+
+ rgbToHex: function(array){
+ var rgb = this.match(/\d{1,3}/g);
+ return (rgb) ? rgb.rgbToHex(array) : null;
+ },
+
+ substitute: function(object, regexp){
+ return this.replace(regexp || (/\\?\{([^{}]+)\}/g), function(match, name){
+ if (match.charAt(0) == '\\') return match.slice(1);
+ return (object[name] != null) ? object[name] : '';
+ });
+ }
+
+});
+
+
+/*
+---
+
+name: Number
+
+description: Contains Number Prototypes like limit, round, times, and ceil.
+
+license: MIT-style license.
+
+requires: Type
+
+provides: Number
+
+...
+*/
+
+Number.implement({
+
+ limit: function(min, max){
+ return Math.min(max, Math.max(min, this));
+ },
+
+ round: function(precision){
+ precision = Math.pow(10, precision || 0).toFixed(precision < 0 ? -precision : 0);
+ return Math.round(this * precision) / precision;
+ },
+
+ times: function(fn, bind){
+ for (var i = 0; i < this; i++) fn.call(bind, i, this);
+ },
+
+ toFloat: function(){
+ return parseFloat(this);
+ },
+
+ toInt: function(base){
+ return parseInt(this, base || 10);
+ }
+
+});
+
+Number.alias('each', 'times');
+
+(function(math){
+ var methods = {};
+ math.each(function(name){
+ if (!Number[name]) methods[name] = function(){
+ return Math[name].apply(null, [this].concat(Array.from(arguments)));
+ };
+ });
+ Number.implement(methods);
+})(['abs', 'acos', 'asin', 'atan', 'atan2', 'ceil', 'cos', 'exp', 'floor', 'log', 'max', 'min', 'pow', 'sin', 'sqrt', 'tan']);
+
+
+/*
+---
+
+name: Function
+
+description: Contains Function Prototypes like create, bind, pass, and delay.
+
+license: MIT-style license.
+
+requires: Type
+
+provides: Function
+
+...
+*/
+
+Function.extend({
+
+ attempt: function(){
+ for (var i = 0, l = arguments.length; i < l; i++){
+ try {
+ return arguments[i]();
+ } catch (e){}
+ }
+ return null;
+ }
+
+});
+
+Function.implement({
+
+ attempt: function(args, bind){
+ try {
+ return this.apply(bind, Array.from(args));
+ } catch (e){}
+
+ return null;
+ },
+
+ bind: function(bind){
+ var self = this,
+ args = (arguments.length > 1) ? Array.slice(arguments, 1) : null;
+
+ return function(){
+ if (!args && !arguments.length) return self.call(bind);
+ if (args && arguments.length) return self.apply(bind, args.concat(Array.from(arguments)));
+ return self.apply(bind, args || arguments);
+ };
+ },
+
+ pass: function(args, bind){
+ var self = this;
+ if (args != null) args = Array.from(args);
+ return function(){
+ return self.apply(bind, args || arguments);
+ };
+ },
+
+ delay: function(delay, bind, args){
+ return setTimeout(this.pass((args == null ? [] : args), bind), delay);
+ },
+
+ periodical: function(periodical, bind, args){
+ return setInterval(this.pass((args == null ? [] : args), bind), periodical);
+ }
+
+});
+
+//<1.2compat>
+
+delete Function.prototype.bind;
+
+Function.implement({
+
+ create: function(options){
+ var self = this;
+ options = options || {};
+ return function(event){
+ var args = options.arguments;
+ args = (args != null) ? Array.from(args) : Array.slice(arguments, (options.event) ? 1 : 0);
+ if (options.event) args = [event || window.event].extend(args);
+ var returns = function(){
+ return self.apply(options.bind || null, args);
+ };
+ if (options.delay) return setTimeout(returns, options.delay);
+ if (options.periodical) return setInterval(returns, options.periodical);
+ if (options.attempt) return Function.attempt(returns);
+ return returns();
+ };
+ },
+
+ bind: function(bind, args){
+ var self = this;
+ if (args != null) args = Array.from(args);
+ return function(){
+ return self.apply(bind, args || arguments);
+ };
+ },
+
+ bindWithEvent: function(bind, args){
+ var self = this;
+ if (args != null) args = Array.from(args);
+ return function(event){
+ return self.apply(bind, (args == null) ? arguments : [event].concat(args));
+ };
+ },
+
+ run: function(args, bind){
+ return this.apply(bind, Array.from(args));
+ }
+
+});
+
+var $try = Function.attempt;
+
+//1.2compat>
+
+
+/*
+---
+
+name: Object
+
+description: Object generic methods
+
+license: MIT-style license.
+
+requires: Type
+
+provides: [Object, Hash]
+
+...
+*/
+
+(function(){
+
+var hasOwnProperty = Object.prototype.hasOwnProperty;
+
+Object.extend({
+
+ subset: function(object, keys){
+ var results = {};
+ for (var i = 0, l = keys.length; i < l; i++){
+ var k = keys[i];
+ results[k] = object[k];
+ }
+ return results;
+ },
+
+ map: function(object, fn, bind){
+ var results = {};
+ for (var key in object){
+ if (hasOwnProperty.call(object, key)) results[key] = fn.call(bind, object[key], key, object);
+ }
+ return results;
+ },
+
+ filter: function(object, fn, bind){
+ var results = {};
+ Object.each(object, function(value, key){
+ if (fn.call(bind, value, key, object)) results[key] = value;
+ });
+ return results;
+ },
+
+ every: function(object, fn, bind){
+ for (var key in object){
+ if (hasOwnProperty.call(object, key) && !fn.call(bind, object[key], key)) return false;
+ }
+ return true;
+ },
+
+ some: function(object, fn, bind){
+ for (var key in object){
+ if (hasOwnProperty.call(object, key) && fn.call(bind, object[key], key)) return true;
+ }
+ return false;
+ },
+
+ keys: function(object){
+ var keys = [];
+ for (var key in object){
+ if (hasOwnProperty.call(object, key)) keys.push(key);
+ }
+ return keys;
+ },
+
+ values: function(object){
+ var values = [];
+ for (var key in object){
+ if (hasOwnProperty.call(object, key)) values.push(object[key]);
+ }
+ return values;
+ },
+
+ getLength: function(object){
+ return Object.keys(object).length;
+ },
+
+ keyOf: function(object, value){
+ for (var key in object){
+ if (hasOwnProperty.call(object, key) && object[key] === value) return key;
+ }
+ return null;
+ },
+
+ contains: function(object, value){
+ return Object.keyOf(object, value) != null;
+ },
+
+ toQueryString: function(object, base){
+ var queryString = [];
+
+ Object.each(object, function(value, key){
+ if (base) key = base + '[' + key + ']';
+ var result;
+ switch (typeOf(value)){
+ case 'object': result = Object.toQueryString(value, key); break;
+ case 'array':
+ var qs = {};
+ value.each(function(val, i){
+ qs[i] = val;
+ });
+ result = Object.toQueryString(qs, key);
+ break;
+ default: result = key + '=' + encodeURIComponent(value);
+ }
+ if (value != null) queryString.push(result);
+ });
+
+ return queryString.join('&');
+ }
+
+});
+
+})();
+
+//<1.2compat>
+
+Hash.implement({
+
+ has: Object.prototype.hasOwnProperty,
+
+ keyOf: function(value){
+ return Object.keyOf(this, value);
+ },
+
+ hasValue: function(value){
+ return Object.contains(this, value);
+ },
+
+ extend: function(properties){
+ Hash.each(properties || {}, function(value, key){
+ Hash.set(this, key, value);
+ }, this);
+ return this;
+ },
+
+ combine: function(properties){
+ Hash.each(properties || {}, function(value, key){
+ Hash.include(this, key, value);
+ }, this);
+ return this;
+ },
+
+ erase: function(key){
+ if (this.hasOwnProperty(key)) delete this[key];
+ return this;
+ },
+
+ get: function(key){
+ return (this.hasOwnProperty(key)) ? this[key] : null;
+ },
+
+ set: function(key, value){
+ if (!this[key] || this.hasOwnProperty(key)) this[key] = value;
+ return this;
+ },
+
+ empty: function(){
+ Hash.each(this, function(value, key){
+ delete this[key];
+ }, this);
+ return this;
+ },
+
+ include: function(key, value){
+ if (this[key] == null) this[key] = value;
+ return this;
+ },
+
+ map: function(fn, bind){
+ return new Hash(Object.map(this, fn, bind));
+ },
+
+ filter: function(fn, bind){
+ return new Hash(Object.filter(this, fn, bind));
+ },
+
+ every: function(fn, bind){
+ return Object.every(this, fn, bind);
+ },
+
+ some: function(fn, bind){
+ return Object.some(this, fn, bind);
+ },
+
+ getKeys: function(){
+ return Object.keys(this);
+ },
+
+ getValues: function(){
+ return Object.values(this);
+ },
+
+ toQueryString: function(base){
+ return Object.toQueryString(this, base);
+ }
+
+});
+
+Hash.extend = Object.append;
+
+Hash.alias({indexOf: 'keyOf', contains: 'hasValue'});
+
+//1.2compat>
+
+
+/*
+---
+
+name: Browser
+
+description: The Browser Object. Contains Browser initialization, Window and Document, and the Browser Hash.
+
+license: MIT-style license.
+
+requires: [Array, Function, Number, String]
+
+provides: [Browser, Window, Document]
+
+...
+*/
+
+(function(){
+
+var document = this.document;
+var window = document.window = this;
+
+var UID = 1;
+
+this.$uid = (window.ActiveXObject) ? function(item){
+ return (item.uid || (item.uid = [UID++]))[0];
+} : function(item){
+ return item.uid || (item.uid = UID++);
+};
+
+$uid(window);
+$uid(document);
+
+var ua = navigator.userAgent.toLowerCase(),
+ platform = navigator.platform.toLowerCase(),
+ UA = ua.match(/(opera|ie|firefox|chrome|version)[\s\/:]([\w\d\.]+)?.*?(safari|version[\s\/:]([\w\d\.]+)|$)/) || [null, 'unknown', 0],
+ mode = UA[1] == 'ie' && document.documentMode;
+
+var Browser = this.Browser = {
+
+ extend: Function.prototype.extend,
+
+ name: (UA[1] == 'version') ? UA[3] : UA[1],
+
+ version: mode || parseFloat((UA[1] == 'opera' && UA[4]) ? UA[4] : UA[2]),
+
+ Platform: {
+ name: ua.match(/ip(?:ad|od|hone)/) ? 'ios' : (ua.match(/(?:webos|android)/) || platform.match(/mac|win|linux/) || ['other'])[0]
+ },
+
+ Features: {
+ xpath: !!(document.evaluate),
+ air: !!(window.runtime),
+ query: !!(document.querySelector),
+ json: !!(window.JSON)
+ },
+
+ Plugins: {}
+
+};
+
+Browser[Browser.name] = true;
+Browser[Browser.name + parseInt(Browser.version, 10)] = true;
+Browser.Platform[Browser.Platform.name] = true;
+
+// Request
+
+Browser.Request = (function(){
+
+ var XMLHTTP = function(){
+ return new XMLHttpRequest();
+ };
+
+ var MSXML2 = function(){
+ return new ActiveXObject('MSXML2.XMLHTTP');
+ };
+
+ var MSXML = function(){
+ return new ActiveXObject('Microsoft.XMLHTTP');
+ };
+
+ return Function.attempt(function(){
+ XMLHTTP();
+ return XMLHTTP;
+ }, function(){
+ MSXML2();
+ return MSXML2;
+ }, function(){
+ MSXML();
+ return MSXML;
+ });
+
+})();
+
+Browser.Features.xhr = !!(Browser.Request);
+
+// Flash detection
+
+var version = (Function.attempt(function(){
+ return navigator.plugins['Shockwave Flash'].description;
+}, function(){
+ return new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version');
+}) || '0 r0').match(/\d+/g);
+
+Browser.Plugins.Flash = {
+ version: Number(version[0] || '0.' + version[1]) || 0,
+ build: Number(version[2]) || 0
+};
+
+// String scripts
+
+Browser.exec = function(text){
+ if (!text) return text;
+ if (window.execScript){
+ window.execScript(text);
+ } else {
+ var script = document.createElement('script');
+ script.setAttribute('type', 'text/javascript');
+ script.text = text;
+ document.head.appendChild(script);
+ document.head.removeChild(script);
+ }
+ return text;
+};
+
+String.implement('stripScripts', function(exec){
+ var scripts = '';
+ var text = this.replace(/