#!/usr/local/bin/perl # iWarden v.9b3 # Keep our carrier from billing us for going over our plan # Warns for data & phone at 75/85/95% # Disables cellular data when we reach our limit # Puts cell phone in 'whitelist only mode' when we're out of minutes # # Copyright (c) 2012 Jeremy Kister # http://jeremy.kister.net./ # Released under Perl's "Artistic" License. # 2012.03.06 use lib qw(/var/mobile/Library/iWarden/lib); $ENV{PATH} = '/bin:/sbin:/usr/bin'; use strict; use DBI; use IPC::Open2; # QQQ temp hack below use File::Temp; use Getopt::Std; use Time::Local 'timelocal_nocheck'; use JK::Misc; our %opt; getopts('D', \%opt); # Debug my $name = 'iWarden'; my %config = ( day => 16, data => 300, minutes => 200, ignore_mobile => 1, ignore_nights_weekends => 1, night_start_hour => 21, night_end_hour => 5, minutes_alert => { dynamic => { 25 => 50, 50 => 100, }, static => { 75 => 150, 85 => 170, 95 => 190, }, }, data_alert => { dynamic => { 25 => 75, 50 => 250, }, static => { 75 => 225, 85 => 255, 95 => 285, }, }, minutes_action => 95, data_action => 95, plist => "/var/mobile/Library/Preferences/${name}.plist", ) my %dsn = ( cdb => 'DBI:SQLite:dbname=/var/wireless/Library/CallHistory/call_history.db', idb => 'DBI:SQLite:dbname=/var/mobile/Library/iBlacklist/iBlacklist.sqlitedb', adb => 'DBI:SQLite:dbname=/var/mobile/Library/AddressBook/AddressBook.sqlitedb', rdb => "DBI:SQLite:dbname=/var/mobile/Library/${name}/main.db", ); my %dbh = ( c => DBI->connect( $dsn{cdb} ), i => DBI->connect( $dsn{idb} ), r => DBI->connect( $dsn{rdb} ), ); if( $config{ignore_mobile} ){ #$dbh{a} = DBI->connect( $dsn{adb} ); # WTF? DBD::SQLite::db prepare failed: file is encrypted or is not a database # https://rt.cpan.org/Ticket/Display.html?id=75494 # QQQ hack below.. } debug( "$name starting." ); $SIG{USR1} = sub { verbose( "SIGUSR1 received.") }; debug( "* checking iBlacklist" ); my $x = $dbh{i}->selectrow_arrayref( q{ SELECT objList FROM Lists WHERE sName = 'iWarden' LIMIT 1 } ); unless( $x->[0] ){ debug( "* configuring iBlacklist" ); $dbh{i}->do( q{ INSERT INTO Lists (sName,iMode,iActive,iOrder) VALUES ('iWarden',1,0,20) } ); $x = $dbh{i}->selectrow_arrayref( q{ SELECT objList FROM Lists WHERE sName = 'iWarden' LIMIT 1 } ); my $obj = $x->[0]; for my $n (1..7){ $dbh{i}->do( "INSERT INTO Scheduler VALUES ($obj,$n,0,0,23,59,1,0)" ); } for my $target (qw/911 **********/ ){ # *{10} for not blocking texts my($name,$call); if( $target eq 911 ){ $name = 'Emergency 911'; $call = 1; }else{ $name = 'All Text'; $call = 0; } my $sql = 'INSERT INTO Contacts (sDisplay,iNumber,bCALL,bSMS,objList,' . 'bAUTOSMS,iAction,bAUTOSMS2,sMsgCall,sMsgSMS) VALUES (' . "'$name','$target',$call,1,$obj,0,0,0,'.','.')"; $dbh{i}->do( $sql ); } } my %mem = (obj => $x->[0], state => { call => 1, data => 1, }, ); my %res; eval { debug( "* checking database." ); for my $table (qw/call_hist data_hist iBlacklistLists iBlacklistExtras/){ $res{$table} = $dbh{r}->selectrow_arrayref( "SELECT count(*) FROM sqlite_master" . " WHERE type='table'" . " AND name='$table'" ); } }; unless( $res{iBlacklistLists}->[0] ){ debug( "* creating iBlacklistLists table." ); $dbh{r}->do( q{CREATE TABLE iBlacklistLists ( id INTEGER PRIMARY KEY AUTOINCREMENT, objList INTEGER NOT NULL, iActive INTEGER NOT NULL, iOrder INTEGER NOT NULL ) } ); } unless( $res{iBlacklistExtras}->[0] ){ debug( "* creating iBlacklistExtras table." ); $dbh{r}->do( q{CREATE TABLE iBlacklistExtras ( sField TEXT NOT NULL, iValue INTEGER NOT NULL ) } ); } my %recalc; my $last_reset = last_reset(); my ($next_reset,$percent,$time); if( $res{call_hist}->[0] ){ $dbh{r}->do( "DELETE FROM call_hist WHERE ctime < $last_reset" ); my $sql = 'SELECT rid,ctime,dur FROM call_hist'; my $sth = $dbh{r}->prepare($sql); $sth->execute; while(my $row = $sth->fetchrow_arrayref){ $mem{call}{$row->[0]} = { time => $row->[1], min => $row->[2], }; } $recalc{call} = 1; }else{ debug( "* creating call_hist table." ); $dbh{r}->do( q{CREATE TABLE call_hist ( id INTEGER PRIMARY KEY AUTOINCREMENT, rid INTEGER NOT NULL, ctime INTEGER NOT NULL, dur INTEGER NOT NULL ) }, ); } if( $res{data_hist}->[0] ){ $dbh{r}->do( "DELETE FROM data_hist WHERE dtime < $last_reset" ); my $x = $dbh{r}->selectrow_arrayref( q{ SELECT bytes FROM data_hist ORDER BY dtime DESC LIMIT 1 } ); $mem{data} = $x->[0]; # most recent $recalc{data} = 1; }else{ debug( "* creating data_hist table." ); $dbh{r}->do( q{CREATE TABLE data_hist ( id INTEGER PRIMARY KEY AUTOINCREMENT, bytes INTEGER NOT NULL, dtime INTEGER NOT NULL ) }, ); } while( 1 ){ debug( "* starting event loop" ); # check to see if our config has been updated my $mtime = (stat($config{plist}))[9]; unless( $config{mtime} eq $mtime ){ debug( "* reading PreferenceLoader config" ); open( PLIST, "plutil -l $plist 2>&1 |" ) || slowerr( "cannot fork plutil: $!" ); while(){ chomp; if( /^\s*"?([^"]+)"?\s+=\s+"?([^"]+)/ ){ my ($key,$value) = ($1,$2); if( $key eq 'minutes_alert' || $key eq 'data_alert' ){ my $plan = $key; $plan =~ s/_alert$//; for my $alert (split /\s+/, $value){ my($type,$thresholds) = split(/:/, $alert); for my $threshold (split /,/, $thresholds){ $config{$key}{$type}{$threshold} = ($config{plan} * ($threshold/100)); } } }else{ $config{$key} = $value; } } } close PLIST; $config{mtime} = $mtime; } # check to see if we are past our "next_reset" # if so, clear alert log and force recalc $time = time(); if( $time >= $next_reset ){ debug( "* entered next cycle; clearing alert cache" ); delete $mem{alert}; $recalc{call} = 1; $recalc{data} = 1; } my %contacts; if( $config{ignore_mobile} ){ # get a fresh list of contact types my $sql = 'SELECT value FROM ABMultiValueLabel;'; #my $sth = $dbh{a}->prepare($sql); #$sth->execute; my $i = 1; #while(my $row = $sth->fetchrow_arrayref){ # my $value = $row->[0]; # QQQ hack (see above) my $pid = open2(my $out, my $in, 'sqlite3', '/var/mobile/Library/AddressBook/AddressBook.sqlitedb'); print $in "$sql\n"; while(<$out>){ chomp(my $value = $_); if( $value eq 'iPhone' || $value eq '_$!!$_' ){ $contacts{label}{$value} = $i; debug( "* found $value => $i" ); last if($contacts{label}{'iPhone'} && $contacts{label}{'_$!!$_'}); } $i++; } # get a fresh list of mobile contacts. $sql = 'SELECT value FROM ABMultiValue' . ' WHERE property = 3' . " AND ( label = $contacts{label}{'iPhone'}" . " OR label = $contacts{label}{'_$!!$_'} );"; #my $sth = $dbh{a}->prepare($sql); #$sth->execute; #while(my $row = $sth->fetchrow_arrayref){ # my $num = $row->[0]; print $in "$sql\n"; close $in; while(<$out>){ chomp(my $num = $_); $num =~ s/[^\d]//g; # (215) 555-1212 -> 2155551212 $num =~ s/^1//; # we want 10 digits, not 11 next unless $num; $contacts{num}{$num} = 1; debug( "* will not count minutes to/from $num" ); } close $out; waitpid( $pid, 0 ); } # calls - date is the answer time my $top = (reverse sort keys %{$mem{call}})[0] || 0; my $sql = <<__EOS__ SELECT ROWID,address,date,duration FROM call WHERE ROWID > $top AND duration > 0 ORDER BY 'date' ASC __EOS__ ; my $sth = $dbh{c}->prepare($sql); $sth->execute; while(my $row = $sth->fetchrow_arrayref){ my $num = $row->[1]; $num =~ s/^1//; next if $contacts{num}{$num}; # mobile number; my $billsec = $row->[3]; if( $config{ignore_nights_weekends} ){ my $start = $row->[2]; my $end = ($start + $billsec); my($smin,$shour,$sday,$smon,$syr,$swday) = (localtime($start))[1,2,3,4,5,6]; my($emin,$ehour,$eday,$emon,$eyr,$ewday) = (localtime($end))[1,2,3,4,5,6]; # did the call start and end on saturday/sunday ? next if( ($swday eq 6 || $swday eq 0) && ($ewday eq 6 || $ewday eq 0) ); if( $shour >= $config{night_start_hour} || $shour <= $config{night_end_hour} ){ if( $ehour > $config{night_end_hour} ){ # start time in "nights", end time in day # how many minutes were after 5:59am ? my $t = timelocal_nocheck(0,59,5,$eday,$emon,$eyr); $billsec = ($end - $t); }else{ # start time and end time are within "nights" next; } }else{ # did the call start in day and end in "nights"? if( $shour > $config{night_end_hour} && $shour < $config{night_start_hour} ){ if( $ehour >= $config{night_start_hour} && $ehour <= $config{night_end_hour} ){ # how many minutes were before 9pm ? my $t = timelocal_nocheck(0,0,9,$sday,$smon,$syr); $billsec = ($t - $start); } } } } $recalc{call} = 1; my $r = ($billsec % 60); my $fuzz = $r ? (60-$r) : 0; my $min = ( ($billsec + $fuzz) / 60 ); $dbh{r}->do( 'INSERT INTO call_hist (rid,ctime,dur) VALUES (' . $dbh{r}->quote( $row->[0] ) . ',' . $dbh{r}->quote( $row->[2] ) . ',' . $dbh{r}->quote( $min ) . ')' ); $mem{call}{$row->[0]} = { time => $row->[2], min => $min, }; } # data $sql = q{ SELECT bytes_lifetime_rcvd,bytes_lifetime_sent FROM data WHERE pdp_ip = 0 }; $sth = $dbh{c}->prepare($sql); $sth->execute; my $row = $sth->fetchrow_arrayref; my $dtotal = ($row->[0] + $row->[1]); unless( $mem{data} eq $dtotal ){ $dbh{r}->do( 'INSERT INTO data_hist (bytes,dtime) VALUES (' . $dtotal . ',' . $time . ')' ); $mem{data} = $dtotal; $recalc{data} = 1; } if( keys %recalc ){ $last_reset = last_reset(); $next_reset = next_reset(); $percent = int( ((time()-$last_reset) / ($next_reset-$last_reset)) + 0.5 ); call_calc() if $recalc{call}; data_calc() if $recalc{data}; } my $nextrun = ( $time + 3600 ); while( time() < $nextrun ){ debug( "* sleeping 300" ); sleep 300; } } sub last_reset { my ($d,$m,$y) = (localtime)[3,4,5]; $y += 1900; if( $d >= $config{day} ){ return( timelocal_nocheck(0,0,0,$config{day},$m,$y) ); }else{ my($lm,$ly); if( $m > 0 ){ $lm = ($m-1); $ly = $y; }else{ $lm = 11; $ly = ($y-1); } return( timelocal_nocheck(0,0,0,$config{day},$lm,$ly) ); } } sub next_reset { my ($d,$m,$y) = (localtime)[3,4,5]; $y += 1900; if( $d <= $config{day} ){ return( timelocal_nocheck(0,0,0,$config{day},$m,$y) ); }else{ my($nm,$ny); if( $m == 11 ){ $nm = 0; $ny = ($y + 1); }else{ $nm = ($m+1); $ny = $y; } return( timelocal_nocheck(0,0,0,$config{day},$nm,$ny) ); } } sub call_calc { my $total = 0; for my $key (keys %{$mem{call}}){ if( $mem{call}{$key}{time} < $last_reset ){ debug( "* expire call/$key" ); $dbh{r}->do( "DELETE FROM call_hist WHERE rid = $key" ); delete $mem{call}{$key}{time}; }else{ $total += $mem{call}{$key}{min}; } } for my $threshold (reverse sort keys %{$config{minutes_alert}{static}}){ if( $total >= $config{minutes_alert}{static}{$threshold} ){ unless( $mem{alert}{call}{$threshold} ){ sb_alert( "${threshold}% of minutes used." ); $mem{alert}{call}{$threshold} = 1; } verbose( "* call over ${threshold}% (total: $total)!" ); if( $total >= $config{minutes_action} ){ unless( $mem{alert}{call}{$threshold} == 2 ){ # remember how iBlacklist used to be configured.. # only remember settings when iWarden list is disabled, otherwise # we go in Whitelist only mode. phone reboots. # we run again, see regular lists not active, and save that my $x = $dbh{i}->selectrow_arrayref( "SELECT COUNT(*) FROM Lists WHERE objList = $mem{obj} AND iActive = 0" ); if( $x->[0] ){ $dbh{r}->do( 'DELETE FROM iBlacklistLists' ); my $sql = "SELECT objList,iActive,iOrder FROM Lists WHERE objList != $mem{obj}"; my $sth = $dbh{i}->prepare($sql); $sth->execute; while( my $row=$sth->fetchrow_arrayref){ my $sql = "INSERT INTO iBlacklistLists (objList,iActive,iOrder)" . " VALUES ($row->[0],$row->[1],$row->[2])"; $dbh{r}->do( $sql ); } $dbh{r}->do( 'DELETE FROM iBlacklistExtras' ); for my $field (qw/bParental bMonitor/){ my $sql = "SELECT iValue FROM Extras WHERE sField = '$field'"; my $sth = $dbh{i}->prepare($sql); $sth->execute; my $row=$sth->fetchrow_arrayref; my $sqla = "INSERT INTO iBlacklistExtras (sField,iValue)" . " VALUES ('$field',$row->[0])"; $dbh{r}->do( $sqla ); } } sb_alert( "Phone in whitelist only mode." ); $mem{alert}{call}{$threshold} = 2; } debug( "* setting iBlacklist to Whitelist mode" ); $dbh{i}->do( "UPDATE Lists SET iActive = 0 WHERE objList != $mem{obj} AND iMode = 1" ); $dbh{i}->do( "UPDATE Lists SET iActive = 1, iOrder = 20 WHERE objList = $mem{obj}" ); $dbh{i}->do( "UPDATE Extras SET iValue = 1 WHERE sField = 'bParental' OR sField = 'bMonitor'" ); debug( "* running open com.iDevBrTeam.iBlacklist" ); system( 'open', 'com.iDevBrTeam.iBlacklist' ); sleep 3; if( my $pid = getpid('iBlacklist') ){ sleep 2; kill( 1, $pid ); }else{ verbose( "cannot find pid of iBlacklist" ); } $mem{state}{call} = 1; } last; }else{ debug( "* call below ${threshold}% (total: $total)" ); if( $mem{state}{call} ){ sb_alert( "Phone in reglar mode." ); # put iBlacklist back to normal my $sql = 'SELECT objList,iActive,iOrder FROM iBlacklistLists'; my $sth = $dbh{r}->prepare($sql); $sth->execute; while( my $row=$sth->fetchrow_arrayref){ my $sql = "UPDATE Lists SET " . " iActive = $row->[1]," . " iOrder = $row->[2]" . " WHERE objList = $row->[0]"; $dbh{i}->do( $sql ); } $dbh{i}->do( "UPDATE Lists SET iActive = 0, iOrder = 1000 WHERE objList = $mem{obj}" ); for my $field (qw/bParental bMonitor/){ my $sql = "SELECT iValue FROM iBlacklistExtras WHERE sField = '$field'"; my $sth = $dbh{r}->prepare($sql); $sth->execute; my $row=$sth->fetchrow_arrayref; next unless defined $row->[0]; # if whitelist-only mode never enabled my $sqla = "UPDATE Extras SET iValue = $row->[0] WHERE sField = '$field'"; $dbh{i}->do( $sqla ); } debug( "* running open com.iDevBrTeam.iBlacklist" ); system( 'open', 'com.iDevBrTeam.iBlacklist' ); sleep 3; if( my $pid = getpid('iBlacklist') ){ sleep 2; kill( 1, $pid ); }else{ verbose( "cannot find pid of iBlacklist" ); } $mem{state}{call} = 0; } } } for my $threshold (reverse sort keys %{$config{minutes_alert}{dynamic}}){ # only alert if time-appropriate if( ($percent >= $threshold) && ($total > $config{minutes_alert}{dynamic}{$threshold}) ){ unless( $mem{alert}{call}{$threshold} ){ sb_alert( "${threshold}% of minutes used." ); $mem{alert}{call}{$threshold} = 1; } verbose( "* call over ${threshold}% (total: $total)!" ); last; } } delete $recalc{call}; } sub data_calc { $dbh{r}->do( "DELETE FROM data_hist WHERE dtime < $last_reset" ); if( $config{data} == 0 ){ unless( $mem{alert}{data}{policy} ){ $mem{alert}{data}{policy} = 1; sb_alert( "Cellular Data disabled per policy." ); } debug( "* running ifconfig pdp_ip0 down" ); system( 'ifconfig', 'pdp_ip0', 'down' ); return; } my $old = $dbh{r}->selectrow_arrayref( q{ SELECT bytes FROM data_hist ORDER BY dtime ASC LIMIT 1 } ); my $current = $dbh{r}->selectrow_arrayref( q{ SELECT bytes FROM data_hist ORDER BY dtime DESC LIMIT 1 } ); my $total = ( ($current->[0] - $old->[0])/1000 ) || 0; # QQQ ATT measure in miB or mB? for my $threshold (reverse sort keys %{$config{data_alert}{static}}){ if( $total > $config{data_alert}{static}{$threshold} ){ unless( $mem{alert}{data}{$threshold} ){ sb_alert( "${threshold}% of data plan used." ); $mem{alert}{data}{$threshold} = 1; } verbose( "* data plan over ${threshold}% (total: $total)!" ); if( $total >= $config{data_action} ){ unless( $mem{alert}{data}{$threshold} ){ sb_alert( "Cellular Data disabled." ); } debug( "* running ifconfig pdp_ip0 down" ); system( 'ifconfig', 'pdp_ip0', 'down' ); $mem{state}{data} = 1; } last; }else{ debug( "* data below ${threshold}% (total: $total)" ); if( $mem{state}{data} ){ sb_alert( "Cellular Data enabled." ); debug( "* running ifconfig pdp_ip0 up" ); system( 'ifconfig', 'pdp_ip0', 'up' ); $mem{state}{data} = 0; } } } for my $threshold (reverse sort keys %{$config{data_alert}{dynamic}}){ # only alert if time-appropriate if( ($percent >= $threshold) && ($total > $config{data_alert}{dynamic}{$threshold}) ){ unless( $mem{alert}{data}{$threshold} ){ sb_alert( "${threshold}% of data plan used." ); $mem{alert}{data}{$threshold} = 1; } verbose( "* data plan over ${threshold}% (total: $total)!" ); last; } } delete $recalc{data}; } sub sb_alert { my ($msg) = join('', @_); # this is too lame XXX QQQ my $fh = File::Temp->new(); my $fname = $fh->filename; print $fh <<__EOF__ #!/usr/bin/cycript -p SpringBoard var message = [[UIAlertView alloc] init]; message.title = "$name alert"; message.message = "$msg"; [message addButtonWithTitle:\@"Dismiss"]; [message show]; __EOF__ ; close $fh; chmod(0744, $fname); system( $fname ); }