/* (C) 2012 Pragmatic Software This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/ */ package com.googlecode.networklog; import android.content.ContextWrapper; import android.app.Service; import android.content.Context; import android.content.Intent; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.widget.TextView; import android.widget.Toast; import android.util.Log; import android.os.Bundle; import android.os.IBinder; import android.os.Looper; import android.os.Handler; import android.os.Messenger; import android.os.Message; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.os.RemoteException; import android.content.BroadcastReceiver; import android.content.IntentFilter; import android.graphics.drawable.GradientDrawable; import java.util.HashMap; import java.util.ArrayList; import java.util.List; import java.io.File; import java.io.PrintWriter; import java.io.FileWriter; import java.io.BufferedWriter; import java.lang.Thread; import java.lang.Runnable; public class NetworkLogService extends Service { ArrayList<Messenger> clients = new ArrayList<Messenger>(); static final int NOTIFICATION_ID = "Network Log".hashCode(); static final int MSG_REGISTER_CLIENT = 1; static final int MSG_UNREGISTER_CLIENT = 2; static final int MSG_UPDATE_NOTIFICATION = 3; static final int MSG_BROADCAST_LOG_ENTRY = 4; static final int MSG_TOGGLE_FOREGROUND = 5; final Messenger messenger = new Messenger(new IncomingHandler(this)); boolean has_root = false; boolean has_binaries = false; public static NetworkLogService instance = null; private static Context context; public static Handler handler; public static String logfileString = ""; public static Toast toast; public static TextView toastTextView; public static CharSequence toastText; public static boolean toastEnabled; public static int toastDuration; public static int toastPosition; public static int toastDefaultYOffset; public static int toastYOffset; public static int toastOpacity; public static boolean toastShowAddress; public static HashMap<String, String> blockedApps; public static HashMap<String, String> toastBlockedApps; public static boolean invertUploadDownload; public static boolean behindFirewall; public static boolean watchRules; public static int watchRulesTimeout; public static boolean throughputBps; private class IncomingHandler extends Handler { private Context context; public IncomingHandler(Context context) { this.context = context; } @Override public void handleMessage(Message msg) { MyLog.d("[service] got message: " + msg); switch(msg.what) { case MSG_REGISTER_CLIENT: MyLog.d("[service] registering client " + msg.replyTo); clients.add(msg.replyTo); break; case MSG_UNREGISTER_CLIENT: MyLog.d("[service] unregistering client " + msg.replyTo); clients.remove(msg.replyTo); break; case MSG_UPDATE_NOTIFICATION: if(MyLog.enabled) { MyLog.d("[service] updating notification: " + ((String)msg.obj)); } updateNotification((String)msg.obj); break; case MSG_TOGGLE_FOREGROUND: MyLog.d("[service] toggling service foreground state: " + ((Boolean)msg.obj)); start_foreground = (Boolean)msg.obj; if(start_foreground) { startForeground(notification); } else { stopForeground(); } break; case MSG_BROADCAST_LOG_ENTRY: MyLog.d("[service] got MSG_BROADCOAST_LOG_ENTRY unexpectedly"); break; default: MyLog.d("[service] unhandled message"); super.handleMessage(msg); } } } @Override public IBinder onBind(Intent intent) { MyLog.d("[service] onBind"); if(!has_root || !has_binaries) { return null; } else { return messenger.getBinder(); } } private static HashMap<String, Integer> logEntriesMap = new HashMap<String, Integer>(); private InteractiveShell loggerShell; private NetworkLogger logger; private static String logfile = null; private PrintWriter logWriter = null; private static NotificationManager nManager; private static Notification notification; private static int notificationIcon; private static LogEntry entry; private static Boolean start_foreground = true; private NetStat netstat = new NetStat(); private FastParser parser = new FastParser(); public void startForeground(Notification n) { startForeground(NOTIFICATION_ID, n); } public void stopForeground() { stopForeground(true); } public Notification createNotification() { notificationIcon = R.drawable.up0_down0; Notification n = new Notification(notificationIcon, getString(R.string.logging_started), System.currentTimeMillis()); Intent i = new Intent(this, NetworkLog.class); i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); PendingIntent pi = PendingIntent.getActivity(this, 0, i, 0); n.setLatestEventInfo(this, getString(R.string.app_name), getString(R.string.logging_active), pi); return n; } static Runnable updateNotificationRunner = new Runnable() { public void run() { updateNotification(); } }; public static void updateNotification(int icon) { if(instance != null && handler != null) { notificationIcon = icon; handler.post(updateNotificationRunner); } } public static void updateNotification() { if(logfileString.length() > 0) { updateNotification(ThroughputTracker.throughputString + " [" + logfileString + "]"); } else { updateNotification(ThroughputTracker.throughputString); } } public static void updateNotification(String text) { if(instance == null || context == null || notification == null) { return; } Intent i = new Intent(context, NetworkLog.class); i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); PendingIntent pi = PendingIntent.getActivity(context, 0, i, 0); notification.setLatestEventInfo(context, context.getResources().getString(R.string.app_name), text, pi); notification.icon = notificationIcon; if(start_foreground) { nManager.notify(NOTIFICATION_ID, notification); } } private static abstract class CancelableRunnable implements Runnable { public boolean cancel; } private static Runnable showOnlyToastRunnable; private static CancelableRunnable showToastRunnable; private static View toastLayout; public static void showToast(final Context context, final Handler handler, final CharSequence text, boolean cancel) { if(showToastRunnable == null) { showToastRunnable = new CancelableRunnable() { public void run() { if(cancel && toast != null) { toast.cancel(); } if(cancel || toast == null) { toastLayout = ((LayoutInflater)context.getSystemService(LAYOUT_INFLATER_SERVICE)).inflate(R.layout.custom_toast, null); toastTextView = (TextView) toastLayout.findViewById(R.id.toasttext); if(android.os.Build.VERSION.SDK_INT > 10 || toast == null) { toast = new Toast(context); } toastDefaultYOffset = toast.getYOffset(); GradientDrawable background = (GradientDrawable) toastLayout.getBackground(); background.setColor(toastOpacity << 24); toast.setView(toastLayout); } switch(toastDuration) { case 3500: toast.setDuration(Toast.LENGTH_LONG); break; case 7000: toast.setDuration(Toast.LENGTH_LONG); if(showOnlyToastRunnable == null) { showOnlyToastRunnable = new Runnable() { public void run() { toast.show(); } }; } handler.postDelayed(showOnlyToastRunnable, 3250); break; default: toast.setDuration(Toast.LENGTH_SHORT); } switch(toastPosition) { case 1: toast.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.TOP, 0, toastYOffset); break; case 2: toast.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM, 0, toastYOffset); break; default: toast.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM, 0, toastDefaultYOffset); break; } toastTextView.setText(android.text.Html.fromHtml(toastText.toString())); toast.show(); } }; } showToastRunnable.cancel = cancel; toastText = text; handler.post(showToastRunnable); } public static void showToast(final CharSequence text) { if(context == null || handler == null || toastEnabled == false) { return; } showToast(context, handler, text, false); } public boolean hasRoot() { return SysUtils.checkRoot(this); } @Override public void onCreate() { MyLog.d("[service] onCreate"); if(NetworkLog.shell == null) { NetworkLog.shell = SysUtils.createRootShell(this, "NLServiceRootShell", true); } if(!hasRoot()) { SysUtils.showError(this, getString(R.string.error_default_title), getString(R.string.error_noroot)); has_root = false; stopSelf(); return; } else { has_root = true; } if(!SysUtils.installBinaries(this)) { has_binaries = false; stopSelf(); return; } else { has_binaries = true; } if(instance != null) { Log.w("NetworkLog", "[service] Last instance destroyed unexpectedly"); } instance = this; handler = new Handler(); if(ApplicationsTracker.installedApps == null) { ApplicationsTracker.getInstalledApps(this, null); } if(NetworkLog.settings == null) { NetworkLog.settings = new Settings(this); } toastEnabled = NetworkLog.settings.getToastNotifications(); toastDuration = NetworkLog.settings.getToastNotificationsDuration(); toastPosition = NetworkLog.settings.getToastNotificationsPosition(); toastYOffset = NetworkLog.settings.getToastNotificationsYOffset(); toastOpacity = NetworkLog.settings.getToastNotificationsOpacity(); toastShowAddress = NetworkLog.settings.getToastNotificationsShowAddress(); toastBlockedApps = new SelectToastApps().loadBlockedApps(this); blockedApps = new SelectBlockedApps().loadBlockedApps(this); invertUploadDownload = NetworkLog.settings.getInvertUploadDownload(); behindFirewall = NetworkLog.settings.getBehindFirewall(); watchRules = NetworkLog.settings.getWatchRules(); watchRulesTimeout = NetworkLog.settings.getWatchRulesTimeout(); throughputBps = NetworkLog.settings.getThroughputBps(); updateLogfileString(); ThroughputTracker.startUpdater(); nManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); notification = createNotification(); start_foreground = NetworkLog.settings.getStartForeground(); if(start_foreground) { startForeground(notification); } } @Override public int onStartCommand(Intent intent, int flags, int startId) { MyLog.d("[service] onStartCommand"); if(!has_root || !has_binaries) { return Service.START_NOT_STICKY; } Bundle ext = null; if(intent == null) { MyLog.d("[service] Service null intent"); } else { ext = intent.getExtras(); } final Bundle extras = ext; context = this; // run in background thread new Thread(new Runnable() { public void run() { String logfile_from_intent = null; if(extras != null) { logfile_from_intent = extras.getString("logfile"); MyLog.d("[service] set logfile: " + logfile_from_intent); } if(logfile_from_intent == null) { logfile_from_intent = NetworkLog.settings.getLogFile(); } MyLog.d("[service] NetworkLog service starting [" + logfile_from_intent + "]");; logfile = logfile_from_intent; initEntriesMap(); if(!startLogging()) { MyLog.d("[service] start logging error, aborting"); handler.post(new Runnable() { public void run() { stopSelf(); } }); } } }).start(); return Service.START_STICKY; } @Override public void onDestroy() { MyLog.d("[service] onDestroy"); stopForeground(); ThroughputTracker.stopUpdater(); if(NetworkLog.loggingButton != null) { NetworkLog.loggingButton.setChecked(false); } if(has_root && has_binaries) { stopLogging(); Toast.makeText(this, getString(R.string.logging_stopped), Toast.LENGTH_SHORT).show(); } instance = null; context = null; handler = null; } public static NetworkLogService getInstance() { return instance; } public void initEntriesMap() { ArrayList<NetStat.Connection> connections = netstat.getConnections(); for(NetStat.Connection connection : connections) { String mapKey = connection.src + ":" + connection.spt + " -> " + connection.dst + ":" + connection.dpt; if(MyLog.enabled && MyLog.level >= 5) { MyLog.d(5, "[netstat src-dst] New entry " + connection.uid + " for [" + mapKey + "]"); } logEntriesMap.put(mapKey, Integer.valueOf(connection.uid)); mapKey = connection.dst + ":" + connection.dpt + " -> " + connection.src + ":" + connection.spt; if(MyLog.enabled && MyLog.level >= 5) { MyLog.d(5, "[netstat dst-src] New entry " + connection.uid + " for [" + mapKey + "]"); } logEntriesMap.put(mapKey, Integer.valueOf(connection.uid)); } } public void parseResult(String result) { if(MyLog.enabled && MyLog.level >= 10) { MyLog.d(10, "--------------- parsing network entry --------------"); } int pos = 0, lastpos, thisEntry, nextEntry, newline, space; String in, out, src, dst, proto, uidString; int spt, dpt, len, uid; parser.setLine(result.toCharArray(), result.length() - 1); while((pos = result.indexOf("{NL}", pos)) > -1) { if(MyLog.enabled && MyLog.level >= 10) { MyLog.d(10, "---- got {NL} at " + pos + " ----"); } pos += "{NL}".length(); // skip past "{NL}" thisEntry = pos; newline = result.indexOf("\n", pos); nextEntry = result.indexOf("{NL}", pos); if(newline == -1) { newline = result.length(); } if(nextEntry != -1 && nextEntry < newline) { // Log.w("NetworkLog", "Skipping corrupted entry [" + result.substring(thisEntry, newline) + "]"); pos = newline; continue; } try { pos = result.indexOf("IN=", pos); if(pos == -1 || pos > newline) { // Log.w("NetworkLog", "Skipping corrupted entry [" + result.substring(thisEntry, newline) + "]"); pos = newline; continue; } space = result.indexOf(" ", pos); if(space == -1 || space > newline) { // Log.w("NetworkLog", "Skipping corrupted entry [" + result.substring(thisEntry, newline) + "]"); pos = newline; continue; } parser.setPos(pos + 3); in = parser.getString(); pos = result.indexOf("OUT=", pos); if(pos == -1 || pos > newline) { // Log.w("NetworkLog", "Skipping corrupted entry [" + result.substring(thisEntry, newline) + "]"); pos = newline; continue; } space = result.indexOf(" ", pos); if(space == -1 || space > newline) { // Log.w("NetworkLog", "Skipping corrupted entry [" + result.substring(thisEntry, newline) + "]"); pos = newline; continue; } parser.setPos(pos + 4); out = parser.getString(); pos = result.indexOf("SRC=", pos); if(pos == -1 || pos > newline) { // Log.w("NetworkLog", "Skipping corrupted entry [" + result.substring(thisEntry, newline) + "]"); pos = newline; continue; } space = result.indexOf(" ", pos); if(space == -1 || space > newline) { // Log.w("NetworkLog", "Skipping corrupted entry [" + result.substring(thisEntry, newline) + "]"); pos = newline; continue; } parser.setPos(pos + 4); src = parser.getString(); pos = result.indexOf("DST=", pos); if(pos == -1 || pos > newline) { // Log.w("NetworkLog", "Skipping corrupted entry [" + result.substring(thisEntry, newline) + "]"); pos = newline; continue; } space = result.indexOf(" ", pos); if(space == -1 || space > newline) { // Log.w("NetworkLog", "Skipping corrupted entry [" + result.substring(thisEntry, newline) + "]"); pos = newline; continue; } parser.setPos(pos + 4); dst = parser.getString(); pos = result.indexOf("LEN=", pos); if(pos == -1 || pos > newline) { // Log.w("NetworkLog", "Skipping corrupted entry [" + result.substring(thisEntry, newline) + "]"); pos = newline; continue; } space = result.indexOf(" ", pos); if(space == -1 || space > newline) { // Log.w("NetworkLog", "Skipping corrupted entry [" + result.substring(thisEntry, newline) + "]"); pos = newline; continue; } parser.setPos(pos + 4); len = parser.getInt(); pos = result.indexOf("PROTO=", pos); if(pos == -1 || pos > newline) { // Log.w("NetworkLog", "Skipping corrupted entry [" + result.substring(thisEntry, newline) + "]"); pos = newline; continue; } space = result.indexOf(" ", pos); if(space == -1 || space > newline) { // Log.w("NetworkLog", "Skipping corrupted entry [" + result.substring(thisEntry, newline) + "]"); pos = newline; continue; } parser.setPos(pos + 6); proto = parser.getString(); lastpos = pos; pos = result.indexOf("SPT=", pos); if(pos == -1 || pos > newline) { // no SPT field, probably a broadcast packet spt = 0; pos = lastpos; } else { space = result.indexOf(" ", pos); if(space == -1 || space > newline) { // Log.w("NetworkLog", "Skipping corrupted entry [" + result.substring(thisEntry, newline) + "]"); pos = newline; continue; } parser.setPos(pos + 4); spt = parser.getInt(); } lastpos = pos; pos = result.indexOf("DPT=", pos); if(pos == -1 || pos > newline) { // no DPT field, probably a broadcast packet dpt = 0; pos = lastpos; } else { space = result.indexOf(" ", pos); if(space == -1 || space > newline) { // Log.w("NetworkLog", "Skipping corrupted entry [" + result.substring(thisEntry, newline) + "]"); pos = newline; continue; } parser.setPos(pos + 4); dpt = parser.getInt(); } lastpos = pos; pos = result.indexOf("UID=", pos); if(pos == -1 || pos > newline) { uid = -1; uidString = "-1"; pos = lastpos; } else { parser.setPos(pos + 4); uid = parser.getInt(); parser.setPos(pos + 4); uidString = parser.getString(); } } catch(Exception e) { Log.e("NetworkLog", "Bad data for: [" + result.substring(thisEntry, newline) + "]", e); pos = newline; continue; } if(MyLog.enabled && MyLog.level >= 9) { MyLog.d(9, "Setting map key: src=[" + src + "] spt=" + spt + " dst=[" + dst + "] dpt=" + dpt); } String srcDstMapKey = src + ":" + spt + "->" + dst + ":" + dpt; String dstSrcMapKey = dst + ":" + dpt + "->" + src + ":" + spt; if(MyLog.enabled && MyLog.level >= 10) { MyLog.d(10, "Checking entry for " + uid + " " + srcDstMapKey + " and " + dstSrcMapKey); } Integer srcDstMapUid = logEntriesMap.get(srcDstMapKey); Integer dstSrcMapUid = logEntriesMap.get(dstSrcMapKey); if(uid < 0) { // Unknown uid, retrieve from entries map if(MyLog.enabled && MyLog.level >= 9) { MyLog.d(9, "Unknown uid"); } if(srcDstMapUid == null || dstSrcMapUid == null) { // refresh netstat and try again if(MyLog.enabled && MyLog.level >= 9) { MyLog.d(9, "Refreshing netstat ..."); } initEntriesMap(); srcDstMapUid = logEntriesMap.get(srcDstMapKey); dstSrcMapUid = logEntriesMap.get(dstSrcMapKey); } if(srcDstMapUid == null) { if(MyLog.enabled && MyLog.level >= 9) { MyLog.d(9, "[src-dst] No entry uid for " + uid + " [" + srcDstMapKey + "]"); } if(uid == -1) { if(dstSrcMapUid != null) { if(MyLog.enabled && MyLog.level >= 9) { MyLog.d(9, "[dst-src] Reassigning kernel packet -1 to " + dstSrcMapUid); } uid = dstSrcMapUid; uidString = StringPool.get(dstSrcMapUid); } else { if(MyLog.enabled && MyLog.level >= 9) { MyLog.d(9, "[src-dst] New kernel entry -1 for [" + srcDstMapKey + "]"); } srcDstMapUid = uid; logEntriesMap.put(srcDstMapKey, srcDstMapUid); } } else { if(MyLog.enabled && MyLog.level >= 9) { MyLog.d(9, "[src-dst] New entry " + uid + " for [" + srcDstMapKey + "]"); } srcDstMapUid = uid; logEntriesMap.put(srcDstMapKey, srcDstMapUid); } } else { if(MyLog.enabled && MyLog.level >= 9) { MyLog.d(9, "[src-dst] Found entry uid " + srcDstMapUid + " for " + uid + " [" + srcDstMapKey + "]"); } uid = srcDstMapUid; uidString = StringPool.get(srcDstMapUid); } if(dstSrcMapUid == null) { if(MyLog.enabled && MyLog.level >= 9) { MyLog.d(9, "[dst-src] No entry uid for " + uid + " [" + dstSrcMapKey + "]"); } if(uid == -1) { if(srcDstMapUid != null) { if(MyLog.enabled && MyLog.level >= 9) { MyLog.d(9, "[src-dst] Reassigning kernel packet -1 to " + srcDstMapUid); } uid = srcDstMapUid; uidString = StringPool.get(srcDstMapUid); } else { if(MyLog.enabled && MyLog.level >= 9) { MyLog.d(9, "[dst-src] New kernel entry -1 for [" + dstSrcMapKey + "]"); } dstSrcMapUid = uid; logEntriesMap.put(dstSrcMapKey, dstSrcMapUid); } } else { if(MyLog.enabled && MyLog.level >= 9) { MyLog.d(9, "[dst-src] New entry " + uid + " for [" + dstSrcMapKey + "]"); } dstSrcMapUid = uid; logEntriesMap.put(dstSrcMapKey, dstSrcMapUid); } } else { if(MyLog.enabled && MyLog.level >= 9) { MyLog.d(9, "[dst-src] Found entry uid " + dstSrcMapUid + " for " + uid + " [" + dstSrcMapKey + "]"); } uid = dstSrcMapUid; uidString = StringPool.get(dstSrcMapUid); } } else { if(MyLog.enabled && MyLog.level >= 9) { MyLog.d(9, "Known uid"); } if(srcDstMapUid == null || dstSrcMapUid == null || srcDstMapUid != uid || dstSrcMapUid != uid) { if(MyLog.enabled && MyLog.level >= 9) { MyLog.d(9, "Updating uid " + uid + " to netstat map for " + srcDstMapKey + " and " + dstSrcMapKey); } logEntriesMap.put(srcDstMapKey, uid); logEntriesMap.put(dstSrcMapKey, uid); } } entry = new LogEntry(); entry.uid = uid; entry.uidString = uidString; entry.in = in; entry.out = out; entry.src = src; entry.spt = spt; entry.dst = dst; entry.dpt = dpt; entry.proto = proto; entry.len = len; entry.timestamp = System.currentTimeMillis(); if(MyLog.enabled && MyLog.level >= 10) { MyLog.d(10, "+++ entry: (" + entry.uid + ") in=" + entry.in + " out=" + entry.out + " " + entry.src + ":" + entry.spt + " -> " + entry.dst + ":" + entry.dpt + " proto=" + entry.proto + " len=" + entry.len); } notifyNewEntry(entry); } } private static ApplicationsTracker.AppEntry appEntry; public void notifyNewEntry(LogEntry entry) { appEntry = ApplicationsTracker.uidMap.get(entry.uidString); // check if logfile needs to be opened and that external storage is available if(logWriter == null) { if(android.os.Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED)) { try { logWriter = new PrintWriter(new BufferedWriter(new FileWriter(logfile, true)), true); Log.d("NetworkLog", "Opened " + logfile + " for logging"); } catch(final Exception e) { Log.e("NetworkLog", "Exception opening logfile [" + logfile +"]", e); handler.post(new Runnable() { public void run() { SysUtils.showError(context, getString(R.string.error_default_title), getString(R.string.error_openlogfile) + e.getMessage()); } }); return; } } else { Log.w("NetworkLog", "External storage " + logfile + " not available"); } } if(!entry.isValid()) { return; } // log entry to logfile if(logWriter != null) { logWriter.println(entry.timestamp + "," + entry.in + "," + entry.out + "," + entry.uid + "," + entry.src + "," + entry.spt + "," + entry.dst + "," + entry.dpt + "," + entry.len + "," + entry.proto); } if(MyLog.enabled && MyLog.level >= 5) { MyLog.d(5, "[service] notifyNewEntry: clients: " + clients.size()); } for(int i = clients.size() - 1; i >= 0; i--) { try { if(MyLog.enabled && MyLog.level >= 5) { MyLog.d(5, "[service] Sending entry to " + clients.get(i)); } clients.get(i).send(Message.obtain(null, MSG_BROADCAST_LOG_ENTRY, entry)); } catch(RemoteException e) { // client dead MyLog.d("[service] Dead client " + clients.get(i)); clients.remove(i); } } ThroughputTracker.updateEntry(entry); } public void stopLogger() { if(logger != null) { logger.stop(); } } public void closeLogfile() { if(logWriter != null) { logWriter.close(); logWriter = null; } } public void killLoggerCommand() { if(loggerShell != null) { loggerShell.sendCommand("kill $!", InteractiveShell.IGNORE_OUTPUT); loggerShell.close(); loggerShell = null; } } public boolean startLoggerCommand() { MyLog.d("Starting iptables logger"); if(Iptables.targets == null && Iptables.getTargets(this) == false) { return false; } String binary; if(Iptables.targets.get("LOG") != null) { binary = SysUtils.getGrepBinary(this); if(binary == null) { return false; } } else if(Iptables.targets.get("NFLOG") != null) { binary = SysUtils.getNflogBinary(this); if(binary == null) { return false; } } else { Log.e("NetworkLog", "No supported iptables targets available"); SysUtils.showError(context, context.getResources().getString(R.string.iptables_error_unsupported_title), context.getResources().getString(R.string.iptables_error_missingfeatures_text)); return false; } if(loggerShell == null) { loggerShell = new InteractiveShell("su", "LoggerShell"); loggerShell.start(); if(loggerShell.hasError()) { String error = loggerShell.getError(true); Log.e("NetworkLog", "Error starting logger shell: " + error); SysUtils.showError(context, context.getResources().getString(R.string.error_default_title), "Error starting logger shell: " + error); return false; } } if(Iptables.targets.get("LOG") != null) { switch(NetworkLog.settings.getLogMethod()) { case 1: loggerShell.sendCommand("grep '{NL}' /proc/kmsg &", InteractiveShell.BACKGROUND); break; case 2: loggerShell.sendCommand("cat /proc/kmsg &", InteractiveShell.BACKGROUND); break; default: loggerShell.sendCommand(binary + " '{NL}' /proc/kmsg &", InteractiveShell.BACKGROUND); } } else if(Iptables.targets.get("NFLOG") != null) { loggerShell.sendCommand(binary + " 0 &", InteractiveShell.BACKGROUND); } try { // give logger command a chance to do its thing Thread.sleep(1500); } catch (Exception e) {} if(loggerShell.hasError()) { SysUtils.showError(this, getString(R.string.error_default_title), loggerShell.getError(true)); loggerShell.close(); loggerShell = null; return false; } // ensure logger command didn't exit if(loggerShell.exitval == 0) { loggerShell.sendCommand("kill -0 $!", InteractiveShell.IGNORE_OUTPUT); } if(loggerShell.exitval != 0) { loggerShell.sendCommand("wait $!", InteractiveShell.IGNORE_OUTPUT); Log.e("NetworkLog", "Error starting logger: exit " + loggerShell.exitval); String error = "Error starting logger: exit " + loggerShell.exitval; loggerShell.close(); loggerShell = null; SysUtils.showError(this, getString(R.string.error_default_title), error); return false; } return true; } public boolean startLogging() { killLoggerCommand(); MyLog.d("adding logging rules"); if(!Iptables.addRules(this)) { return false; } if(!startLoggerCommand()) { return false; } logger = new NetworkLogger(); new Thread(logger, "NetworkLogger").start(); startWatchingExternalStorage(); startWatchingRules(); return true; } public void stopLogging() { stopWatchingRules(); Iptables.removeRules(this); stopWatchingExternalStorage(); stopLogger(); closeLogfile(); killLoggerCommand(); } public class NetworkLogger implements Runnable { boolean running = false; public void stop() { running = false; } public void run() { Log.d("NetworkLog", "Network logger " + this + " starting"); String result; running = true; while(true) { while(running && loggerShell.checkForExit() == false) { if(loggerShell.stdoutAvailable()) { result = loggerShell.readLine(); } else { try { Thread.sleep(500); } catch(Exception e) { Log.d("NetworkLog", "NetworkLogger exception while sleeping", e); } continue; } if(running == false) { break; } if(result == null) { Log.d("NetworkLog", "Network logger " + this + " read null; exiting"); break; } parseResult(result); } if(running != false) { Log.d("NetworkLog", "Network logger " + this + " terminated unexpectedly, restarting in 5 seconds"); try { Thread.sleep(5000); } catch (Exception e) { // ignored } if(!startLoggerCommand()) { SysUtils.showError(context, context.getResources().getString(R.string.error_default_title), "Logger process has terminated unexpectedly and was unable to restart"); running = false; } } else { Log.d("NetworkLog", "Network logger " + this + " reached end of loop; exiting"); break; } } } } public static void updateLogfileString() { if(context == null) { return; } try { String file = logfile; if(file == null) { file = NetworkLog.settings.getLogFile(); } logfileString = StringUtils.formatToBytes(new File(file).length()) + "B"; } catch(Exception e) { logfileString = context.getResources().getString(R.string.logfile_bad) + e.getMessage(); } if(instance != null && handler != null) { handler.post(updateNotificationRunner); } if(NetworkLog.handler != null) { NetworkLog.handler.post(NetworkLog.updateStatusRunner); } } BroadcastReceiver mExternalStorageReceiver = null; void updateExternalStorageState() { if(!android.os.Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED)) { // internal storage not mounted if(logWriter != null) { MyLog.d("Stopping logfile logging"); logWriter.close(); logWriter = null; } } } void startWatchingExternalStorage() { if(mExternalStorageReceiver == null) { mExternalStorageReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Log.i("NetworkLog", "External storage: " + intent.getData()); updateExternalStorageState(); } }; } IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_MEDIA_MOUNTED); filter.addAction(Intent.ACTION_MEDIA_REMOVED); registerReceiver(mExternalStorageReceiver, filter); updateExternalStorageState(); } void stopWatchingExternalStorage() { try { unregisterReceiver(mExternalStorageReceiver); } catch (Exception e) { // ignored } } public class RulesWatcher extends Thread { boolean running = false; public RulesWatcher() { setName("RulesWatcher"); } public void stopRunning() { running = false; interrupt(); } @Override public void run() { String md5sum; String lastMd5sum = null; running = true; while(running) { try { Thread.sleep(watchRulesTimeout); } catch(Exception e) { // ignored } if(context == null || running == false) { break; } md5sum = MD5Sum.digestString(Iptables.getRules(context)); if(lastMd5sum == null) { lastMd5sum = md5sum; } else { if(!md5sum.equals(lastMd5sum)) { Log.i("NetworkLog", "Iptables rules changed, reapplying Network Log rules"); Iptables.removeRules(context); Iptables.addRules(context); lastMd5sum = MD5Sum.digestString(Iptables.getRules(context)); } } } } } public static RulesWatcher rulesWatcher; void startWatchingRules() { stopWatchingRules(); if(watchRules) { rulesWatcher = new RulesWatcher(); rulesWatcher.start(); } } void stopWatchingRules() { if(rulesWatcher != null) { rulesWatcher.stopRunning(); rulesWatcher = null; } } }