org.csploit.android.services.UpdateService.java Source code

Java tutorial

Introduction

Here is the source code for org.csploit.android.services.UpdateService.java

Source

/*
 * This file is part of the cSploit.
 *
 * Copyleft of Massimo Dragano aka tux_mind <tux_mind@csploit.org>
 *
 * cSploit is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * cSploit 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with cSploit.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.csploit.android.services;

import android.app.IntentService;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.support.v4.app.NotificationCompat;

import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.apache.commons.compress.compressors.xz.XZCompressorInputStream;
import org.apache.commons.compress.utils.CountingInputStream;
import org.apache.commons.compress.utils.IOUtils;
import org.csploit.android.R;
import org.csploit.android.core.*;
import org.csploit.android.update.CoreUpdate;
import org.csploit.android.update.MsfUpdate;
import org.csploit.android.update.Update;
import org.csploit.android.update.Update.archiveAlgorithm;
import org.csploit.android.core.System;
import org.csploit.android.tools.Raw;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.KeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.concurrent.CancellationException;

public class UpdateService extends IntentService {
    // Intent defines
    public static final String START = "UpdateService.action.START";
    public static final String ERROR = "UpdateService.action.ERROR";
    public static final String DONE = "UpdateService.action.DONE";
    public static final String UPDATE = "UpdateService.data.UPDATE";
    public static final String MESSAGE = "UpdateService.data.MESSAGE";

    // notification defines
    private static final int NOTIFICATION_ID = 1;
    private static final int DOWNLOAD_COMPLETE_CODE = 1;
    private static final int CANCEL_CODE = 2;
    private static final String NOTIFICATION_CANCELLED = "org.csploit.android.services.UpdateService.CANCELLED";

    private boolean mRunning = false;
    private Update mCurrentTask = null;
    final private StringBuffer mErrorOutput = new StringBuffer();
    private Raw.RawReceiver mErrorReceiver = null;

    private NotificationManager mNotificationManager = null;
    private NotificationCompat.Builder mBuilder = null;
    private BroadcastReceiver mReceiver = null;

    public UpdateService() {
        super("UpdateService");
        // prepare error receiver
        mErrorReceiver = new Raw.RawReceiver() {
            @Override
            public void onStart(String command) {
                mErrorOutput.delete(0, mErrorOutput.length());
                mErrorOutput.append("running: ");
                mErrorOutput.append(command);
                mErrorOutput.append("\n");
            }

            @Override
            public void onNewLine(String line) {
                mErrorOutput.append(line);
                mErrorOutput.append("\n");
            }

            @Override
            public void onStderr(String line) {
                mErrorOutput.append(line);
                mErrorOutput.append("\n");
            }

            @Override
            public void onEnd(int exitCode) {
                mErrorOutput.append("exitValue: ");
                mErrorOutput.append(exitCode);
            }
        };
    }

    /**
     * notify activities that some error occurred
     * @param message an error message
     */
    private void sendError(int message) {
        Intent i = new Intent(ERROR);
        i.putExtra(MESSAGE, message);
        sendBroadcast(i);
    }

    /**
     * notify activities that we finished our job
     */
    private void sendDone() {
        Intent i = new Intent(DONE);
        i.putExtra(UPDATE, mCurrentTask);
        sendBroadcast(i);
    }

    /**
     * convert a byte array to it's hexadecimal string representation
     * @param digest the byte array to convert
     * @return the hexadecimal string that represent the given array
     */
    private String digest2string(byte[] digest) {
        StringBuilder sb = new StringBuilder();
        for (byte b : digest) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

    /**
     * open a compressed InputStream
     * @param in the InputStream to decompress
     * @return the InputStream to read from
     * @throws IOException if an I/O error occurs
     */
    private InputStream openCompressedStream(InputStream in) throws IOException {
        if (mCurrentTask.compression == null)
            return in;
        switch (mCurrentTask.compression) {
        default:
        case none:
            return in;
        case gzip:
            return new GzipCompressorInputStream(in);
        case bzip:
            return new BZip2CompressorInputStream(in);
        case xz:
            return new XZCompressorInputStream(in);
        }
    }

    /**
     * open an Archive InputStream
     * @param in the InputStream to the archive
     * @return the ArchiveInputStream to read from
     * @throws IOException if an I/O error occurs
     * @throws java.lang.IllegalStateException if no archive method has been choose
     */
    private ArchiveInputStream openArchiveStream(InputStream in) throws IOException, IllegalStateException {
        switch (mCurrentTask.archiver) {
        case tar:
            return new TarArchiveInputStream(new BufferedInputStream(openCompressedStream(in)));
        case zip:
            return new ZipArchiveInputStream(new BufferedInputStream(openCompressedStream(in)));
        default:
            throw new IllegalStateException("trying to open an archive, but no archive algorithm selected.");
        }
    }

    /**
     * delete a directory recursively
     * @param f the file/directory to delete
     * @throws IOException if cannot delete something
     */
    private void deleteRecursively(File f) throws IOException {
        File[] files = f.listFiles();

        if (files != null) {
            for (File c : files) {
                deleteRecursively(c);
            }
        }
        if (!f.delete())
            throw new IOException("Failed to delete file: " + f);
    }

    /**
     * wipe the destination dir ( rm -rf )
     */
    private void wipe() {
        File outputFile;
        if (mCurrentTask == null || mCurrentTask.outputDir == null || mCurrentTask.outputDir.isEmpty()
                || !(outputFile = new File(mCurrentTask.outputDir)).exists())
            return;

        try {
            System.getTools().raw.run("rm -rf '" + mCurrentTask.outputDir + "'");
            return;
        } catch (Exception e) {
            System.errorLogging(e);
        }

        try {
            deleteRecursively(outputFile);
        } catch (IOException e) {
            System.errorLogging(e);
        }
    }

    /**
     * connect to the notification manager and create a notification builder.
     * it also setup the cancellation Intent for get notified when our notification got cancelled.
     */
    private void setupNotification() {
        // get notification manager
        mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        // get notification builder
        mBuilder = new NotificationCompat.Builder(this);
        // create a broadcast receiver to get actions
        // performed on the notification by the user
        mReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                String action = intent.getAction();
                if (action == null)
                    return;
                // user cancelled our notification
                if (action.equals(NOTIFICATION_CANCELLED)) {
                    mRunning = false;
                }
            }
        };
        // register our receiver
        registerReceiver(mReceiver, new IntentFilter(NOTIFICATION_CANCELLED));
        // set common notification actions
        mBuilder.setDeleteIntent(
                PendingIntent.getBroadcast(this, CANCEL_CODE, new Intent(NOTIFICATION_CANCELLED), 0));
        mBuilder.setContentIntent(PendingIntent.getActivity(this, 0, new Intent(), 0));
    }

    /**
     * if mContentIntent is null delete our notification,
     * else assign it to the notification onClick
     */
    private void finishNotification() {
        boolean errorOccurred;
        Intent contentIntent;

        errorOccurred = mCurrentTask.errorOccurred;
        contentIntent = mCurrentTask.haveIntent() ? mCurrentTask.buildIntent() : null;

        if (errorOccurred || contentIntent == null) {
            Logger.debug("deleting notifications");
            if (mNotificationManager != null)
                mNotificationManager.cancel(NOTIFICATION_ID);
        } else {
            Logger.debug("assign '" + contentIntent.toString() + "' to notification");
            if (mBuilder != null && mNotificationManager != null) {
                mBuilder.setContentIntent(
                        PendingIntent.getActivity(this, DOWNLOAD_COMPLETE_CODE, contentIntent, 0));
                mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());
            }
        }
        if (mReceiver != null)
            unregisterReceiver(mReceiver);
        mReceiver = null;
        mBuilder = null;
        mNotificationManager = null;
    }

    /**
     * wait that a shell terminate or user cancel the notification.
     * @param child the Child returned by {@link org.csploit.android.tools.Tool#async(String)}
     * @param cancellationMessage the message of the CancellationException
     * @throws java.io.IOException when cannot execute shell
     * @throws java.util.concurrent.CancellationException when user cancelled the notification
     */
    private int execShell(Child child, String cancellationMessage)
            throws IOException, CancellationException, InterruptedException {
        while (mRunning && child.running)
            Thread.sleep(10);
        if (!mRunning) {
            child.kill();
            throw new CancellationException(cancellationMessage);
        }

        if ((child.exitValue != 0 || child.signal >= 0) && mErrorOutput.length() > 0)
            for (String line : mErrorOutput.toString().split("\n"))
                if (line.length() > 0)
                    Logger.error(line);

        if (child.signal >= 0) {
            return 128 + child.signal;
        }
        return child.exitValue;
    }

    /**
     * check if an archive is valid by reading it.
     * @throws RuntimeException if trying to run this with no archive
     */
    private void verifyArchiveIntegrity() throws RuntimeException, KeyException {
        File f;
        long total;
        short old_percentage, percentage;
        CountingInputStream counter;
        ArchiveInputStream is;
        byte[] buffer;
        String rootDirectory;

        Logger.info("verifying archive integrity");

        if (mCurrentTask == null || mCurrentTask.path == null)
            throw new RuntimeException("no archive to test");

        mBuilder.setContentTitle(getString(R.string.checking)).setSmallIcon(android.R.drawable.ic_popup_sync)
                .setContentText("").setContentInfo("").setProgress(100, 0, true);
        mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());

        f = new File(mCurrentTask.path);
        try {
            counter = new CountingInputStream(new FileInputStream(f));
        } catch (FileNotFoundException e) {
            throw new RuntimeException(String.format("archive '%s' does not exists", mCurrentTask.path));
        }

        try {
            is = openArchiveStream(counter);
            ArchiveEntry entry;
            buffer = new byte[2048];
            total = f.length();
            old_percentage = -1;
            rootDirectory = null;

            // consume the archive
            while (mRunning && (entry = is.getNextEntry()) != null) {
                if (!mCurrentTask.skipRoot)
                    continue;

                String name = entry.getName();

                if (rootDirectory == null) {
                    if (name.contains("/")) {
                        rootDirectory = name.substring(0, name.indexOf('/'));
                    } else if (entry.isDirectory()) {
                        rootDirectory = name;
                    } else {
                        throw new IOException(
                                String.format("archive '%s' contains files under it's root", mCurrentTask.path));
                    }
                } else {
                    if (!name.startsWith(rootDirectory)) {
                        throw new IOException("multiple directories found in the archive root");
                    }
                }
            }

            while (mRunning && is.read(buffer) > 0) {
                percentage = (short) (((double) counter.getBytesRead() / total) * 100);
                if (percentage != old_percentage) {
                    mBuilder.setProgress(100, percentage, false).setContentInfo(percentage + "%");
                    mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());
                    old_percentage = percentage;
                }
            }
        } catch (IOException e) {
            throw new KeyException("corrupted archive: " + e.getMessage());
        } finally {
            try {
                counter.close();
            } catch (IOException ignore) {
            }
        }

        if (!mRunning)
            throw new CancellationException("archive integrity check cancelled");

        if (mCurrentTask.skipRoot && rootDirectory == null)
            throw new KeyException(String.format("archive '%s' is empty", mCurrentTask.path));
    }

    /**
     * check if mLocalFile exists.
     *
     * @return true if file exists and match md5sum and sha1sum.
     * @throws java.util.concurrent.CancellationException when check is cancelled by user
     * @throws SecurityException bad file permissions
     * @throws IOException when IOException occurs
     * @throws java.security.NoSuchAlgorithmException when digests cannot be created
     * @throws java.security.KeyException when file checksum fails
     */
    private boolean haveLocalFile()
            throws CancellationException, SecurityException, IOException, NoSuchAlgorithmException, KeyException {

        File file = null;
        InputStream reader = null;
        boolean exitForError = true;

        if (mCurrentTask.path == null)
            return false;

        try {
            MessageDigest md5, sha1;
            byte[] buffer;
            int read;
            short percentage, previous_percentage;
            long read_counter, total;

            file = new File(mCurrentTask.path);
            buffer = new byte[4096];
            total = file.length();
            read_counter = 0;
            previous_percentage = -1;

            if (!file.exists() || !file.isFile())
                return false;

            if (!file.canWrite() || !file.canRead()) {
                read = -1;
                try {
                    read = System.getTools().raw.run(String.format("chmod 777 '%s'", mCurrentTask.path));
                } catch (Exception e) {
                    System.errorLogging(e);
                }
                if (read != 0)
                    throw new SecurityException(String.format("bad file permissions for '%s', chmod returned: %d",
                            mCurrentTask.path, read));
            }

            if (mCurrentTask.md5 != null || mCurrentTask.sha1 != null) {
                mBuilder.setContentTitle(getString(R.string.checking))
                        .setSmallIcon(android.R.drawable.ic_popup_sync).setContentText("")
                        .setProgress(100, 0, false);
                mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());

                md5 = (mCurrentTask.md5 != null ? MessageDigest.getInstance("MD5") : null);
                sha1 = (mCurrentTask.sha1 != null ? MessageDigest.getInstance("SHA-1") : null);

                reader = new FileInputStream(file);
                while (mRunning && (read = reader.read(buffer)) != -1) {
                    if (md5 != null)
                        md5.update(buffer, 0, read);
                    if (sha1 != null)
                        sha1.update(buffer, 0, read);

                    read_counter += read;

                    percentage = (short) (((double) read_counter / total) * 100);
                    if (percentage != previous_percentage) {
                        mBuilder.setProgress(100, percentage, false).setContentInfo(percentage + "%");
                        mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());
                        previous_percentage = percentage;
                    }
                }
                reader.close();
                reader = null;
                if (!mRunning) {
                    exitForError = false;
                    throw new CancellationException("local file check cancelled");
                }
                if (md5 != null && !mCurrentTask.md5.equals(digest2string(md5.digest())))
                    throw new KeyException("wrong MD5");
                if (sha1 != null && !mCurrentTask.sha1.equals(digest2string(sha1.digest())))
                    throw new KeyException("wrong SHA-1");
                Logger.info(String.format("checksum ok: '%s'", mCurrentTask.path));
            } else if (mCurrentTask.archiver != null) {
                verifyArchiveIntegrity();
            }
            Logger.info(String.format("file already exists: '%s'", mCurrentTask.path));
            mBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done)
                    .setContentTitle(getString(R.string.update_available))
                    .setContentText(getString(R.string.click_here_to_upgrade)).setProgress(0, 0, false) // remove progress bar
                    .setAutoCancel(true);
            exitForError = false;
            return true;
        } finally {
            if (exitForError && file != null && file.exists() && !file.delete())
                Logger.error(String.format("cannot delete local file '%s'", mCurrentTask.path));
            try {
                if (reader != null)
                    reader.close();
            } catch (IOException e) {
                System.errorLogging(e);
            }
        }
    }

    /**
     * download mCurrentTask.url to mCurrentTask.path
     *
     * @throws KeyException when MD5 or SHA1 sum fails
     * @throws IOException when IOError occurs
     * @throws NoSuchAlgorithmException when required digest algorithm is not available
     * @throws CancellationException when user cancelled the download via notification
     */
    private void downloadFile()
            throws SecurityException, KeyException, IOException, NoSuchAlgorithmException, CancellationException {
        if (mCurrentTask.url == null || mCurrentTask.path == null)
            return;

        File file = null;
        FileOutputStream writer = null;
        InputStream reader = null;
        HttpURLConnection connection = null;
        boolean exitForError = true;

        try {
            MessageDigest md5, sha1;
            URL url;
            byte[] buffer;
            int read;
            short percentage, previous_percentage;
            long downloaded, total;

            mBuilder.setContentTitle(getString(R.string.downloading_update))
                    .setContentText(getString(R.string.connecting))
                    .setSmallIcon(android.R.drawable.stat_sys_download).setProgress(100, 0, true);
            mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());

            md5 = (mCurrentTask.md5 != null ? MessageDigest.getInstance("MD5") : null);
            sha1 = (mCurrentTask.sha1 != null ? MessageDigest.getInstance("SHA-1") : null);
            buffer = new byte[4096];
            file = new File(mCurrentTask.path);

            if (file.exists() && file.isFile())
                //noinspection ResultOfMethodCallIgnored
                file.delete();

            HttpURLConnection.setFollowRedirects(true);
            url = new URL(mCurrentTask.url);
            connection = (HttpURLConnection) url.openConnection();

            connection.connect();

            writer = new FileOutputStream(file);
            reader = connection.getInputStream();

            total = connection.getContentLength();
            read = connection.getResponseCode();

            if (read != 200)
                throw new IOException(
                        String.format("cannot download '%s': responseCode: %d", mCurrentTask.url, read));

            downloaded = 0;
            previous_percentage = -1;

            mBuilder.setContentText(file.getName());
            mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());

            Logger.info(String.format("downloading '%s' to '%s'", mCurrentTask.url, mCurrentTask.path));

            while (mRunning && (read = reader.read(buffer)) != -1) {
                writer.write(buffer, 0, read);
                if (md5 != null)
                    md5.update(buffer, 0, read);
                if (sha1 != null)
                    sha1.update(buffer, 0, read);

                if (total >= 0) {
                    downloaded += read;

                    percentage = (short) (((double) downloaded / total) * 100);

                    if (percentage != previous_percentage) {
                        mBuilder.setProgress(100, percentage, false).setContentInfo(percentage + "%");
                        mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());
                        previous_percentage = percentage;
                    }
                }
            }

            if (!mRunning)
                throw new CancellationException("download cancelled");

            Logger.info("download finished successfully");

            if (md5 != null || sha1 != null) {
                if (md5 != null && !mCurrentTask.md5.equals(digest2string(md5.digest()))) {
                    throw new KeyException("wrong MD5");
                } else if (sha1 != null && !mCurrentTask.sha1.equals(digest2string(sha1.digest()))) {
                    throw new KeyException("wrong SHA-1");
                }
            } else if (mCurrentTask.archiver != null) {
                verifyArchiveIntegrity();
            }

            exitForError = false;

        } finally {
            if (exitForError && file != null && file.exists() && !file.delete())
                Logger.error(String.format("cannot delete file '%s'", mCurrentTask.path));
            try {
                if (writer != null)
                    writer.close();
                if (reader != null)
                    reader.close();
                if (connection != null)
                    connection.disconnect();
            } catch (IOException e) {
                System.errorLogging(e);
            }
        }
    }

    /**
     * extract an archive into a directory
     *
     * @throws IOException if some I/O error occurs
     * @throws java.util.concurrent.CancellationException if task is cancelled by user
     * @throws java.lang.InterruptedException when the the running thread get cancelled.
     */
    private void extract()
            throws RuntimeException, IOException, InterruptedException, ChildManager.ChildNotStartedException {
        ArchiveInputStream is = null;
        ArchiveEntry entry;
        CountingInputStream counter;
        OutputStream outputStream = null;
        File f, inFile;
        File[] list;
        String name;
        String envPath;
        final StringBuffer sb = new StringBuffer();
        int mode;
        int count;
        long total;
        boolean isTar, r, w, x, isElf, isScript;
        short percentage, old_percentage;
        Child which;

        if (mCurrentTask.path == null || mCurrentTask.outputDir == null)
            return;

        mBuilder.setContentTitle(getString(R.string.extracting)).setContentText("").setContentInfo("")
                .setSmallIcon(android.R.drawable.ic_popup_sync).setProgress(100, 0, false);
        mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());

        Logger.info(String.format("extracting '%s' to '%s'", mCurrentTask.path, mCurrentTask.outputDir));

        envPath = null;
        which = null;

        try {
            if (mCurrentTask.fixShebang) {
                which = System.getTools().raw.async("which env", new Raw.RawReceiver() {
                    @Override
                    public void onNewLine(String line) {
                        sb.delete(0, sb.length());
                        sb.append(line);
                    }
                });
            }

            inFile = new File(mCurrentTask.path);
            total = inFile.length();
            counter = new CountingInputStream(new FileInputStream(inFile));
            is = openArchiveStream(counter);
            isTar = mCurrentTask.archiver.equals(archiveAlgorithm.tar);
            old_percentage = -1;

            f = new File(mCurrentTask.outputDir);
            if (f.exists() && f.isDirectory() && (list = f.listFiles()) != null && list.length > 2)
                wipe();

            if (mCurrentTask.fixShebang) {
                if (execShell(which, "cancelled while retrieving env path") != 0) {
                    throw new RuntimeException("cannot find 'env' executable");
                }
                envPath = sb.toString();
            }

            while (mRunning && (entry = is.getNextEntry()) != null) {
                name = entry.getName().replaceFirst("^\\./?", "");

                if (mCurrentTask.skipRoot) {
                    if (name.contains("/"))
                        name = name.substring(name.indexOf('/') + 1);
                    else if (entry.isDirectory())
                        continue;
                }

                f = new File(mCurrentTask.outputDir, name);

                isElf = isScript = false;

                if (entry.isDirectory()) {
                    if (!f.exists()) {
                        if (!f.mkdirs()) {
                            throw new IOException(
                                    String.format("Couldn't create directory '%s'.", f.getAbsolutePath()));
                        }
                    }
                } else {
                    byte[] buffer = null;
                    byte[] writeMe = null;

                    outputStream = new FileOutputStream(f);

                    // check il file is an ELF or a script
                    if ((!isTar || mCurrentTask.fixShebang) && entry.getSize() > 4) {
                        writeMe = buffer = new byte[4];

                        IOUtils.readFully(is, buffer);

                        if (buffer[0] == 0x7F && buffer[1] == 0x45 && buffer[2] == 0x4C && buffer[3] == 0x46) {
                            isElf = true;
                        } else if (buffer[0] == '#' && buffer[1] == '!') {
                            isScript = true;

                            ByteArrayOutputStream firstLine = new ByteArrayOutputStream();
                            int newline = -1;

                            // assume that '\n' is more far then 4 chars.
                            firstLine.write(buffer);
                            buffer = new byte[1024];
                            count = 0;

                            while (mRunning && (count = is.read(buffer)) >= 0
                                    && (newline = Arrays.binarySearch(buffer, 0, count, (byte) 0x0A)) < 0) {
                                firstLine.write(buffer, 0, count);
                            }

                            if (!mRunning) {
                                throw new CancellationException("cancelled while searching for newline.");
                            } else if (count < 0) {
                                newline = count = 0;
                            } else if (newline < 0) {
                                newline = count;
                            }

                            firstLine.write(buffer, 0, newline);
                            firstLine.close();

                            byte[] newFirstLine = new String(firstLine.toByteArray())
                                    .replace("/usr/bin/env", envPath).getBytes();

                            writeMe = new byte[newFirstLine.length + (count - newline)];

                            java.lang.System.arraycopy(newFirstLine, 0, writeMe, 0, newFirstLine.length);
                            java.lang.System.arraycopy(buffer, newline, writeMe, newFirstLine.length,
                                    count - newline);
                        }
                    }

                    if (writeMe != null) {
                        outputStream.write(writeMe);
                    }

                    IOUtils.copy(is, outputStream);

                    outputStream.close();
                    outputStream = null;

                    percentage = (short) (((double) counter.getBytesRead() / total) * 100);
                    if (percentage != old_percentage) {
                        mBuilder.setProgress(100, percentage, false).setContentInfo(percentage + "%");
                        mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());
                        old_percentage = percentage;
                    }
                }
                // Zip does not store file permissions.
                if (isTar) {
                    mode = ((TarArchiveEntry) entry).getMode();

                    r = (mode & 0400) > 0;
                    w = (mode & 0200) > 0;
                    x = (mode & 0100) > 0;
                } else if (isElf || isScript) {
                    r = w = x = true;
                } else {
                    continue;
                }

                if (!f.setExecutable(x, true)) {
                    Logger.warning(String.format("cannot set executable permission of '%s'", name));
                }

                if (!f.setWritable(w, true)) {
                    Logger.warning(String.format("cannot set writable permission of '%s'", name));
                }

                if (!f.setReadable(r, true)) {
                    Logger.warning(String.format("cannot set readable permission of '%s'", name));
                }
            }

            if (!mRunning)
                throw new CancellationException("extraction cancelled.");

            Logger.info("extraction completed");

            f = new File(mCurrentTask.outputDir, ".nomedia");
            if (f.createNewFile())
                Logger.info(".nomedia created");

            mBuilder.setContentInfo("").setProgress(100, 100, true);
            mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());
        } finally {
            if (is != null)
                is.close();
            if (outputStream != null)
                outputStream.close();
        }
    }

    /**
     * install gems required by the MSF
     */
    private void installGems() throws RuntimeException, IOException, InterruptedException,
            ChildManager.ChildNotStartedException, ChildManager.ChildDiedException {
        String msfPath = System.getMsfPath();

        mBuilder.setContentTitle(getString(R.string.installing_gems))
                .setContentText(getString(R.string.installing_bundle)).setContentInfo("")
                .setSmallIcon(android.R.drawable.stat_sys_download).setProgress(100, 0, true);
        mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());
        Child bundleInstallTask;

        bundleInstallTask = null;

        try {
            // install bundle gem, required for install msf
            if (System.getTools().ruby.run("which bundle") != 0) {
                bundleInstallTask = System.getTools().ruby.async("gem install bundle", mErrorReceiver);
            }

            mBuilder.setContentText(getString(R.string.installing_msf_gems));
            mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());

            // remove cache version file
            new File(msfPath, "Gemfile.lock").delete();

            if (bundleInstallTask != null && execShell(bundleInstallTask, "cancelled while install bundle") != 0)
                throw new RuntimeException("cannot install bundle");

            bundleInstallTask = System.getTools().ruby.async(String.format(
                    "bundle install --verbose --local --gemfile '%s/Gemfile' --without development test", msfPath),
                    mErrorReceiver);

            // install gem required by msf using bundle
            if (execShell(bundleInstallTask, "cancelled on bundle install") != 0)
                throw new RuntimeException("cannot install msf gems");
        } finally {
            if (bundleInstallTask != null && bundleInstallTask.running)
                bundleInstallTask.kill(2);
        }
    }

    private void clearGemsCache() {

        if (!System.getSettings().getBoolean("MSF_ENABLED", true))
            return;

        try {
            System.getTools().raw
                    .run(String.format("rm -rf '%s/lib/ruby/gems/1.9.1/cache/'", System.getRubyPath()));
        } catch (Exception e) {
            System.errorLogging(e);
        }
    }

    private void deleteTemporaryFiles() {
        if (mCurrentTask.outputDir == null || mCurrentTask.path == null || mCurrentTask.path.isEmpty())
            return;
        if (!(new File(mCurrentTask.path)).delete())
            Logger.error(String.format("cannot delete temporary file '%s'", mCurrentTask.path));
    }

    private void createVersionFile() throws IOException {
        File f;
        FileOutputStream fos = null;

        if (mCurrentTask.outputDir == null)
            return;

        if (mCurrentTask.version == null || mCurrentTask.version.isEmpty()) {
            Logger.warning("version string not found");
            return;
        }

        try {
            f = new File(mCurrentTask.outputDir, "VERSION");
            fos = new FileOutputStream(f);
            fos.write(mCurrentTask.version.getBytes());
        } catch (Exception e) {
            System.errorLogging(e);
            throw new IOException("cannot create VERSION file");
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (Exception e) {
                    // ignored
                }
            }
        }
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        mCurrentTask = (Update) intent.getSerializableExtra(UPDATE);
        boolean exitForError = true;

        if (mCurrentTask == null) {
            Logger.error("received null update");
            return;
        }

        mRunning = true;

        try {
            setupNotification();

            mCurrentTask.errorOccurred = true;
            if (!haveLocalFile())
                downloadFile();

            if (mCurrentTask instanceof CoreUpdate)
                System.shutdownCoreDaemon();

            extract();

            if (mCurrentTask instanceof MsfUpdate)
                installGems();

            deleteTemporaryFiles();
            createVersionFile();

            mCurrentTask.errorOccurred = exitForError = false;

            sendDone();
        } catch (SecurityException e) {
            sendError(R.string.bad_permissions);
            Logger.warning(e.getClass().getName() + ": " + e.getMessage());
        } catch (KeyException e) {
            sendError(R.string.checksum_failed);
            Logger.warning(e.getClass().getName() + ": " + e.getMessage());
        } catch (CancellationException e) {
            sendError(R.string.update_cancelled);
            Logger.warning(e.getClass().getName() + ": " + e.getMessage());
        } catch (NoSuchAlgorithmException | RuntimeException | ChildManager.ChildDiedException
                | ChildManager.ChildNotStartedException | InterruptedException | IOException e) {
            sendError(R.string.error_occured);
            System.errorLogging(e);
        } finally {
            if (exitForError) {
                if (mCurrentTask instanceof MsfUpdate)
                    clearGemsCache();
                if (!(mCurrentTask instanceof CoreUpdate))
                    wipe();
            }
            stopSelf();
            mRunning = false;
        }
    }

    @Override
    public void onDestroy() {
        finishNotification();
        super.onDestroy();
    }
}